From 55f9112eed264c38b933055d64c045e464479004 Mon Sep 17 00:00:00 2001
From: Namekuji <nmkj@mx.kazuno.co>
Date: Thu, 20 Apr 2023 22:03:18 -0400
Subject: [PATCH] set alsoKnownAs via /i/update

---
 .../backend/src/core/AccountMoveService.ts    | 32 +------
 .../backend/src/server/api/EndpointsModule.ts |  4 -
 packages/backend/src/server/api/endpoints.ts  |  2 -
 .../src/server/api/endpoints/i/known-as.ts    | 87 -------------------
 .../src/server/api/endpoints/i/update.ts      | 65 ++++++++++++++
 packages/backend/test/e2e/move.ts             | 70 ++++++++-------
 .../frontend/src/pages/settings/migration.vue | 56 +++++++-----
 packages/misskey-js/etc/misskey-js.api.md     |  8 +-
 packages/misskey-js/src/api.types.ts          |  2 +-
 9 files changed, 145 insertions(+), 181 deletions(-)
 delete mode 100644 packages/backend/src/server/api/endpoints/i/known-as.ts

diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts
index 8be7b04b67..6c04154a30 100644
--- a/packages/backend/src/core/AccountMoveService.ts
+++ b/packages/backend/src/core/AccountMoveService.ts
@@ -102,32 +102,6 @@ export class AccountMoveService {
 		return iObj;
 	}
 
