enhance: account migration (#10592)
* copy block and mute then create follow and unfollow jobs * copy block and mute and update lists when detecting an account has moved * no need to care promise orders * refactor updating actor and target * automatically accept if a locked account had accepted an old account * fix exception format * prevent the old account from calling some endpoints * do not unfollow when moving * adjust following and follower counts * check movedToUri when receiving a follow request * skip if no need to adjust * Revert "disable account migration" This reverts commit2321214c98. * fix translation specifier * fix checking alsoKnownAs and uri * fix updating account * fix refollowing locked account * decrease followersCount if followed by the old account * adjust following and followers counts when unfollowing * fix copying mutings * prohibit moved account from moving again * fix move service * allow app creation after moving * fix lint * remove unnecessary field * fix cache update * add e2e test * add e2e test of accepting the new account automatically * force follow if any error happens * remove unnecessary joins * use Array.map instead of for const of * ユーザーリストの移行は追加のみを行う * nanka iroiro * fix misskey-js? * ✌️ * 移行を行ったアカウントからのフォローリクエストの自動許可を調整 * newUriを外に出す * newUriを外に出す2 * clean up * fix newUri * prevent moving if the destination account has already moved * set alsoKnownAs via /i/update * fix database initialization * add return type * prohibit updating alsoKnownAs after moving * skip to add to alsoKnownAs if toUrl is known * skip adding to the list if it already has * use Acct.parse instead * rename error code * 🎨 * 制限を5から10に緩和 * movedTo(Uri), alsoKnownAsはユーザーidを返すように * test api res * fix * 元アカウントはミュートし続ける * 🎨 * unfollow * fix * getUserUriをUserEntityServiceに * ? * job! * 🎨 * instance => server * accountMovedShort, forbiddenBecauseYouAreMigrated * accountMovedShort * fix test * import, pin禁止 * 実績を凍結する * clean up * ✌️ * change message * ブロック, フォロー, ミュート, リストのインポートファイルの制限を32MiBに * Revert "ブロック, フォロー, ミュート, リストのインポートファイルの制限を32MiBに" This reverts commit3bd7be35d8. * validateAlsoKnownAs * 移行後2時間以内はインポート可能なファイルサイズを拡大 * clean up * どうせactorをupdatePersonで更新するならupdatePersonしか移行処理を発行しないことにする * handle error? * リモートからの移行処理の条件を是正 * log, port * fix * fix * enhance(dev): non-production環境でhttpサーバー間でもユーザー、ノートの連合が可能なように * refactor (use checkHttps) * MISSKEY_WEBFINGER_USE_HTTP * Environment Variable readme * NEVER USE IN PRODUCTION * fix punyHost * fix indent * fix * experimental --------- Co-authored-by: tamaina <tamaina@hotmail.co.jp> Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
		| @@ -11,7 +11,7 @@ import * as url from '@/misc/prelude/url.js'; | ||||
| import type { Config } from '@/config.js'; | ||||
| import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; | ||||
| import { QueueService } from '@/core/QueueService.js'; | ||||
| import type { LocalUser, User } from '@/models/entities/User.js'; | ||||
| import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; | ||||
| import { UserKeypairService } from '@/core/UserKeypairService.js'; | ||||
| import type { Following } from '@/models/entities/Following.js'; | ||||
| import { countIf } from '@/misc/prelude/array.js'; | ||||
| @@ -630,7 +630,7 @@ export class ActivityPubServerService { | ||||
| 					id: request.params.followee, | ||||
| 					host: Not(IsNull()), | ||||
| 				}), | ||||
| 			]); | ||||
| 			]) as [LocalUser | RemoteUser | null, LocalUser | RemoteUser | null]; | ||||
|  | ||||
| 			if (follower == null || followee == null) { | ||||
| 				reply.code(404); | ||||
| @@ -665,7 +665,7 @@ export class ActivityPubServerService { | ||||
| 					id: followRequest.followeeId, | ||||
| 					host: Not(IsNull()), | ||||
| 				}), | ||||
| 			]); | ||||
| 			]) as [LocalUser | RemoteUser | null, LocalUser | RemoteUser | null]; | ||||
|  | ||||
| 			if (follower == null || followee == null) { | ||||
| 				reply.code(404); | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js'; | ||||
| import type { User } from '@/models/entities/User.js'; | ||||
| import * as Acct from '@/misc/acct.js'; | ||||
| import { NodeinfoServerService } from './NodeinfoServerService.js'; | ||||
| import { UserEntityService } from '@/core/entities/UserEntityService.js'; | ||||
| import type { FindOptionsWhere } from 'typeorm'; | ||||
| import { bindThis } from '@/decorators.js'; | ||||
| import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; | ||||
| @@ -23,6 +24,7 @@ export class WellKnownServerService { | ||||
| 		private usersRepository: UsersRepository, | ||||
|  | ||||
| 		private nodeinfoServerService: NodeinfoServerService, | ||||
| 		private userEntityService: UserEntityService, | ||||
| 	) { | ||||
| 		//this.createServer = this.createServer.bind(this); | ||||
| 	} | ||||
| @@ -130,7 +132,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => { | ||||
| 			const self = { | ||||
| 				rel: 'self', | ||||
| 				type: 'application/activity+json', | ||||
| 				href: `${this.config.url}/users/${user.id}`, | ||||
| 				href: this.userEntityService.genLocalUserUri(user.id), | ||||
| 			}; | ||||
| 			const profilePage = { | ||||
| 				rel: 'http://webfinger.net/rel/profile-page', | ||||
|   | ||||
| @@ -261,6 +261,17 @@ export class ApiCallService implements OnApplicationShutdown { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (ep.meta.prohibitMoved) { | ||||
| 			if (user?.movedToUri) { | ||||
| 				throw new ApiError({ | ||||
| 					message: 'You have moved your account.', | ||||
| 					code: 'YOUR_ACCOUNT_MOVED', | ||||
| 					id: '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31', | ||||
| 					httpStatusCode: 403, | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) { | ||||
| 			const myRoles = await this.roleService.getUserRoles(user!.id); | ||||
| 			if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) { | ||||
|   | ||||
| @@ -223,7 +223,6 @@ import * as ep___i_unpin from './endpoints/i/unpin.js'; | ||||
| import * as ep___i_updateEmail from './endpoints/i/update-email.js'; | ||||
| import * as ep___i_update from './endpoints/i/update.js'; | ||||
| import * as ep___i_move from './endpoints/i/move.js'; | ||||
| import * as ep___i_knownAs from './endpoints/i/known-as.js'; | ||||
| import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js'; | ||||
| import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; | ||||
| import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; | ||||
| @@ -560,7 +559,6 @@ const $i_unpin: Provider = { provide: 'ep:i/unpin', useClass: ep___i_unpin.defau | ||||
| const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default }; | ||||
| const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default }; | ||||
| const $i_move: Provider = { provide: 'ep:i/move', useClass: ep___i_move.default }; | ||||
| const $i_knownAs: Provider = { provide: 'ep:i/known-as', useClass: ep___i_knownAs.default }; | ||||
| const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default }; | ||||
| const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default }; | ||||
| const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default }; | ||||
| @@ -901,7 +899,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention | ||||
| 		$i_updateEmail, | ||||
| 		$i_update, | ||||
| 		$i_move, | ||||
| 		$i_knownAs, | ||||
| 		$i_webhooks_create, | ||||
| 		$i_webhooks_list, | ||||
| 		$i_webhooks_show, | ||||
| @@ -1236,7 +1233,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention | ||||
| 		$i_updateEmail, | ||||
| 		$i_update, | ||||
| 		$i_move, | ||||
| 		$i_knownAs, | ||||
| 		$i_webhooks_create, | ||||
| 		$i_webhooks_list, | ||||
| 		$i_webhooks_show, | ||||
|   | ||||
| @@ -223,7 +223,6 @@ import * as ep___i_unpin from './endpoints/i/unpin.js'; | ||||
| import * as ep___i_updateEmail from './endpoints/i/update-email.js'; | ||||
| import * as ep___i_update from './endpoints/i/update.js'; | ||||
| import * as ep___i_move from './endpoints/i/move.js'; | ||||
| import * as ep___i_knownAs from './endpoints/i/known-as.js'; | ||||
| import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js'; | ||||
| import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; | ||||
| import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; | ||||
| @@ -557,8 +556,7 @@ const eps = [ | ||||
| 	['i/unpin', ep___i_unpin], | ||||
| 	['i/update-email', ep___i_updateEmail], | ||||
| 	['i/update', ep___i_update], | ||||
| 	//['i/move', ep___i_move], | ||||
| 	//['i/known-as', ep___i_knownAs], | ||||
| 	['i/move', ep___i_move], | ||||
| 	['i/webhooks/create', ep___i_webhooks_create], | ||||
| 	['i/webhooks/list', ep___i_webhooks_list], | ||||
| 	['i/webhooks/show', ep___i_webhooks_show], | ||||
| @@ -704,6 +702,12 @@ export interface IEndpointMeta { | ||||
|  | ||||
| 	readonly requireRolePolicy?: keyof RolePolicies; | ||||
|  | ||||
| 	/** | ||||
| 	 * 引っ越し済みのユーザーによるリクエストを禁止するか | ||||
| 	 * 省略した場合は false として解釈されます。 | ||||
| 	 */ | ||||
| 	readonly prohibitMoved?: boolean; | ||||
|  | ||||
| 	/** | ||||
| 	 * エンドポイントのリミテーションに関するやつ | ||||
| 	 * 省略した場合はリミテーションは無いものとして解釈されます。 | ||||
|   | ||||
| @@ -13,6 +13,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:account', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -11,6 +11,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:account', | ||||
|  | ||||
| 	errors: { | ||||
| @@ -71,7 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
|  | ||||
| 		@Inject(DI.userListsRepository) | ||||
| 		private userListsRepository: UserListsRepository, | ||||
| 		 | ||||
|  | ||||
| 		private antennaEntityService: AntennaEntityService, | ||||
| 		private globalEventService: GlobalEventService, | ||||
| 	) { | ||||
|   | ||||
| @@ -13,6 +13,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:channels', | ||||
|  | ||||
| 	limit: { | ||||
|   | ||||
| @@ -10,6 +10,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:channels', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -11,6 +11,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:channels', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -9,6 +9,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:channels', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -10,6 +10,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:channels', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -13,6 +13,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:account', | ||||
|  | ||||
| 	limit: { | ||||
|   | ||||
| @@ -12,6 +12,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:account', | ||||
|  | ||||
| 	res: { | ||||
| @@ -57,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 			if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) { | ||||
| 				throw new ApiError(meta.errors.tooManyClips); | ||||
| 			} | ||||
| 	 | ||||
|  | ||||
| 			const clip = await this.clipsRepository.insert({ | ||||
| 				id: this.idService.genId(), | ||||
| 				createdAt: new Date(), | ||||
|   | ||||
| @@ -10,6 +10,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:clip-favorite', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -10,6 +10,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:account', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -9,6 +9,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:clip-favorite', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -10,6 +10,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:account', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -15,6 +15,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	limit: { | ||||
| 		duration: ms('1hour'), | ||||
| 		max: 120, | ||||
|   | ||||
| @@ -19,6 +19,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:drive', | ||||
| } as const; | ||||
|  | ||||
|   | ||||
| @@ -11,6 +11,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:flash', | ||||
|  | ||||
| 	limit: { | ||||
|   | ||||
| @@ -10,6 +10,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:flash-likes', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -9,6 +9,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:flash-likes', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -10,6 +10,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:flash', | ||||
|  | ||||
| 	limit: { | ||||
|   | ||||
| @@ -19,6 +19,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:following', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -13,6 +13,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:gallery', | ||||
|  | ||||
| 	limit: { | ||||
|   | ||||
| @@ -10,6 +10,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:gallery-likes', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -9,6 +9,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:gallery-likes', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -11,6 +11,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:gallery', | ||||
|  | ||||
| 	limit: { | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService | ||||
|  | ||||
| export const meta = { | ||||
| 	requireCredential: true, | ||||
| 	prohibitMoved: true, | ||||
| } as const; | ||||
|  | ||||
| export const paramDef = { | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; | ||||
| import ms from 'ms'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import { QueueService } from '@/core/QueueService.js'; | ||||
| import { AccountMoveService } from '@/core/AccountMoveService.js'; | ||||
| import type { DriveFilesRepository } from '@/models/index.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import { ApiError } from '../../error.js'; | ||||
| @@ -9,6 +10,7 @@ import { ApiError } from '../../error.js'; | ||||
| export const meta = { | ||||
| 	secure: true, | ||||
| 	requireCredential: true, | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	limit: { | ||||
| 		duration: ms('1hour'), | ||||
| @@ -58,15 +60,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 		private driveFilesRepository: DriveFilesRepository, | ||||
|  | ||||
| 		private queueService: QueueService, | ||||
| 		private accountMoveService: AccountMoveService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); | ||||
|  | ||||
| 			if (file == null) throw new ApiError(meta.errors.noSuchFile); | ||||
| 			//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); | ||||
| 			if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); | ||||
| 			if (file.size === 0) throw new ApiError(meta.errors.emptyFile); | ||||
|  | ||||
| 			const checkMoving = await this.accountMoveService.validateAlsoKnownAs( | ||||
| 				me, | ||||
| 				(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), | ||||
| 				true | ||||
| 			); | ||||
| 			if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); | ||||
|  | ||||
| 			this.queueService.createImportBlockingJob(me, file.id); | ||||
| 		}); | ||||
| 	} | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; | ||||
| import ms from 'ms'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import { QueueService } from '@/core/QueueService.js'; | ||||
| import { AccountMoveService } from '@/core/AccountMoveService.js'; | ||||
| import type { DriveFilesRepository } from '@/models/index.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import { ApiError } from '../../error.js'; | ||||
| @@ -9,6 +10,7 @@ import { ApiError } from '../../error.js'; | ||||
| export const meta = { | ||||
| 	secure: true, | ||||
| 	requireCredential: true, | ||||
| 	prohibitMoved: true, | ||||
| 	limit: { | ||||
| 		duration: ms('1hour'), | ||||
| 		max: 1, | ||||
| @@ -57,15 +59,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 		private driveFilesRepository: DriveFilesRepository, | ||||
|  | ||||
| 		private queueService: QueueService, | ||||
| 		private accountMoveService: AccountMoveService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); | ||||
|  | ||||
| 			if (file == null) throw new ApiError(meta.errors.noSuchFile); | ||||
| 			//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); | ||||
| 			if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); | ||||
| 			if (file.size === 0) throw new ApiError(meta.errors.emptyFile); | ||||
|  | ||||
| 			const checkMoving = await this.accountMoveService.validateAlsoKnownAs( | ||||
| 				me, | ||||
| 				(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), | ||||
| 				true | ||||
| 			); | ||||
| 			if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); | ||||
|  | ||||
| 			this.queueService.createImportFollowingJob(me, file.id); | ||||
| 		}); | ||||
| 	} | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; | ||||
| import ms from 'ms'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import { QueueService } from '@/core/QueueService.js'; | ||||
| import { AccountMoveService } from '@/core/AccountMoveService.js'; | ||||
| import type { DriveFilesRepository } from '@/models/index.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import { ApiError } from '../../error.js'; | ||||
| @@ -9,6 +10,7 @@ import { ApiError } from '../../error.js'; | ||||
| export const meta = { | ||||
| 	secure: true, | ||||
| 	requireCredential: true, | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	limit: { | ||||
| 		duration: ms('1hour'), | ||||
| @@ -58,15 +60,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 		private driveFilesRepository: DriveFilesRepository, | ||||
|  | ||||
| 		private queueService: QueueService, | ||||
| 		private accountMoveService: AccountMoveService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); | ||||
|  | ||||
| 			if (file == null) throw new ApiError(meta.errors.noSuchFile); | ||||
| 			//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); | ||||
| 			if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); | ||||
| 			if (file.size === 0) throw new ApiError(meta.errors.emptyFile); | ||||
|  | ||||
| 			const checkMoving = await this.accountMoveService.validateAlsoKnownAs( | ||||
| 				me, | ||||
| 				(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), | ||||
| 				true | ||||
| 			); | ||||
| 			if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); | ||||
|  | ||||
| 			this.queueService.createImportMutingJob(me, file.id); | ||||
| 		}); | ||||
| 	} | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; | ||||
| import ms from 'ms'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import { QueueService } from '@/core/QueueService.js'; | ||||
| import { AccountMoveService } from '@/core/AccountMoveService.js'; | ||||
| import type { DriveFilesRepository } from '@/models/index.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import { ApiError } from '../../error.js'; | ||||
| @@ -9,6 +10,7 @@ import { ApiError } from '../../error.js'; | ||||
| export const meta = { | ||||
| 	secure: true, | ||||
| 	requireCredential: true, | ||||
| 	prohibitMoved: true, | ||||
| 	limit: { | ||||
| 		duration: ms('1hour'), | ||||
| 		max: 1, | ||||
| @@ -57,15 +59,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 		private driveFilesRepository: DriveFilesRepository, | ||||
|  | ||||
| 		private queueService: QueueService, | ||||
| 		private accountMoveService: AccountMoveService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); | ||||
|  | ||||
| 			if (file == null) throw new ApiError(meta.errors.noSuchFile); | ||||
| 			//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); | ||||
| 			if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile); | ||||
| 			if (file.size === 0) throw new ApiError(meta.errors.emptyFile); | ||||
|  | ||||
| 			const checkMoving = await this.accountMoveService.validateAlsoKnownAs( | ||||
| 				me, | ||||
| 				(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), | ||||
| 				true | ||||
| 			); | ||||
| 			if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); | ||||
|  | ||||
| 			this.queueService.createImportUserListsJob(me, file.id); | ||||
| 		}); | ||||
| 	} | ||||
|   | ||||
| @@ -1,92 +0,0 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
| import ms from 'ms'; | ||||
|  | ||||
| import { User } from '@/models/entities/User.js'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import { ApiError } from '@/server/api/error.js'; | ||||
|  | ||||
| import { AccountMoveService } from '@/core/AccountMoveService.js'; | ||||
| import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; | ||||
| import { UserEntityService } from '@/core/entities/UserEntityService.js'; | ||||
| import { ApiLoggerService } from '@/server/api/ApiLoggerService.js'; | ||||
|  | ||||
| export const meta = { | ||||
| 	tags: ['users'], | ||||
|  | ||||
| 	secure: true, | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	limit: { | ||||
| 		duration: ms('1day'), | ||||
| 		max: 30, | ||||
| 	}, | ||||
|  | ||||
| 	errors: { | ||||
| 		noSuchUser: { | ||||
| 			message: 'No such user.', | ||||
| 			code: 'NO_SUCH_USER', | ||||
| 			id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5', | ||||
| 		}, | ||||
| 		notRemote: { | ||||
| 			message: 'User is not remote. You can only migrate from other instances.', | ||||
| 			code: 'NOT_REMOTE', | ||||
| 			id: '4362f8dc-731f-4ad8-a694-be2a88922a24', | ||||
| 		}, | ||||
| 		uriNull: { | ||||
| 			message: 'User ActivityPup URI is null.', | ||||
| 			code: 'URI_NULL', | ||||
| 			id: 'bf326f31-d430-4f97-9933-5d61e4d48a23', | ||||
| 		}, | ||||
| 	}, | ||||
| } as const; | ||||
|  | ||||
| export const paramDef = { | ||||
| 	type: 'object', | ||||
| 	properties: { | ||||
| 		alsoKnownAs: { type: 'string' }, | ||||
| 	}, | ||||
| 	required: ['alsoKnownAs'], | ||||
| } as const; | ||||
|  | ||||
| @Injectable() | ||||
| export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 	constructor( | ||||
| 		private userEntityService: UserEntityService, | ||||
| 		private remoteUserResolveService: RemoteUserResolveService, | ||||
| 		private apiLoggerService: ApiLoggerService, | ||||
| 		private accountMoveService: AccountMoveService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			// Check parameter | ||||
| 			if (!ps.alsoKnownAs) throw new ApiError(meta.errors.noSuchUser); | ||||
|  | ||||
| 			let unfiltered = ps.alsoKnownAs; | ||||
| 			const updates = {} as Partial<User>; | ||||
|  | ||||
| 			if (!unfiltered) { | ||||
| 				updates.alsoKnownAs = null; | ||||
| 			} else { | ||||
| 				// Parse user's input into the old account | ||||
| 				if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5); | ||||
| 				if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1); | ||||
| 				if (!unfiltered.includes('@')) throw new ApiError(meta.errors.notRemote); | ||||
|  | ||||
| 				const userAddress = unfiltered.split('@'); | ||||
| 				// Retrieve the old account | ||||
| 				const knownAs = await this.remoteUserResolveService.resolveUser(userAddress[0], userAddress[1]).catch((e) => { | ||||
| 					this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`); | ||||
| 					throw new ApiError(meta.errors.noSuchUser); | ||||
| 				}); | ||||
|  | ||||
| 				const toUrl: string | null = knownAs.uri; | ||||
| 				if (!toUrl) throw new ApiError(meta.errors.uriNull); | ||||
| 				// Only allow moving from a remote account | ||||
| 				if (this.userEntityService.isLocalUser(knownAs)) throw new ApiError(meta.errors.notRemote); | ||||
|  | ||||
| 				updates.alsoKnownAs = updates.alsoKnownAs?.concat([toUrl]) ?? [toUrl]; | ||||
| 			} | ||||
|  | ||||
| 			return await this.accountMoveService.createAlias(me, updates); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
| @@ -7,40 +7,35 @@ import { DI } from '@/di-symbols.js'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import { ApiError } from '@/server/api/error.js'; | ||||
|  | ||||
| import { LocalUser, RemoteUser } from '@/models/entities/User.js'; | ||||
|  | ||||
| import { AccountMoveService } from '@/core/AccountMoveService.js'; | ||||
| import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; | ||||
| import { UserEntityService } from '@/core/entities/UserEntityService.js'; | ||||
| import { ApiLoggerService } from '@/server/api/ApiLoggerService.js'; | ||||
| import { GetterService } from '@/server/api/GetterService.js'; | ||||
| import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; | ||||
| import { UserEntityService } from '@/core/entities/UserEntityService.js'; | ||||
|  | ||||
| import * as Acct from '@/misc/acct.js'; | ||||
|  | ||||
| export const meta = { | ||||
| 	tags: ['users'], | ||||
|  | ||||
| 	secure: true, | ||||
| 	requireCredential: true, | ||||
| 	prohibitMoved: true, | ||||
| 	limit: { | ||||
| 		duration: ms('1day'), | ||||
| 		max: 5, | ||||
| 	}, | ||||
|  | ||||
| 	errors: { | ||||
| 		noSuchMoveTarget: { | ||||
| 			message: 'No such move target.', | ||||
| 			code: 'NO_SUCH_MOVE_TARGET', | ||||
| 			id: 'b5c90186-4ab0-49c8-9bba-a1f76c202ba4', | ||||
| 		}, | ||||
| 		remoteAccountForbids: { | ||||
| 		destinationAccountForbids: { | ||||
| 			message: | ||||
| 				'Remote account doesn\'t have proper \'Known As\' alias. Did you remember to set it?', | ||||
| 			code: 'REMOTE_ACCOUNT_FORBIDS', | ||||
| 				'Destination account doesn\'t have proper \'Known As\' alias, or has already moved.', | ||||
| 			code: 'DESTINATION_ACCOUNT_FORBIDS', | ||||
| 			id: 'b5c90186-4ab0-49c8-9bba-a1f766282ba4', | ||||
| 		}, | ||||
| 		notRemote: { | ||||
| 			message: 'User is not remote. You can only migrate to other instances.', | ||||
| 			code: 'NOT_REMOTE', | ||||
| 			id: '4362f8dc-731f-4ad8-a694-be2a88922a24', | ||||
| 		}, | ||||
| 		rootForbidden: { | ||||
| 			message: 'The root can\'t migrate.', | ||||
| 			code: 'NOT_ROOT_FORBIDDEN', | ||||
| @@ -84,57 +79,52 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 		@Inject(DI.config) | ||||
| 		private config: Config, | ||||
|  | ||||
| 		private userEntityService: UserEntityService, | ||||
| 		private remoteUserResolveService: RemoteUserResolveService, | ||||
| 		private apiLoggerService: ApiLoggerService, | ||||
| 		private accountMoveService: AccountMoveService, | ||||
| 		private getterService: GetterService, | ||||
| 		private apPersonService: ApPersonService, | ||||
| 		private userEntityService: UserEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			// check parameter | ||||
| 			if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchMoveTarget); | ||||
| 			if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchUser); | ||||
| 			// abort if user is the root | ||||
| 			if (me.isRoot) throw new ApiError(meta.errors.rootForbidden); | ||||
| 			// abort if user has already moved | ||||
| 			if (me.movedToUri) throw new ApiError(meta.errors.alreadyMoved); | ||||
|  | ||||
| 			let unfiltered = ps.moveToAccount; | ||||
| 			if (!unfiltered) throw new ApiError(meta.errors.noSuchMoveTarget); | ||||
|  | ||||
| 			// parse user's input into the destination account | ||||
| 			if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5); | ||||
| 			if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1); | ||||
| 			if (!unfiltered.includes('@')) throw new ApiError(meta.errors.notRemote); | ||||
|  | ||||
| 			const userAddress = unfiltered.split('@'); | ||||
| 			const { username, host } = Acct.parse(ps.moveToAccount); | ||||
| 			// retrieve the destination account | ||||
| 			let moveTo = await this.remoteUserResolveService.resolveUser(userAddress[0], userAddress[1]).catch((e) => { | ||||
| 			let moveTo = await this.remoteUserResolveService.resolveUser(username, host).catch((e) => { | ||||
| 				this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`); | ||||
| 				throw new ApiError(meta.errors.noSuchMoveTarget); | ||||
| 				throw new ApiError(meta.errors.noSuchUser); | ||||
| 			}); | ||||
| 			const remoteMoveTo = await this.getterService.getRemoteUser(moveTo.id); | ||||
| 			if (!remoteMoveTo.uri) throw new ApiError(meta.errors.uriNull); | ||||
| 			const destination = await this.getterService.getUser(moveTo.id) as LocalUser | RemoteUser; | ||||
| 			const newUri = this.userEntityService.getUserUri(destination); | ||||
|  | ||||
| 			// update local db | ||||
| 			await this.apPersonService.updatePerson(remoteMoveTo.uri); | ||||
| 			await this.apPersonService.updatePerson(newUri); | ||||
| 			// retrieve updated user | ||||
| 			moveTo = await this.apPersonService.resolvePerson(remoteMoveTo.uri); | ||||
| 			// only allow moving to a remote account | ||||
| 			if (this.userEntityService.isLocalUser(moveTo)) throw new ApiError(meta.errors.notRemote); | ||||
| 			moveTo = await this.apPersonService.resolvePerson(newUri); | ||||
|  | ||||
| 			let allowed = false; | ||||
|  | ||||
| 			const fromUrl = `${this.config.url}/users/${me.id}`; | ||||
| 			// make sure that the user has indicated the old account as an alias | ||||
| 			moveTo.alsoKnownAs?.forEach((elem) => { | ||||
| 				if (fromUrl.includes(elem)) allowed = true; | ||||
| 			}); | ||||
| 			const fromUrl = this.userEntityService.genLocalUserUri(me.id); | ||||
| 			let allowed = false; | ||||
| 			if (moveTo.alsoKnownAs) { | ||||
| 				for (const knownAs of moveTo.alsoKnownAs) { | ||||
| 					if (knownAs.includes(fromUrl)) { | ||||
| 						allowed = true; | ||||
| 						break; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// abort if unintended | ||||
| 			if (!(allowed && moveTo.uri && fromUrl)) throw new ApiError(meta.errors.remoteAccountForbids); | ||||
| 			if (!allowed || moveTo.movedToUri) throw new ApiError(meta.errors.destinationAccountForbids); | ||||
|  | ||||
| 			return await this.accountMoveService.moveToRemote(me, moveTo); | ||||
| 			return await this.accountMoveService.moveFromLocal(me, moveTo); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ export const meta = { | ||||
| 	tags: ['account', 'notes'], | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:account', | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import * as mfm from 'mfm-js'; | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; | ||||
| import { extractHashtags } from '@/misc/extract-hashtags.js'; | ||||
| import * as Acct from '@/misc/acct.js'; | ||||
| import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/index.js'; | ||||
| import type { User } from '@/models/entities/User.js'; | ||||
| import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/entities/User.js'; | ||||
| @@ -19,7 +20,10 @@ import { HashtagService } from '@/core/HashtagService.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import { RoleService } from '@/core/RoleService.js'; | ||||
| import { CacheService } from '@/core/CacheService.js'; | ||||
| import { AccountMoveService } from '@/core/AccountMoveService.js'; | ||||
| import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; | ||||
| import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; | ||||
| import { ApiLoggerService } from '../../ApiLoggerService.js'; | ||||
| import { ApiError } from '../../error.js'; | ||||
|  | ||||
| export const meta = { | ||||
| @@ -71,6 +75,24 @@ export const meta = { | ||||
| 			code: 'TOO_MANY_MUTED_WORDS', | ||||
| 			id: '010665b1-a211-42d2-bc64-8f6609d79785', | ||||
| 		}, | ||||
|  | ||||
| 		noSuchUser: { | ||||
| 			message: 'No such user.', | ||||
| 			code: 'NO_SUCH_USER', | ||||
| 			id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5', | ||||
| 		}, | ||||
|  | ||||
| 		uriNull: { | ||||
| 			message: 'User ActivityPup URI is null.', | ||||
| 			code: 'URI_NULL', | ||||
| 			id: 'bf326f31-d430-4f97-9933-5d61e4d48a23', | ||||
| 		}, | ||||
|  | ||||
| 		forbiddenToSetYourself: { | ||||
| 			message: 'You can\'t set yourself as your own alias.', | ||||
| 			code: 'FORBIDDEN_TO_SET_YOURSELF', | ||||
| 			id: '25c90186-4ab0-49c8-9bba-a1fa6c202ba4', | ||||
| 		}, | ||||
| 	}, | ||||
|  | ||||
| 	res: { | ||||
| @@ -129,6 +151,12 @@ export const paramDef = { | ||||
| 		emailNotificationTypes: { type: 'array', items: { | ||||
| 			type: 'string', | ||||
| 		} }, | ||||
| 		alsoKnownAs: { | ||||
| 			type: 'array', | ||||
| 			maxItems: 10, | ||||
| 			uniqueItems: true, | ||||
| 			items: { type: 'string' }, | ||||
| 		}, | ||||
| 	}, | ||||
| } as const; | ||||
|  | ||||
| @@ -153,6 +181,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 		private globalEventService: GlobalEventService, | ||||
| 		private userFollowingService: UserFollowingService, | ||||
| 		private accountUpdateService: AccountUpdateService, | ||||
| 		private accountMoveService: AccountMoveService, | ||||
| 		private remoteUserResolveService: RemoteUserResolveService, | ||||
| 		private apiLoggerService: ApiLoggerService, | ||||
| 		private hashtagService: HashtagService, | ||||
| 		private roleService: RoleService, | ||||
| 		private cacheService: CacheService, | ||||
| @@ -260,6 +291,38 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 					}); | ||||
| 			} | ||||
|  | ||||
| 			if (ps.alsoKnownAs) { | ||||
| 				if (_user.movedToUri) { | ||||
| 					throw new ApiError({ | ||||
| 						message: 'You have moved your account.', | ||||
| 						code: 'YOUR_ACCOUNT_MOVED', | ||||
| 						id: '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31', | ||||
| 						httpStatusCode: 403, | ||||
| 					}); | ||||
| 				} | ||||
|  | ||||
| 				// Parse user's input into the old account | ||||
| 				const newAlsoKnownAs = new Set<string>(); | ||||
| 				for (const line of ps.alsoKnownAs) { | ||||
| 					if (!line) throw new ApiError(meta.errors.noSuchUser); | ||||
| 					const { username, host } = Acct.parse(line); | ||||
|  | ||||
| 					// Retrieve the old account | ||||
| 					const knownAs = await this.remoteUserResolveService.resolveUser(username, host).catch((e) => { | ||||
| 						this.apiLoggerService.logger.warn(`failed to resolve dstination user: ${e}`); | ||||
| 						throw new ApiError(meta.errors.noSuchUser); | ||||
| 					}); | ||||
| 					if (knownAs.id === _user.id) throw new ApiError(meta.errors.forbiddenToSetYourself); | ||||
|  | ||||
| 					const toUrl = this.userEntityService.getUserUri(knownAs); | ||||
| 					if (!toUrl) throw new ApiError(meta.errors.uriNull); | ||||
|  | ||||
| 					newAlsoKnownAs.add(toUrl); | ||||
| 				} | ||||
|  | ||||
| 				updates.alsoKnownAs = newAlsoKnownAs.size > 0 ? Array.from(newAlsoKnownAs) : null; | ||||
| 			} | ||||
|  | ||||
| 			//#region emojis/tags | ||||
|  | ||||
| 			let emojis = [] as string[]; | ||||
| @@ -287,6 +350,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 			//#endregion | ||||
|  | ||||
| 			if (Object.keys(updates).length > 0) await this.usersRepository.update(user.id, updates); | ||||
| 			if (Object.keys(updates).includes('alsoKnownAs')) { | ||||
| 				this.cacheService.uriPersonCache.set(this.userEntityService.genLocalUserUri(user.id), { ...user, ...updates }); | ||||
| 			} | ||||
| 			if (Object.keys(profileUpdates).length > 0) await this.userProfilesRepository.update(user.id, profileUpdates); | ||||
|  | ||||
| 			const iObj = await this.userEntityService.pack<true, true>(user.id, user, { | ||||
|   | ||||
| @@ -11,6 +11,7 @@ export const meta = { | ||||
| 	tags: ['account'], | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:mutes', | ||||
|  | ||||
|   | ||||
| @@ -18,6 +18,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	limit: { | ||||
| 		duration: ms('1hour'), | ||||
| 		max: 300, | ||||
|   | ||||
| @@ -12,6 +12,7 @@ export const meta = { | ||||
| 	tags: ['notes', 'favorites'], | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:favorites', | ||||
|  | ||||
|   | ||||
| @@ -17,6 +17,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:votes', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -9,6 +9,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:reactions', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -13,6 +13,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:pages', | ||||
|  | ||||
| 	limit: { | ||||
|   | ||||
| @@ -10,6 +10,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:page-likes', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -9,6 +9,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:page-likes', | ||||
|  | ||||
| 	errors: { | ||||
|   | ||||
| @@ -11,6 +11,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:pages', | ||||
|  | ||||
| 	limit: { | ||||
|   | ||||
| @@ -13,6 +13,7 @@ export const meta = { | ||||
| 	tags: ['account'], | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:mutes', | ||||
|  | ||||
|   | ||||
| @@ -13,6 +13,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:account', | ||||
|  | ||||
| 	description: 'Create a new list of users.', | ||||
| @@ -58,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 			if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { | ||||
| 				throw new ApiError(meta.errors.tooManyUserLists); | ||||
| 			} | ||||
| 	 | ||||
|  | ||||
| 			const userList = await this.userListsRepository.insert({ | ||||
| 				id: this.idService.genId(), | ||||
| 				createdAt: new Date(), | ||||
|   | ||||
| @@ -12,6 +12,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:account', | ||||
|  | ||||
| 	description: 'Remove a user from a list.', | ||||
|   | ||||
| @@ -12,6 +12,8 @@ export const meta = { | ||||
|  | ||||
| 	requireCredential: true, | ||||
|  | ||||
| 	prohibitMoved: true, | ||||
|  | ||||
| 	kind: 'write:account', | ||||
|  | ||||
| 	description: 'Add a user to an existing list.', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Namekuji
					Namekuji