From 55f9112eed264c38b933055d64c045e464479004 Mon Sep 17 00:00:00 2001 From: Namekuji 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): Promise { - await this.usersRepository.update(me.id, updates); - me = Object.assign(me, updates); - - // Publish meUpdated event - const iObj = await this.userEntityService.pack(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 { // 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 { - constructor( - private remoteUserResolveService: RemoteUserResolveService, - private apiLoggerService: ApiLoggerService, - private accountMoveService: AccountMoveService, - ) { - super(meta, paramDef, async (ps, me) => { - let unfiltered = ps.alsoKnownAs; - const updates = {} as Partial; - - 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 { 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 { }); } + 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 @@
- - - - +
+
+ + + + +
+
+ {{ i18n.ts.ok }} +
+
{{ i18n.ts._accountMigration.moveAccountDescription }} - - - - +
+
+ + + + +
+
+ {{ i18n.ts.add }} + {{ i18n.ts.save }} +
+
{{ i18n.ts._accountMigration.moveFromDescription }}