-	/**
-	 * Create an alias of an old remote account.
-	 *
-	 * The user's new profile will be published to the followers.
-	 */
-	@bindThis
-	public async createAlias(me: LocalUser, updates: Partial<User>): Promise<unknown> {
-		await this.usersRepository.update(me.id, updates);
-		me = Object.assign(me, updates);
-
-		// Publish meUpdated event
-		const iObj = await this.userEntityService.pack<true, true>(me.id, me, {
-			detail: true,
-			includeSecrets: true,
-		});
-		this.globalEventService.publishMainStream(me.id, 'meUpdated', iObj);
-
-		if (me.isLocked === false) {
-			await this.userFollowingService.acceptAllFollowRequests(me);
-		}
-
-		this.accountUpdateService.publishToFollowers(me.id);
-
-		return iObj;
-	}
-
 	@bindThis
 	public async move(src: User, dst: User): Promise<void> {
 		// Copy blockings and mutings, and update lists
@@ -144,9 +118,9 @@ export class AccountMoveService {
 		// follow the new account and unfollow the old one
 		const proxy = await this.proxyAccountService.fetch();
 		const followings = await this.followingsRepository.findBy({
-				followeeId: src.id,
-				followerHost: IsNull(), // follower is local
-				followerId: proxy ? Not(proxy.id) : undefined,
+			followeeId: src.id,
+			followerHost: IsNull(), // follower is local
+			followerId: proxy ? Not(proxy.id) : undefined,
 		});
 		const followJobs = followings.map(following => ({
 			from: { id: following.followerId },
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index e4e594ec54..6dc1313e59 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -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,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index e7051abc26..acd7f7ec3e 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -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';
@@ -558,7 +557,6 @@ const eps = [
 	['i/update-email', ep___i_updateEmail],
 	['i/update', ep___i_update],
 	['i/move', ep___i_move],
-	['i/known-as', ep___i_knownAs],
 	['i/webhooks/create', ep___i_webhooks_create],
 	['i/webhooks/list', ep___i_webhooks_list],
 	['i/webhooks/show', ep___i_webhooks_show],
diff --git a/packages/backend/src/server/api/endpoints/i/known-as.ts b/packages/backend/src/server/api/endpoints/i/known-as.ts
deleted file mode 100644
index d63e4a9716..0000000000
--- a/packages/backend/src/server/api/endpoints/i/known-as.ts
+++ /dev/null
@@ -1,87 +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 { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
-
-export const meta = {
-	tags: ['users'],
-
-	secure: true,
-	requireCredential: true,
-	prohibitMoved: true,
-
-	limit: {
-		duration: ms('1day'),
-		max: 30,
-	},
-
-	errors: {
-		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',
-		},
-	},
-} 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 remoteUserResolveService: RemoteUserResolveService,
-		private apiLoggerService: ApiLoggerService,
-		private accountMoveService: AccountMoveService,
-	) {
-		super(meta, paramDef, async (ps, me) => {
-			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.noSuchUser);
-
-				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 dstination user: ${e}`);
-					throw new ApiError(meta.errors.noSuchUser);
-				});
-				if (knownAs.id === me.id) throw new ApiError(meta.errors.forbiddenToSetYourself);
-
-				const toUrl = this.accountMoveService.getUserUri(knownAs);
-				if (!toUrl) throw new ApiError(meta.errors.uriNull);
-
-				updates.alsoKnownAs = me.alsoKnownAs?.includes(toUrl) ? me.alsoKnownAs : me.alsoKnownAs?.concat([toUrl]) ?? [toUrl];
-			}
-
-			return await this.accountMoveService.createAlias(me, updates);
-		});
-	}
-}
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 97699f3bef..e59b759b58 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -19,7 +19,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 +74,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 +150,12 @@ export const paramDef = {
 		emailNotificationTypes: { type: 'array', items: {
 			type: 'string',
 		} },
+		alsoKnownAs: {
+			type: 'array',
+			maxItems: 5,
+			uniqueItems: true,
+			items: { type: 'string' },
+		},
 	},
 } as const;
 
@@ -153,6 +180,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 +290,41 @@ 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: string[] = [];
+				for (const line of ps.alsoKnownAs) {
+					let unfiltered = line;
+					if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5);
+					if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1);
+					if (!unfiltered.includes('@')) throw new ApiError(meta.errors.noSuchUser);
+
+					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 dstination user: ${e}`);
+						throw new ApiError(meta.errors.noSuchUser);
+					});
+					if (knownAs.id === _user.id) throw new ApiError(meta.errors.forbiddenToSetYourself);
+
+					const toUrl = this.accountMoveService.getUserUri(knownAs);
+					if (!toUrl) throw new ApiError(meta.errors.uriNull);
+
+					newAlsoKnownAs.push(toUrl);
+				}
+
+				updates.alsoKnownAs = newAlsoKnownAs.length > 0 ? newAlsoKnownAs : null;
+			}
+
 			//#region emojis/tags
 
 			let emojis = [] as string[];
diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts
index f063819475..aa5c932f10 100644
--- a/packages/backend/test/e2e/move.ts
+++ b/packages/backend/test/e2e/move.ts
@@ -48,8 +48,8 @@ describe('Account Move', () => {
 		}, 1000 * 10);
 
 		test('Able to create an alias', async () => {
-			await api('/i/known-as', {
-				alsoKnownAs: `@alice@${url.hostname}`,
+			await api('/i/update', {
+				alsoKnownAs: [`@alice@${url.hostname}`],
 			}, bob);
 
 			const newBob = await Users.findOneByOrFail({ id: bob.id });
@@ -58,8 +58,8 @@ describe('Account Move', () => {
 		});
 
 		test('Able to set remote user (but may fail)', async () => {
-			const res = await api('/i/known-as', {
-				alsoKnownAs: '@syuilo@example.com',
+			const res = await api('/i/update', {
+				alsoKnownAs: ['@syuilo@example.com'],
 			}, bob);
 
 			assert.strictEqual(res.status, 400);
@@ -67,22 +67,19 @@ describe('Account Move', () => {
 			assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
 		});
 
-		test('Nothing happen when alias duplicated', async () => {
-			await api('/i/known-as', {
-				alsoKnownAs: `@alice@${url.hostname}`,
-			}, bob);
-			await api('/i/known-as', {
-				alsoKnownAs: `@alice@${url.hostname}`,
+		test('Unable to add duplicated aliases to alsoKnownAs', async () => {
+			const res = await api('/i/update', {
+				alsoKnownAs: [`@alice@${url.hostname}`, `@alice@${url.hostname}`],
 			}, bob);
 
-			const newBob = await Users.findOneByOrFail({ id: bob.id });
-			assert.strictEqual(newBob.alsoKnownAs?.length, 1);
-			assert.strictEqual(newBob.alsoKnownAs[0], `${url.origin}/users/${alice.id}`);
+			assert.strictEqual(res.status, 400);
+			assert.strictEqual(res.body.error.code, 'INVALID_PARAM');
+			assert.strictEqual(res.body.error.id, '3d81ceae-475f-4600-b2a8-2bc116157532');
 		});
 
 		test('Unable to add itself', async () => {
-			const res = await api('/i/known-as', {
-				alsoKnownAs: `@bob@${url.hostname}`,
+			const res = await api('/i/update', {
+				alsoKnownAs: [`@bob@${url.hostname}`],
 			}, bob);
 
 			assert.strictEqual(res.status, 400);
@@ -91,8 +88,8 @@ describe('Account Move', () => {
 		});
 
 		test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
-			const res = await api('/i/known-as', {
-				alsoKnownAs: `@nonexist@${url.hostname}`,
+			const res = await api('/i/update', {
+				alsoKnownAs: [`@nonexist@${url.hostname}`],
 			}, bob);
 
 			assert.strictEqual(res.status, 400);
@@ -101,11 +98,8 @@ describe('Account Move', () => {
 		});
 
 		test('Able to add two existing local account to alsoKnownAs', async () => {
-			await api('/i/known-as', {
-				alsoKnownAs: `@alice@${url.hostname}`,
-			}, bob);
-			await api('/i/known-as', {
-				alsoKnownAs: `@carol@${url.hostname}`,
+			await api('/i/update', {
+				alsoKnownAs: [`@alice@${url.hostname}`, `@carol@${url.hostname}`],
 			}, bob);
 
 			const newBob = await Users.findOneByOrFail({ id: bob.id });
@@ -114,17 +108,31 @@ describe('Account Move', () => {
 			assert.strictEqual(newBob.alsoKnownAs[1], `${url.origin}/users/${carol.id}`);
 		});
 
+		test('Able to properly overwrite alsoKnownAs', async () => {
+			await api('/i/update', {
+				alsoKnownAs: [`@alice@${url.hostname}`],
+			}, bob);
+			await api('/i/update', {
+				alsoKnownAs: [`@carol@${url.hostname}`, `@dave@${url.hostname}`],
+			}, bob);
+
+			const newBob = await Users.findOneByOrFail({ id: bob.id });
+			assert.strictEqual(newBob.alsoKnownAs?.length, 2);
+			assert.strictEqual(newBob.alsoKnownAs[0], `${url.origin}/users/${carol.id}`);
+			assert.strictEqual(newBob.alsoKnownAs[1], `${url.origin}/users/${dave.id}`);
+		});
+
 		test('Unable to create an alias without the second @', async () => {
-			const res1 = await api('/i/known-as', {
-				alsoKnownAs: '@alice',
+			const res1 = await api('/i/update', {
+				alsoKnownAs: ['@alice'],
 			}, bob);
 
 			assert.strictEqual(res1.status, 400);
 			assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER');
 			assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
 
-			const res2 = await api('/i/known-as', {
-				alsoKnownAs: 'alice',
+			const res2 = await api('/i/update', {
+				alsoKnownAs: ['alice'],
 			}, bob);
 
 			assert.strictEqual(res2.status, 400);
@@ -137,8 +145,8 @@ describe('Account Move', () => {
 		let antennaId = '';
 
 		beforeAll(async () => {
-			await api('/i/known-as', {
-				alsoKnownAs: `@alice@${url.hostname}`,
+			await api('/i/update', {
+				alsoKnownAs: [`@alice@${url.hostname}`],
 			}, root);
 			const list = await api('/users/lists/create', {
 				name: rndstr('0-9a-z', 8),
@@ -167,8 +175,8 @@ describe('Account Move', () => {
 			}, alice);
 			antennaId = antenna.body.id;
 
-			await api('/i/known-as', {
-				alsoKnownAs: `@alice@${url.hostname}`,
+			await api('/i/update', {
+				alsoKnownAs: [`@alice@${url.hostname}`],
 			}, bob);
 
 			await api('/following/create', {
@@ -342,7 +350,7 @@ describe('Account Move', () => {
 			'/gallery/posts/like',
 			'/gallery/posts/unlike',
 			'/gallery/posts/update',
-			'/i/known-as',
+			'/i/update',
 			'/i/move',
 			'/notes/create',
 			'/notes/polls/vote',
diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue
index 612391b00b..2260ee0f63 100644
--- a/packages/frontend/src/pages/settings/migration.vue
+++ b/packages/frontend/src/pages/settings/migration.vue
@@ -2,35 +2,51 @@
 <div class="_gaps_m">
 	<FormSection first>
 		<template #label>{{ i18n.ts._accountMigration.moveTo }}</template>
-		<MkInput v-model="moveToAccount" manual-save>
-			<template #prefix><i class="ti ti-plane-departure"></i></template>
-			<template #label>{{ i18n.ts._accountMigration.moveToLabel }}</template>
-		</MkInput>
+		<div class="_gaps_m">
+			<div>
+				<MkInput v-model="moveToAccount">
+					<template #prefix><i class="ti ti-plane-departure"></i></template>
+					<template #label>{{ i18n.ts._accountMigration.moveToLabel }}</template>
+				</MkInput>
+			</div>
+			<div>
+				<MkButton inline primary :disabled="!moveToAccount" @click="move"><i class="ti ti-check"></i> {{ i18n.ts.ok }}</MkButton>
+			</div>
+		</div>
 	</FormSection>
 	<FormInfo warn>{{ i18n.ts._accountMigration.moveAccountDescription }}</FormInfo>
 
 	<FormSection>
 		<template #label>{{ i18n.ts._accountMigration.moveFrom }}</template>
-		<MkInput v-model="accountAlias" manual-save>
-			<template #prefix><i class="ti ti-plane-arrival"></i></template>
-			<template #label>{{ i18n.ts._accountMigration.moveFromLabel }}</template>
-		</MkInput>
+		<div class="_gaps_m">
+			<div v-for="(_, i) in accountAliases">
+				<MkInput v-model="accountAliases[i]">
+					<template #prefix><i class="ti ti-plane-arrival"></i></template>
+					<template #label>{{ i18n.ts._accountMigration.moveToLabel }}</template>
+				</MkInput>
+			</div>
+			<div>
+				<MkButton :disabled="accountAliases.length >= 5" inline style="margin-right: 8px;" @click="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+				<MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+			</div>
+		</div>
 	</FormSection>
 	<FormInfo warn>{{ i18n.ts._accountMigration.moveFromDescription }}</FormInfo>
 </div>
 </template>
 
 <script lang="ts" setup>
-import { ref, watch } from 'vue';
+import { ref } from 'vue';
 import FormSection from '@/components/form/section.vue';
 import FormInfo from '@/components/MkInfo.vue';
 import MkInput from '@/components/MkInput.vue';
+import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
 
 const moveToAccount = ref('');
-const accountAlias = ref('');
+const accountAliases = ref(['']);
 
 async function move(): Promise<void> {
 	const account = moveToAccount.value;
@@ -44,20 +60,16 @@ async function move(): Promise<void> {
 	});
 }
 
-async function save(): Promise<void> {
-	const account = accountAlias.value;
-	os.apiWithDialog('i/known-as', {
-		alsoKnownAs: account,
-	});
+function add() {
+	accountAliases.value.push('');
 }
 
-watch(accountAlias, async () => {
-	await save();
-});
-
-watch(moveToAccount, async () => {
-	await move();
-});
+async function save(): Promise<void> {
+	const alsoKnownAs = accountAliases.value.map(alias => alias.trim()).filter(alias => alias !== '');
+	os.apiWithDialog('i/update', {
+		alsoKnownAs,
+	});
+}
 
 definePageMetadata({
 	title: i18n.ts.accountMigration,
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 67d12000b8..54424cd6ec 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -1357,10 +1357,6 @@ export type Endpoints = {
         req: TODO;
         res: TODO;
     };
-    'i/known-as': {
-        req: TODO;
-        res: TODO;
-    };
     'i/notifications': {
         req: {
             limit?: number;
@@ -1511,6 +1507,7 @@ export type Endpoints = {
             mutedWords?: string[][];
             mutingNotificationTypes?: Notification_2['type'][];
             emailNotificationTypes?: string[];
+            alsoKnownAs?: string[];
         };
         res: MeDetailed;
     };
@@ -2348,6 +2345,7 @@ type LiteInstanceMetadata = {
         imageUrl: string;
     }[];
     translatorAvailable: boolean;
+    serverRules: string[];
 };
 
 // @public (undocumented)
@@ -2663,6 +2661,7 @@ type UserDetailed = UserLite & {
     lang: string | null;
     lastFetchedAt?: DateString;
     location: string | null;
+    movedToUri: string;
     notesCount: number;
     pinnedNoteIds: ID[];
     pinnedNotes: Note[];
@@ -2697,7 +2696,6 @@ type UserLite = {
     avatarUrl: string;
     avatarBlurhash: string;
     alsoKnownAs: string[];
-    movedToUri: any;
     emojis: {
         name: string;
         url: string;
diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts
index aed9f5bf84..cc88c4b1a4 100644
--- a/packages/misskey-js/src/api.types.ts
+++ b/packages/misskey-js/src/api.types.ts
@@ -363,7 +363,6 @@ export type Endpoints = {
 	'i/import-following': { req: TODO; res: TODO; };
 	'i/import-user-lists': { req: TODO; res: TODO; };
 	'i/move': { req: TODO; res: TODO; };
-	'i/known-as': { req: TODO; res: TODO; };
 	'i/notifications': { req: {
 		limit?: number;
 		sinceId?: Notification['id'];
@@ -421,6 +420,7 @@ export type Endpoints = {
 		mutedWords?: string[][];
 		mutingNotificationTypes?: Notification['type'][];
 		emailNotificationTypes?: string[];
+		alsoKnownAs?: string[];
 	}; res: MeDetailed; };
 	'i/user-group-invites': { req: TODO; res: TODO; };
 	'i/2fa/done': { req: TODO; res: TODO; };