From 8c031e9f42bb2a6f7b6f3f57711e27573979998e Mon Sep 17 00:00:00 2001 From: Namekuji Date: Tue, 11 Apr 2023 20:47:54 -0400 Subject: [PATCH 01/90] copy block and mute then create follow and unfollow jobs --- .../backend/src/core/AccountMoveService.ts | 121 ++++++++++++++---- .../src/core/activitypub/ApInboxService.ts | 63 ++++----- .../src/server/api/endpoints/i/known-as.ts | 13 +- .../src/server/api/endpoints/i/move.ts | 30 ++--- 4 files changed, 136 insertions(+), 91 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 3f2a19b771c5..78d12ae7052a 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -3,27 +3,43 @@ import { IsNull } from 'typeorm'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; import type { LocalUser } from '@/models/entities/User.js'; +import type { BlockingsRepository, FollowingsRepository, Muting, MutingsRepository, UsersRepository } from '@/models/index.js'; +import type { RelationshipJobData } from '@/queue/types.js'; + import { User } from '@/models/entities/User.js'; -import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; +import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { QueueService } from '@/core/QueueService.js'; +import { RelayService } from '@/core/RelayService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { AccountUpdateService } from '@/core/AccountUpdateService.js'; -import { RelayService } from '@/core/RelayService.js'; +import { IdService } from '@/core/IdService.js'; +import { CacheService } from '@/core/CacheService'; @Injectable() export class AccountMoveService { constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + private idService: IdService, private userEntityService: UserEntityService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, @@ -31,18 +47,18 @@ export class AccountMoveService { private userFollowingService: UserFollowingService, private accountUpdateService: AccountUpdateService, private relayService: RelayService, + private cacheService: CacheService, + private queueService: QueueService, ) { } /** - * Move a local account to a remote account. + * Move a local account to a new account. * * After delivering Move activity, its local followers unfollow the old account and then follow the new one. */ @bindThis - public async moveToRemote(src: LocalUser, dst: User): Promise { - // Make sure that the destination is a remote account. - if (this.userEntityService.isLocalUser(dst)) throw new Error('move destiantion is not remote'); + public async moveFromLocal(src: LocalUser, dst: User): Promise { if (!dst.uri) throw new Error('destination uri is empty'); // add movedToUri to indicate that the user has moved @@ -64,25 +80,8 @@ export class AccountMoveService { const iObj = await this.userEntityService.pack(src.id, src, { detail: true, includeSecrets: true }); this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj); - // follow the new account and unfollow the old one - const followings = await this.followingsRepository.find({ - relations: { - follower: true, - }, - where: { - followeeId: src.id, - followerHost: IsNull(), // follower is local - }, - }); - for (const following of followings) { - if (!following.follower) continue; - try { - await this.userFollowingService.follow(following.follower, dst); - await this.userFollowingService.unfollow(following.follower, src); - } catch { - /* empty */ - } - } + // Move! + await this.move(src, dst); return iObj; } @@ -111,4 +110,74 @@ export class AccountMoveService { return iObj; } + + @bindThis + public async move(src: User, dst: User): Promise { + // Copy blockings: + // Followers shouldn't overlap with blockers, but the destination account, different from the blockee (i.e., old account), may have followed the local user before moving. + // So block the destination account here. + const blockings = await this.blockingsRepository.find({ + relations: { + blocker: true + }, + where: { + blockeeId: src.id + } + }) + // reblock the destination account + const blockJobs: RelationshipJobData[] = []; + for (const blocking of blockings) { + if (!blocking.blocker) continue; + blockJobs.push({ from: blocking.blocker, to: dst }); + } + // no need to unblock the old account because it may be still functional + this.queueService.createBlockJob(blockJobs); + + // Copy mutings: + // Insert new mutings with the same values except mutee + const mutings = await this.mutingsRepository.findBy({ muteeId: src.id }); + const newMuting: Partial[] = []; + for (const muting of mutings) { + newMuting.push({ + id: this.idService.genId(), + createdAt: new Date(), + expiresAt: muting.expiresAt, + muterId: muting.muterId, + muteeId: dst.id, + }) + } + this.mutingsRepository.insert(mutings); // no need to wait + for (const mute of mutings) { + if (mute.muter) this.cacheService.userMutingsCache.refresh(mute.muter.id); + } + // no need to unmute the old account because it may be still functional + + // follow the new account and unfollow the old one + const followings = await this.followingsRepository.find({ + relations: { + follower: true, + }, + where: { + followeeId: src.id, + followerHost: IsNull(), // follower is local + }, + }); + const followJobs: RelationshipJobData[] = []; + const unfollowJobs: RelationshipJobData[] = []; + for (const following of followings) { + if (!following.follower) continue; + followJobs.push({ from: following.follower, to: dst }); + unfollowJobs.push({ from: following.follower, to: src }); + } + // Should be queued because this can cause a number of follow/unfollow per one move. + // No need to care job orders as there should be no overlaps of follow/unfollow target. + this.queueService.createFollowJob(followJobs); + this.queueService.createUnfollowJob(unfollowJobs); + } + + @bindThis + public getUserUri(user: User): string { + return this.userEntityService.isRemoteUser(user) + ? user.uri : `${this.config.url}/users/${user.id}`; + } } diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 3fca0bb1fd57..982487e5f5f5 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { In, IsNull } from 'typeorm'; +import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; @@ -13,13 +13,15 @@ import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; import { AppLockService } from '@/core/AppLockService.js'; import type Logger from '@/logger.js'; import { MetaService } from '@/core/MetaService.js'; +import { AccountMoveService } from '@/core/AccountMoveService.js'; import { IdService } from '@/core/IdService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { CacheService } from '@/core/CacheService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { QueueService } from '@/core/QueueService.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import type { RemoteUser } from '@/models/entities/User.js'; import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; @@ -76,6 +78,8 @@ export class ApInboxService { private apNoteService: ApNoteService, private apPersonService: ApPersonService, private apQuestionService: ApQuestionService, + private accountMoveService: AccountMoveService, + private cacheService: CacheService, private queueService: QueueService, ) { this.logger = this.apLoggerService.logger; @@ -140,7 +144,7 @@ export class ApInboxService { } else if (isFlag(activity)) { await this.flag(actor, activity); } else if (isMove(activity)) { - //await this.move(actor, activity); + await this.move(actor, activity); } else { this.logger.warn(`unrecognized activity type: ${activity.type}`); } @@ -158,6 +162,7 @@ export class ApInboxService { return 'skip: フォローしようとしているユーザーはローカルユーザーではありません'; } + // don't queue because the sender may attempt again when timeout await this.userFollowingService.follow(actor, followee, activity.id); return 'ok'; } @@ -596,6 +601,7 @@ export class ApInboxService { throw e; }); + // don't queue because the sender may attempt again when timeout if (isFollow(object)) return await this.undoFollow(actor, object); if (isBlock(object)) return await this.undoBlock(actor, object); if (isLike(object)) return await this.undoLike(actor, object); @@ -736,52 +742,37 @@ export class ApInboxService { // fetch the new and old accounts const targetUri = getApHrefNullable(activity.target); if (!targetUri) return 'skip: invalid activity target'; - let new_acc = await this.apPersonService.resolvePerson(targetUri); - let old_acc = await this.apPersonService.resolvePerson(actor.uri); + let newAccount = await this.apPersonService.resolvePerson(targetUri); + let oldAccount = await this.apPersonService.resolvePerson(actor.uri); // update them if they're remote - if (new_acc.uri) await this.apPersonService.updatePerson(new_acc.uri); - if (old_acc.uri) await this.apPersonService.updatePerson(old_acc.uri); - - // retrieve updated users - new_acc = await this.apPersonService.resolvePerson(targetUri); - old_acc = await this.apPersonService.resolvePerson(actor.uri); + if (newAccount.uri) { + await this.apPersonService.updatePerson(newAccount.uri); + newAccount = await this.apPersonService.resolvePerson(newAccount.uri); + } + if (oldAccount.uri) { + await this.apPersonService.updatePerson(oldAccount.uri); + oldAccount = await this.apPersonService.resolvePerson(oldAccount.uri); + } // check if alsoKnownAs of the new account is valid let isValidMove = true; - if (old_acc.uri) { - if (!new_acc.alsoKnownAs?.includes(old_acc.uri)) { + if (oldAccount.uri) { + if (!newAccount.alsoKnownAs?.includes(oldAccount.uri)) { isValidMove = false; } - } else if (!new_acc.alsoKnownAs?.includes(old_acc.id)) { + } else if (!newAccount.alsoKnownAs?.includes(this.accountMoveService.getUserUri(oldAccount))) { isValidMove = false; } if (!isValidMove) { - return 'skip: accounts invalid'; + return 'skip: destination account invalid'; } // add target uri to movedToUri in order to indicate that the user has moved - await this.usersRepository.update(old_acc.id, { movedToUri: targetUri }); - - // follow the new account and unfollow the old one - const followings = await this.followingsRepository.find({ - relations: { - follower: true, - }, - where: { - followeeId: old_acc.id, - followerHost: IsNull(), // follower is local - }, - }); - for (const following of followings) { - if (!following.follower) continue; - try { - await this.userFollowingService.follow(following.follower, new_acc); - await this.userFollowingService.unfollow(following.follower, old_acc); - } catch { - /* empty */ - } - } + this.usersRepository.update(oldAccount.id, { movedToUri: targetUri }); + + // Move! + await this.accountMoveService.move(oldAccount, newAccount); return 'ok'; } diff --git a/packages/backend/src/server/api/endpoints/i/known-as.ts b/packages/backend/src/server/api/endpoints/i/known-as.ts index 964704d82b05..7aa401e9bb78 100644 --- a/packages/backend/src/server/api/endpoints/i/known-as.ts +++ b/packages/backend/src/server/api/endpoints/i/known-as.ts @@ -27,11 +27,6 @@ export const meta = { 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', @@ -69,19 +64,17 @@ export default class extends Endpoint { // 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); + 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 remote user: ${e}`); + this.apiLoggerService.logger.warn(`failed to resolve dstination user: ${e}`); throw new ApiError(meta.errors.noSuchUser); }); - const toUrl: string | null = knownAs.uri; + const toUrl = this.accountMoveService.getUserUri(knownAs); 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]; } diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index ac76e1f62093..53195d9c6302 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -30,17 +30,12 @@ export const meta = { 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?', + 'Destination account doesn\'t have proper \'Known As\' alias. Did you remember to set it?', code: 'REMOTE_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', @@ -105,7 +100,7 @@ export default class extends Endpoint { // 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); + if (!unfiltered.includes('@')) throw new ApiError(meta.errors.noSuchMoveTarget); const userAddress = unfiltered.split('@'); // retrieve the destination account @@ -113,28 +108,25 @@ export default class extends Endpoint { this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`); throw new ApiError(meta.errors.noSuchMoveTarget); }); - 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); + moveTo.uri = this.accountMoveService.getUserUri(destination) // update local db - await this.apPersonService.updatePerson(remoteMoveTo.uri); + await this.apPersonService.updatePerson(moveTo.uri); // 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); - - let allowed = false; + moveTo = await this.apPersonService.resolvePerson(moveTo.uri); - const fromUrl = `${this.config.url}/users/${me.id}`; // make sure that the user has indicated the old account as an alias + const fromUrl = `${this.config.url}/users/${me.id}`; + let allowed = false; moveTo.alsoKnownAs?.forEach((elem) => { if (fromUrl.includes(elem)) allowed = true; }); // abort if unintended - if (!(allowed && moveTo.uri && fromUrl)) throw new ApiError(meta.errors.remoteAccountForbids); + if (!(allowed && moveTo.uri && fromUrl)) throw new ApiError(meta.errors.destinationAccountForbids); - return await this.accountMoveService.moveToRemote(me, moveTo); + return await this.accountMoveService.moveFromLocal(me, moveTo); }); } } From 91fcad0c85d7623c94032aad369762129b4fae1d Mon Sep 17 00:00:00 2001 From: Namekuji Date: Thu, 13 Apr 2023 10:43:29 -0400 Subject: [PATCH 02/90] copy block and mute and update lists when detecting an account has moved --- .../backend/src/core/AccountMoveService.ts | 92 +++++++++++++------ .../src/core/activitypub/ApInboxService.ts | 4 +- .../activitypub/models/ApPersonService.ts | 25 ++++- .../src/server/api/endpoints/i/move.ts | 2 +- 4 files changed, 89 insertions(+), 34 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 78d12ae7052a..5f63d3008916 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -5,8 +5,8 @@ import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { LocalUser } from '@/models/entities/User.js'; -import type { BlockingsRepository, FollowingsRepository, Muting, MutingsRepository, UsersRepository } from '@/models/index.js'; -import type { RelationshipJobData } from '@/queue/types.js'; +import type { BlockingsRepository, FollowingsRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; +import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; import { User } from '@/models/entities/User.js'; @@ -20,6 +20,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService'; +import { ProxyAccountService } from '@/core/ProxyAccountService.js'; @Injectable() export class AccountMoveService { @@ -39,6 +40,9 @@ export class AccountMoveService { @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + private idService: IdService, private userEntityService: UserEntityService, private apRendererService: ApRendererService, @@ -46,6 +50,7 @@ export class AccountMoveService { private globalEventService: GlobalEventService, private userFollowingService: UserFollowingService, private accountUpdateService: AccountUpdateService, + private proxyAccountService: ProxyAccountService, private relayService: RelayService, private cacheService: CacheService, private queueService: QueueService, @@ -114,26 +119,61 @@ export class AccountMoveService { @bindThis public async move(src: User, dst: User): Promise { // Copy blockings: + await this.copyBlocking(src, dst); + + // Copy mutings: + await this.copyMutings(src, dst); + + // Update lists: + await this.updateLists(src, dst); + + // follow the new account and unfollow the old one + const followings = await this.followingsRepository.find({ + relations: { + follower: true, + }, + where: { + followeeId: src.id, + followerHost: IsNull(), // follower is local + }, + }); + const followJobs: RelationshipJobData[] = []; + const unfollowJobs: RelationshipJobData[] = []; + for (const following of followings) { + if (!following.follower) continue; + followJobs.push({ from: { id: following.follower.id }, to: { id: dst.id } }); + unfollowJobs.push({ from: { id: following.follower.id }, to: { id: src.id } }); + } + // Should be queued because this can cause a number of follow/unfollow per one move. + // No need to care job orders as there should be no overlaps of follow/unfollow target. + this.queueService.createFollowJob(followJobs); + this.queueService.createUnfollowJob(unfollowJobs); + } + + @bindThis + public async copyBlocking(src: ThinUser, dst: ThinUser): Promise { // Followers shouldn't overlap with blockers, but the destination account, different from the blockee (i.e., old account), may have followed the local user before moving. // So block the destination account here. - const blockings = await this.blockingsRepository.find({ + const blockings = await this.blockingsRepository.find({ // FIXME: might be expensive relations: { blocker: true }, where: { blockeeId: src.id } - }) + }); // reblock the destination account const blockJobs: RelationshipJobData[] = []; for (const blocking of blockings) { if (!blocking.blocker) continue; - blockJobs.push({ from: blocking.blocker, to: dst }); + blockJobs.push({ from: { id: blocking.blocker.id }, to: { id: dst.id } }); } // no need to unblock the old account because it may be still functional this.queueService.createBlockJob(blockJobs); + } - // Copy mutings: + @bindThis + public async copyMutings(src: ThinUser, dst: ThinUser): Promise { // Insert new mutings with the same values except mutee const mutings = await this.mutingsRepository.findBy({ muteeId: src.id }); const newMuting: Partial[] = []; @@ -144,35 +184,33 @@ export class AccountMoveService { expiresAt: muting.expiresAt, muterId: muting.muterId, muteeId: dst.id, - }) + }); } this.mutingsRepository.insert(mutings); // no need to wait for (const mute of mutings) { if (mute.muter) this.cacheService.userMutingsCache.refresh(mute.muter.id); } // no need to unmute the old account because it may be still functional + } - // follow the new account and unfollow the old one - const followings = await this.followingsRepository.find({ - relations: { - follower: true, - }, - where: { - followeeId: src.id, - followerHost: IsNull(), // follower is local - }, - }); - const followJobs: RelationshipJobData[] = []; - const unfollowJobs: RelationshipJobData[] = []; - for (const following of followings) { - if (!following.follower) continue; - followJobs.push({ from: following.follower, to: dst }); - unfollowJobs.push({ from: following.follower, to: src }); + @bindThis + public async updateLists(src: ThinUser, dst: User): Promise { + // Return if there is no list to be updated + const numOfLists = await this.userListJoiningsRepository.countBy({ userId: src.id }); + if (numOfLists === 0) return; + + await this.userListJoiningsRepository.update( + { userId: src.id }, + { userId: dst.id, user: dst } + ); + + // Have the proxy account follow the new account in the same way as UserListService.push + if (this.userEntityService.isRemoteUser(dst)) { + const proxy = await this.proxyAccountService.fetch(); + if (proxy) { + this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: dst.id } }]); + } } - // Should be queued because this can cause a number of follow/unfollow per one move. - // No need to care job orders as there should be no overlaps of follow/unfollow target. - this.queueService.createFollowJob(followJobs); - this.queueService.createUnfollowJob(unfollowJobs); } @bindThis diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 982487e5f5f5..cf8172c9fccd 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -747,11 +747,11 @@ export class ApInboxService { // update them if they're remote if (newAccount.uri) { - await this.apPersonService.updatePerson(newAccount.uri); + await this.apPersonService.updatePerson(newAccount.uri); newAccount = await this.apPersonService.resolvePerson(newAccount.uri); } if (oldAccount.uri) { - await this.apPersonService.updatePerson(oldAccount.uri); + await this.apPersonService.updatePerson(oldAccount.uri); oldAccount = await this.apPersonService.resolvePerson(oldAccount.uri); } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 21797cfcb7ae..2a5d3803562e 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -3,7 +3,7 @@ import promiseLimit from 'promise-limit'; import { DataSource } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; +import type { BlockingsRepository, MutingsRepository, FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type { RemoteUser } from '@/models/entities/User.js'; import { User } from '@/models/entities/User.js'; @@ -42,6 +42,7 @@ import type { ApLoggerService } from '../ApLoggerService.js'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import type { ApImageService } from './ApImageService.js'; import type { IActor, IObject } from '../type.js'; +import type { AccountMoveService } from '@/core/AccountMoveService.js'; const nameLength = 128; const summaryLength = 2048; @@ -66,6 +67,7 @@ export class ApPersonService implements OnModuleInit { private usersChart: UsersChart; private instanceChart: InstanceChart; private apLoggerService: ApLoggerService; + private accountMoveService: AccountMoveService; private logger: Logger; constructor( @@ -131,6 +133,7 @@ export class ApPersonService implements OnModuleInit { this.usersChart = this.moduleRef.get('UsersChart'); this.instanceChart = this.moduleRef.get('InstanceChart'); this.apLoggerService = this.moduleRef.get('ApLoggerService'); + this.accountMoveService = this.moduleRef.get('AccountMoveService'); this.logger = this.apLoggerService.logger; } @@ -413,14 +416,14 @@ export class ApPersonService implements OnModuleInit { if (typeof uri !== 'string') throw new Error('uri is not string'); // URIがこのサーバーを指しているならスキップ - if (uri.startsWith(this.config.url + '/')) { + if (uri.startsWith(`${this.config.url}/`)) { return; } //#region このサーバーに既に登録されているか - const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser; + const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null; - if (exist == null) { + if (exist === null) { return; } //#endregion @@ -523,6 +526,20 @@ export class ApPersonService implements OnModuleInit { }); await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err)); + + // Copy blocking and muting if we know its moving for the first time. + if (!exist.movedToUri && updates.movedToUri) { + try { + const newAccount = await this.resolvePerson(updates.movedToUri); + // Aggressively block and/or mute the new account: + // This does NOT check alsoKnownAs, assuming that other implmenetations properly check alsoKnownAs when firing account migration + await this.accountMoveService.copyBlocking(exist, newAccount); + await this.accountMoveService.copyMutings(exist, newAccount); + await this.accountMoveService.updateLists(exist, newAccount); + } catch { + /* skip if any error happens */ + } + } } /** diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index 53195d9c6302..c1aa0f4ccb98 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -109,7 +109,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchMoveTarget); }); const destination = await this.getterService.getUser(moveTo.id); - moveTo.uri = this.accountMoveService.getUserUri(destination) + moveTo.uri = this.accountMoveService.getUserUri(destination); // update local db await this.apPersonService.updatePerson(moveTo.uri); From 0474efc008d739349112aae72a6d8d41b13e64b8 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Thu, 13 Apr 2023 10:49:17 -0400 Subject: [PATCH 03/90] no need to care promise orders --- .../backend/src/core/AccountMoveService.ts | 24 ++++++++++--------- .../activitypub/models/ApPersonService.ts | 8 ++++--- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 5f63d3008916..75b15e052227 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -118,14 +118,12 @@ export class AccountMoveService { @bindThis public async move(src: User, dst: User): Promise { - // Copy blockings: - await this.copyBlocking(src, dst); - - // Copy mutings: - await this.copyMutings(src, dst); - - // Update lists: - await this.updateLists(src, dst); + // Copy blockings and mutings, and update lists + await Promise.all([ + this.copyBlocking(src, dst), + this.copyMutings(src, dst), + this.updateLists(src, dst), + ]); // follow the new account and unfollow the old one const followings = await this.followingsRepository.find({ @@ -195,9 +193,13 @@ export class AccountMoveService { @bindThis public async updateLists(src: ThinUser, dst: User): Promise { - // Return if there is no list to be updated - const numOfLists = await this.userListJoiningsRepository.countBy({ userId: src.id }); - if (numOfLists === 0) return; + // Return if there is no list to be updated. + const exists = await this.userListJoiningsRepository.exist({ + where: { + userId: src.id, + }, + }); + if (!exists) return; await this.userListJoiningsRepository.update( { userId: src.id }, diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 2a5d3803562e..819e8b9bf560 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -533,9 +533,11 @@ export class ApPersonService implements OnModuleInit { const newAccount = await this.resolvePerson(updates.movedToUri); // Aggressively block and/or mute the new account: // This does NOT check alsoKnownAs, assuming that other implmenetations properly check alsoKnownAs when firing account migration - await this.accountMoveService.copyBlocking(exist, newAccount); - await this.accountMoveService.copyMutings(exist, newAccount); - await this.accountMoveService.updateLists(exist, newAccount); + await Promise.all([ + this.accountMoveService.copyBlocking(exist, newAccount), + this.accountMoveService.copyMutings(exist, newAccount), + this.accountMoveService.updateLists(exist, newAccount), + ]); } catch { /* skip if any error happens */ } From 75c2c105fb50dc6915d40a40859ef5282e1e7c56 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Thu, 13 Apr 2023 11:19:35 -0400 Subject: [PATCH 04/90] refactor updating actor and target --- .../src/core/activitypub/ApInboxService.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index cf8172c9fccd..95b41626aaed 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -742,18 +742,14 @@ export class ApInboxService { // fetch the new and old accounts const targetUri = getApHrefNullable(activity.target); if (!targetUri) return 'skip: invalid activity target'; - let newAccount = await this.apPersonService.resolvePerson(targetUri); - let oldAccount = await this.apPersonService.resolvePerson(actor.uri); - - // update them if they're remote - if (newAccount.uri) { - await this.apPersonService.updatePerson(newAccount.uri); - newAccount = await this.apPersonService.resolvePerson(newAccount.uri); - } - if (oldAccount.uri) { - await this.apPersonService.updatePerson(oldAccount.uri); - oldAccount = await this.apPersonService.resolvePerson(oldAccount.uri); - } + await Promise.all([ + this.apPersonService.updatePerson(targetUri), + this.apPersonService.updatePerson(actor.uri), + ]); + const [newAccount, oldAccount] = await Promise.all([ + this.apPersonService.resolvePerson(targetUri), + this.apPersonService.resolvePerson(actor.uri), + ]); // check if alsoKnownAs of the new account is valid let isValidMove = true; From 71a2fbd232bb36394d9c0804eccfd6a29134b7d7 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Thu, 13 Apr 2023 11:46:32 -0400 Subject: [PATCH 05/90] automatically accept if a locked account had accepted an old account --- .../backend/src/core/UserFollowingService.ts | 20 +++++++++++++++++++ .../activitypub/models/ApPersonService.ts | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index a8eded673351..f32048bf3862 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -22,6 +22,7 @@ import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; import Logger from '../logger.js'; +import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; const logger = new Logger('following/create'); @@ -73,6 +74,7 @@ export class UserFollowingService implements OnModuleInit { private federatedInstanceService: FederatedInstanceService, private webhookService: WebhookService, private apRendererService: ApRendererService, + private apPersonService: ApPersonService, private perUserFollowingChart: PerUserFollowingChart, private instanceChart: InstanceChart, ) { @@ -137,6 +139,24 @@ export class UserFollowingService implements OnModuleInit { if (followed) autoAccept = true; } + // Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account. + if (followee.isLocked && !autoAccept && follower.alsoKnownAs) { + for (const oldUri of follower.alsoKnownAs) { + try { + await this.apPersonService.updatePerson(oldUri); + const oldAccount = await this.apPersonService.resolvePerson(oldUri); + autoAccept = await this.followingsRepository.exist({ + where: { + followeeId: followee.id, + followerId: oldAccount.id, + }, + }); + } catch { + /* skip if any error happens */ + } + } + } + if (!autoAccept) { await this.createFollowRequest(follower, followee, requestId); return; diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 819e8b9bf560..21a53ea85f1a 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -217,7 +217,7 @@ export class ApPersonService implements OnModuleInit { if (cached) return cached; // URIがこのサーバーを指しているならデータベースからフェッチ - if (uri.startsWith(this.config.url + '/')) { + if (uri.startsWith(`${this.config.url}/`)) { const id = uri.split('/').pop(); const u = await this.usersRepository.findOneBy({ id }); if (u) this.cacheService.uriPersonCache.set(uri, u); From 91a59da055bfb2b538b1d010023681128fdcb231 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Thu, 13 Apr 2023 13:45:04 -0400 Subject: [PATCH 06/90] fix exception format --- packages/backend/src/core/activitypub/models/ApPersonService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 21a53ea85f1a..3e86d3b44dfe 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -463,7 +463,7 @@ export class ApPersonService implements OnModuleInit { const url = getOneApHrefNullable(person.url); if (url && !url.startsWith('https://')) { - throw new Error('unexpected shcema of person url: ' + url); + throw new Error(`unexpected shcema of person url: ${url}`); } const updates = { From 5e845f1ad5853c444a0a5c03d7ffa87d99a648c0 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Thu, 13 Apr 2023 13:46:01 -0400 Subject: [PATCH 07/90] prevent the old account from calling some endpoints --- packages/backend/src/server/api/ApiCallService.ts | 11 +++++++++++ packages/backend/src/server/api/endpoints.ts | 6 ++++++ .../src/server/api/endpoints/antennas/create.ts | 2 ++ .../src/server/api/endpoints/antennas/update.ts | 4 +++- .../backend/src/server/api/endpoints/app/create.ts | 2 ++ .../src/server/api/endpoints/channels/create.ts | 2 ++ .../src/server/api/endpoints/channels/favorite.ts | 2 ++ .../src/server/api/endpoints/channels/follow.ts | 2 ++ .../src/server/api/endpoints/channels/unfavorite.ts | 2 ++ .../src/server/api/endpoints/channels/unfollow.ts | 2 ++ .../src/server/api/endpoints/clips/add-note.ts | 2 ++ .../backend/src/server/api/endpoints/clips/create.ts | 4 +++- .../src/server/api/endpoints/clips/favorite.ts | 2 ++ .../src/server/api/endpoints/clips/remove-note.ts | 2 ++ .../src/server/api/endpoints/clips/unfavorite.ts | 2 ++ .../backend/src/server/api/endpoints/clips/update.ts | 2 ++ .../src/server/api/endpoints/drive/files/create.ts | 2 ++ .../api/endpoints/drive/files/upload-from-url.ts | 2 ++ .../backend/src/server/api/endpoints/flash/create.ts | 2 ++ .../backend/src/server/api/endpoints/flash/like.ts | 2 ++ .../backend/src/server/api/endpoints/flash/unlike.ts | 2 ++ .../backend/src/server/api/endpoints/flash/update.ts | 2 ++ .../src/server/api/endpoints/following/create.ts | 2 ++ .../src/server/api/endpoints/gallery/posts/create.ts | 2 ++ .../src/server/api/endpoints/gallery/posts/like.ts | 2 ++ .../src/server/api/endpoints/gallery/posts/unlike.ts | 2 ++ .../src/server/api/endpoints/gallery/posts/update.ts | 2 ++ .../backend/src/server/api/endpoints/notes/create.ts | 2 ++ .../src/server/api/endpoints/notes/polls/vote.ts | 2 ++ .../server/api/endpoints/notes/reactions/create.ts | 2 ++ .../backend/src/server/api/endpoints/pages/create.ts | 2 ++ .../backend/src/server/api/endpoints/pages/like.ts | 2 ++ .../backend/src/server/api/endpoints/pages/unlike.ts | 2 ++ .../backend/src/server/api/endpoints/pages/update.ts | 2 ++ .../src/server/api/endpoints/users/lists/create.ts | 4 +++- .../src/server/api/endpoints/users/lists/pull.ts | 2 ++ .../src/server/api/endpoints/users/lists/push.ts | 2 ++ .../src/server/api/endpoints/users/lists/update.ts | 2 ++ 38 files changed, 92 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index bf5cb20918f7..e3483c82c67d 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -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)) { diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index dc82c04e4e77..765ab6d93652 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -702,6 +702,12 @@ export interface IEndpointMeta { readonly requireRolePolicy?: keyof RolePolicies; + /** + * 引っ越し済みのユーザーによるリクエストを禁止するか + * 省略した場合は false として解釈されます。 + */ + readonly prohibitMoved?: boolean; + /** * エンドポイントのリミテーションに関するやつ * 省略した場合はリミテーションは無いものとして解釈されます。 diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index b7ce3363a93a..5754a9f12aa5 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -13,6 +13,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:account', errors: { diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 3f85442131b6..5f980bdbeb10 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -11,6 +11,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:account', errors: { @@ -71,7 +73,7 @@ export default class extends Endpoint { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - + private antennaEntityService: AntennaEntityService, private globalEventService: GlobalEventService, ) { diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts index c1d0a9dd7485..e5c8d08fb394 100644 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ b/packages/backend/src/server/api/endpoints/app/create.ts @@ -12,6 +12,8 @@ export const meta = { requireCredential: false, + prohibitMoved: true, + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts index dff8a9d10df4..6294b08fa064 100644 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ b/packages/backend/src/server/api/endpoints/channels/create.ts @@ -13,6 +13,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:channels', limit: { diff --git a/packages/backend/src/server/api/endpoints/channels/favorite.ts b/packages/backend/src/server/api/endpoints/channels/favorite.ts index f52b45ccf3f8..c8544273a178 100644 --- a/packages/backend/src/server/api/endpoints/channels/favorite.ts +++ b/packages/backend/src/server/api/endpoints/channels/favorite.ts @@ -10,6 +10,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:channels', errors: { diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts index 8ab59991c785..f3ca66cfd2d6 100644 --- a/packages/backend/src/server/api/endpoints/channels/follow.ts +++ b/packages/backend/src/server/api/endpoints/channels/follow.ts @@ -11,6 +11,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:channels', errors: { diff --git a/packages/backend/src/server/api/endpoints/channels/unfavorite.ts b/packages/backend/src/server/api/endpoints/channels/unfavorite.ts index 0c3f6c485531..67fb1ea03e11 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfavorite.ts @@ -9,6 +9,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:channels', errors: { diff --git a/packages/backend/src/server/api/endpoints/channels/unfollow.ts b/packages/backend/src/server/api/endpoints/channels/unfollow.ts index 855ba47f8c35..f46ff9f2865c 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfollow.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfollow.ts @@ -10,6 +10,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:channels', errors: { diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index b9d8dce47a4f..c3561e2a710d 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -13,6 +13,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:account', limit: { diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts index a770dc986d2d..5395a5c373a9 100644 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ b/packages/backend/src/server/api/endpoints/clips/create.ts @@ -12,6 +12,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:account', res: { @@ -57,7 +59,7 @@ export default class extends Endpoint { 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(), diff --git a/packages/backend/src/server/api/endpoints/clips/favorite.ts b/packages/backend/src/server/api/endpoints/clips/favorite.ts index 6addf743a272..f08caaf8d7e1 100644 --- a/packages/backend/src/server/api/endpoints/clips/favorite.ts +++ b/packages/backend/src/server/api/endpoints/clips/favorite.ts @@ -10,6 +10,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:clip-favorite', errors: { diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts index 5d88870ed27f..50c5d758bda3 100644 --- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts @@ -10,6 +10,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:account', errors: { diff --git a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts index 244843d50f2f..3da252a2269d 100644 --- a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts @@ -9,6 +9,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:clip-favorite', errors: { diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts index a103c3f7d316..70f195935392 100644 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ b/packages/backend/src/server/api/endpoints/clips/update.ts @@ -10,6 +10,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:account', errors: { diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index b3bdef41d3e3..a1c1f9325ec7 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -15,6 +15,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + limit: { duration: ms('1hour'), max: 120, diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index cfef7938318f..c835587c4aa0 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -19,6 +19,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:drive', } as const; diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts index f21d9d5c3363..3172bdbfdaf5 100644 --- a/packages/backend/src/server/api/endpoints/flash/create.ts +++ b/packages/backend/src/server/api/endpoints/flash/create.ts @@ -11,6 +11,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:flash', limit: { diff --git a/packages/backend/src/server/api/endpoints/flash/like.ts b/packages/backend/src/server/api/endpoints/flash/like.ts index 5581b8ec6069..23de2f397098 100644 --- a/packages/backend/src/server/api/endpoints/flash/like.ts +++ b/packages/backend/src/server/api/endpoints/flash/like.ts @@ -10,6 +10,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:flash-likes', errors: { diff --git a/packages/backend/src/server/api/endpoints/flash/unlike.ts b/packages/backend/src/server/api/endpoints/flash/unlike.ts index b994f5d3479c..696512b06c9e 100644 --- a/packages/backend/src/server/api/endpoints/flash/unlike.ts +++ b/packages/backend/src/server/api/endpoints/flash/unlike.ts @@ -9,6 +9,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:flash-likes', errors: { diff --git a/packages/backend/src/server/api/endpoints/flash/update.ts b/packages/backend/src/server/api/endpoints/flash/update.ts index cd4e413a403b..78dfd4a06a71 100644 --- a/packages/backend/src/server/api/endpoints/flash/update.ts +++ b/packages/backend/src/server/api/endpoints/flash/update.ts @@ -10,6 +10,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:flash', limit: { diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index 411c39110a5b..4ad16de911ec 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -19,6 +19,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:following', errors: { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts index cb8b6a2e3eb1..ca6bfa7e0f1b 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -13,6 +13,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:gallery', limit: { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts index 519e56ed6a0f..6ac5fa860650 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -10,6 +10,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:gallery-likes', errors: { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts index cfbedcc4d93b..513089217dfd 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts @@ -9,6 +9,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:gallery-likes', errors: { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts index f14d644a3aff..a2a10d840038 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -11,6 +11,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:gallery', limit: { diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 69fafcb9c773..fa2dc447d859 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -18,6 +18,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + limit: { duration: ms('1hour'), max: 300, diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index 2a44dc537e8f..3a33b037f868 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -17,6 +17,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:votes', errors: { diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts index 04e374d1aed3..97cb026779c5 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts @@ -9,6 +9,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:reactions', errors: { diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index 4015bf1f29df..e08ab399f88f 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -13,6 +13,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:pages', limit: { diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index d27990f7e13c..543c126d9cf9 100644 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ b/packages/backend/src/server/api/endpoints/pages/like.ts @@ -10,6 +10,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:page-likes', errors: { diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts index e397e2a23b81..f0c019846014 100644 --- a/packages/backend/src/server/api/endpoints/pages/unlike.ts +++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts @@ -9,6 +9,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:page-likes', errors: { diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index 35b402ec568c..751274067ed2 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -11,6 +11,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:pages', limit: { diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts index a840c1a04e27..751088952634 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -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 { 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(), diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts index d2dd5731eebe..d50b70efc2fb 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -12,6 +12,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:account', description: 'Remove a user from a list.', diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 1c1fdc23f16f..925037e48420 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -12,6 +12,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:account', description: 'Add a user to an existing list.', diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index 6453d7d9808c..a1a81597a241 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -10,6 +10,8 @@ export const meta = { requireCredential: true, + prohibitMoved: true, + kind: 'write:account', description: 'Update the properties of a list.', From 75d02a51a61fdf97996a16fc3c6d1a822c184aea Mon Sep 17 00:00:00 2001 From: Namekuji Date: Thu, 13 Apr 2023 14:22:20 -0400 Subject: [PATCH 08/90] do not unfollow when moving --- packages/backend/src/core/AccountMoveService.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 75b15e052227..4362b40e2918 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -136,16 +136,12 @@ export class AccountMoveService { }, }); const followJobs: RelationshipJobData[] = []; - const unfollowJobs: RelationshipJobData[] = []; for (const following of followings) { if (!following.follower) continue; followJobs.push({ from: { id: following.follower.id }, to: { id: dst.id } }); - unfollowJobs.push({ from: { id: following.follower.id }, to: { id: src.id } }); } - // Should be queued because this can cause a number of follow/unfollow per one move. - // No need to care job orders as there should be no overlaps of follow/unfollow target. + // Should be queued because this can cause a number of follow per one move. this.queueService.createFollowJob(followJobs); - this.queueService.createUnfollowJob(unfollowJobs); } @bindThis From fc327f0567c0d058b4d5b48ad34eb3c875a20751 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Fri, 14 Apr 2023 09:15:13 -0400 Subject: [PATCH 09/90] adjust following and follower counts --- .../backend/src/core/AccountMoveService.ts | 48 +++++++++- .../backend/src/core/UserFollowingService.ts | 91 ++++++++++++------- 2 files changed, 102 insertions(+), 37 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 4362b40e2918..3a9d70a796c2 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -1,14 +1,13 @@ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull } from 'typeorm'; +import { IsNull, In } from 'typeorm'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { LocalUser } from '@/models/entities/User.js'; -import type { BlockingsRepository, FollowingsRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; +import type { BlockingsRepository, FollowingsRepository, InstancesRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; - -import { User } from '@/models/entities/User.js'; +import type { User } from '@/models/entities/User.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -19,8 +18,12 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { IdService } from '@/core/IdService.js'; -import { CacheService } from '@/core/CacheService'; +import { CacheService } from '@/core/CacheService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { MetaService } from '@/core/MetaService.js'; +import InstanceChart from '@/core/chart/charts/instance.js'; +import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; @Injectable() export class AccountMoveService { @@ -43,6 +46,9 @@ export class AccountMoveService { @Inject(DI.userListJoiningsRepository) private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + private idService: IdService, private userEntityService: UserEntityService, private apRendererService: ApRendererService, @@ -51,6 +57,10 @@ export class AccountMoveService { private userFollowingService: UserFollowingService, private accountUpdateService: AccountUpdateService, private proxyAccountService: ProxyAccountService, + private perUserFollowingChart: PerUserFollowingChart, + private federatedInstanceService: FederatedInstanceService, + private instanceChart: InstanceChart, + private metaService: MetaService, private relayService: RelayService, private cacheService: CacheService, private queueService: QueueService, @@ -140,6 +150,10 @@ export class AccountMoveService { if (!following.follower) continue; followJobs.push({ from: { id: following.follower.id }, to: { id: dst.id } }); } + + // Decrease following count instead of unfollowing. + await this.adjustFollowingCounts(followJobs.map(job => job.from.id), src); + // Should be queued because this can cause a number of follow per one move. this.queueService.createFollowJob(followJobs); } @@ -216,4 +230,28 @@ export class AccountMoveService { return this.userEntityService.isRemoteUser(user) ? user.uri : `${this.config.url}/users/${user.id}`; } + + @bindThis + private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: User) { + // Set the old account's following and followers counts to 0. + await this.usersRepository.update(oldAccount.id, { followersCount: 0, followingCount: 0 }); + + // Decrease following counts of local followers by 1. + await this.usersRepository.decrement({ id: In(localFollowerIds) }, 'followingCount', 1); + + // Update instance stats by decreasing remote followers count by the number of local followers who were following the old account. + if (this.userEntityService.isRemoteUser(oldAccount)) { + this.federatedInstanceService.fetch(oldAccount.host).then(async i => { + this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowers(i.host, false); + } + }); + } + + // FIXME: expensive? + for (const followerId of localFollowerIds) { + this.perUserFollowingChart.update({ id: followerId, host: null }, oldAccount, false); + } + } } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index f32048bf3862..addbca8000bc 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -230,32 +230,40 @@ export class UserFollowingService implements OnModuleInit { this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id }); - //#region Increment counts - await Promise.all([ - this.usersRepository.increment({ id: follower.id }, 'followingCount', 1), - this.usersRepository.increment({ id: followee.id }, 'followersCount', 1), + const [followeeUser, followerUser] = await Promise.all([ + this.usersRepository.findOneByOrFail({ id: followee.id }), + this.usersRepository.findOneByOrFail({ id: follower.id }), ]); - //#endregion - //#region Update instance stats - if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - this.federatedInstanceService.fetch(follower.host).then(async i => { - this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { - this.instanceChart.updateFollowing(i.host, true); - } - }); - } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - this.federatedInstanceService.fetch(followee.host).then(async i => { - this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { - this.instanceChart.updateFollowers(i.host, true); - } - }); - } - //#endregion + // Neither followee nor follower has moved. + if (!followeeUser.movedToUri && !followerUser.movedToUri) { + //#region Increment counts + await Promise.all([ + this.usersRepository.increment({ id: follower.id }, 'followingCount', 1), + this.usersRepository.increment({ id: followee.id }, 'followersCount', 1), + ]); + //#endregion + + //#region Update instance stats + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { + this.federatedInstanceService.fetch(follower.host).then(async i => { + this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowing(i.host, true); + } + }); + } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { + this.federatedInstanceService.fetch(followee.host).then(async i => { + this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowers(i.host, true); + } + }); + } + //#endregion - this.perUserFollowingChart.update(follower, followee, true); + this.perUserFollowingChart.update(follower, followee, true); + } // Publish follow event if (this.userEntityService.isLocalUser(follower) && !silent) { @@ -303,12 +311,18 @@ export class UserFollowingService implements OnModuleInit { }, silent = false, ): Promise { - const following = await this.followingsRepository.findOneBy({ - followerId: follower.id, - followeeId: followee.id, + const following = await this.followingsRepository.findOne({ + relations: { + follower: true, + followee: true, + }, + where: { + followerId: follower.id, + followeeId: followee.id, + } }); - if (following == null) { + if (following === null) { logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); return; } @@ -317,7 +331,10 @@ export class UserFollowingService implements OnModuleInit { this.cacheService.userFollowingsCache.refresh(follower.id); - this.decrementFollowing(follower, followee); + // Neither followee nor follower has moved. + if (!following.followee?.movedToUri && !following.follower?.movedToUri) { + this.decrementFollowing(follower, followee); + } // Publish unfollow event if (!silent && this.userEntityService.isLocalUser(follower)) { @@ -582,15 +599,25 @@ export class UserFollowingService implements OnModuleInit { */ @bindThis private async removeFollow(followee: Both, follower: Both): Promise { - const following = await this.followingsRepository.findOneBy({ - followeeId: followee.id, - followerId: follower.id, + const following = await this.followingsRepository.findOne({ + relations: { + followee: true, + follower: true, + }, + where: { + followeeId: followee.id, + followerId: follower.id, + } }); if (!following) return; await this.followingsRepository.delete(following.id); - this.decrementFollowing(follower, followee); + + // Neither followee nor follower has moved. + if (!following.followee?.movedToUri && !following.follower?.movedToUri) { + this.decrementFollowing(follower, followee); + } } /** From 942d5b6672beeabc5b7baf23d470a7ddcd1c4cee Mon Sep 17 00:00:00 2001 From: Namekuji Date: Fri, 14 Apr 2023 10:02:01 -0400 Subject: [PATCH 10/90] check movedToUri when receiving a follow request --- packages/backend/src/core/UserFollowingService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index addbca8000bc..df85ec852974 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -145,7 +145,9 @@ export class UserFollowingService implements OnModuleInit { try { await this.apPersonService.updatePerson(oldUri); const oldAccount = await this.apPersonService.resolvePerson(oldUri); - autoAccept = await this.followingsRepository.exist({ + const newUri = this.userEntityService.isRemoteUser(follower) ? follower.uri : `${this.config.url}/users/${follower.id}`; + + autoAccept = oldAccount.movedToUri === newUri && await this.followingsRepository.exist({ where: { followeeId: followee.id, followerId: oldAccount.id, From aaf098f6974faec76dc6a053ed2db84426c0d791 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Fri, 14 Apr 2023 10:14:40 -0400 Subject: [PATCH 11/90] skip if no need to adjust --- packages/backend/src/core/AccountMoveService.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 3a9d70a796c2..9e4462df0090 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -232,9 +232,11 @@ export class AccountMoveService { } @bindThis - private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: User) { + private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: User): Promise { + if (localFollowerIds.length === 0) return; + // Set the old account's following and followers counts to 0. - await this.usersRepository.update(oldAccount.id, { followersCount: 0, followingCount: 0 }); + await this.usersRepository.update({ id: oldAccount.id }, { followersCount: 0, followingCount: 0 }); // Decrease following counts of local followers by 1. await this.usersRepository.decrement({ id: In(localFollowerIds) }, 'followingCount', 1); From 69a1dadcbb074df9f329f24f37d30b48b507eda1 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Fri, 14 Apr 2023 19:53:59 -0400 Subject: [PATCH 12/90] Revert "disable account migration" This reverts commit 2321214c98591bcfe1385c1ab5bf0ff7b471ae1d. --- CHANGELOG.md | 1 + packages/backend/src/server/api/endpoints.ts | 4 ++-- packages/frontend/src/pages/settings/index.vue | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c18b47f0d68..dcd37e22c258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,6 +105,7 @@ - チャンネルをお気に入りに登録できるように - タイムラインのアンテナ選択などでは、フォローしているアンテナの代わりにお気に入りしたアンテナが表示されるようになっています。チャンネルをお気に入りに登録するには、当該チャンネルのページ→概要→⭐️のボタンを押します。 - チャンネルにノートをピン留めできるように +- アカウントの引っ越し(フォロワー引き継ぎ)に対応 ### Client - 投稿フォームのデザインを改善 diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 765ab6d93652..97b040c4ee36 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -555,8 +555,8 @@ 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/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/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index 17af7417fda4..7e9ec3de9366 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -164,12 +164,12 @@ const menuDef = computed(() => [{ text: i18n.ts.importAndExport, to: '/settings/import-export', active: currentPage?.route.name === 'import-export', - }, /*{ + }, { icon: 'ti ti-plane', text: i18n.ts.accountMigration, to: '/settings/migration', active: currentPage?.route.name === 'migration', - },*/ { + }, { icon: 'ti ti-dots', text: i18n.ts.other, to: '/settings/other', From 33851fbab6cd4fb32e98bf43a635d93184c0cfc0 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sat, 15 Apr 2023 00:19:13 -0400 Subject: [PATCH 13/90] fix translation specifier --- packages/frontend/src/pages/settings/migration.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue index 2ef8af7481d1..612391b00bab 100644 --- a/packages/frontend/src/pages/settings/migration.vue +++ b/packages/frontend/src/pages/settings/migration.vue @@ -36,7 +36,7 @@ async function move(): Promise { const account = moveToAccount.value; const confirm = await os.confirm({ type: 'warning', - text: i18n.t('migrationConfirm', { account: account.toString() }), + text: i18n.t('_accountMigration.migrationConfirm', { account: account.toString() }), }); if (confirm.canceled) return; os.apiWithDialog('i/move', { From 0e088cb2e427fec1253cae979f96a46b5c34320b Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sat, 15 Apr 2023 00:47:01 -0400 Subject: [PATCH 14/90] fix checking alsoKnownAs and uri --- .../backend/src/core/AccountMoveService.ts | 6 +++--- .../src/server/api/endpoints/i/move.ts | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 9e4462df0090..d239155700b6 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -74,12 +74,12 @@ export class AccountMoveService { */ @bindThis public async moveFromLocal(src: LocalUser, dst: User): Promise { - if (!dst.uri) throw new Error('destination uri is empty'); + const dstUri = this.getUserUri(dst); // add movedToUri to indicate that the user has moved const update = {} as Partial; - update.alsoKnownAs = src.alsoKnownAs?.concat([dst.uri]) ?? [dst.uri]; - update.movedToUri = dst.uri; + update.alsoKnownAs = src.alsoKnownAs?.concat([dstUri]) ?? [dstUri]; + update.movedToUri = dstUri; await this.usersRepository.update(src.id, update); const srcPerson = await this.apRendererService.renderPerson(src); diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index c1aa0f4ccb98..972dd6076d16 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -109,22 +109,27 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchMoveTarget); }); const destination = await this.getterService.getUser(moveTo.id); - moveTo.uri = this.accountMoveService.getUserUri(destination); + const newUri = this.accountMoveService.getUserUri(destination); // update local db - await this.apPersonService.updatePerson(moveTo.uri); + await this.apPersonService.updatePerson(newUri); // retrieve updated user - moveTo = await this.apPersonService.resolvePerson(moveTo.uri); + moveTo = await this.apPersonService.resolvePerson(newUri); // make sure that the user has indicated the old account as an alias const fromUrl = `${this.config.url}/users/${me.id}`; let allowed = false; - moveTo.alsoKnownAs?.forEach((elem) => { - if (fromUrl.includes(elem)) allowed = true; - }); + 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.destinationAccountForbids); + if (!allowed) throw new ApiError(meta.errors.destinationAccountForbids); return await this.accountMoveService.moveFromLocal(me, moveTo); }); From 17c80e82e016312fd4cc52641ae58a2e273c72eb Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sat, 15 Apr 2023 01:15:23 -0400 Subject: [PATCH 15/90] fix updating account --- packages/backend/src/core/AccountMoveService.ts | 2 ++ packages/backend/src/core/activitypub/ApInboxService.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index d239155700b6..1b89512d01b0 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -81,6 +81,7 @@ export class AccountMoveService { update.alsoKnownAs = src.alsoKnownAs?.concat([dstUri]) ?? [dstUri]; update.movedToUri = dstUri; await this.usersRepository.update(src.id, update); + src = Object.assign(src, update); const srcPerson = await this.apRendererService.renderPerson(src); const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src)); @@ -109,6 +110,7 @@ export class AccountMoveService { @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, { diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 95b41626aaed..60539e58ee3e 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -765,7 +765,8 @@ export class ApInboxService { } // add target uri to movedToUri in order to indicate that the user has moved - this.usersRepository.update(oldAccount.id, { movedToUri: targetUri }); + await this.usersRepository.update(oldAccount.id, { movedToUri: targetUri }); + oldAccount.movedToUri = targetUri; // Move! await this.accountMoveService.move(oldAccount, newAccount); From 2f5d9b86ef724bd93e08b0661ed881de035d3b70 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sat, 15 Apr 2023 02:01:34 -0400 Subject: [PATCH 16/90] fix refollowing locked account --- .../backend/src/core/UserFollowingService.ts | 38 +++++++++++-------- .../activitypub/models/ApPersonService.ts | 2 + 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index df85ec852974..d9799749967f 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -86,7 +86,7 @@ export class UserFollowingService implements OnModuleInit { @bindThis public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string, silent = false): Promise { - const [follower, followee] = await Promise.all([ + let [follower, followee] = await Promise.all([ this.usersRepository.findOneByOrFail({ id: _follower.id }), this.usersRepository.findOneByOrFail({ id: _followee.id }), ]); @@ -140,21 +140,27 @@ export class UserFollowingService implements OnModuleInit { } // Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account. - if (followee.isLocked && !autoAccept && follower.alsoKnownAs) { - for (const oldUri of follower.alsoKnownAs) { - try { - await this.apPersonService.updatePerson(oldUri); - const oldAccount = await this.apPersonService.resolvePerson(oldUri); - const newUri = this.userEntityService.isRemoteUser(follower) ? follower.uri : `${this.config.url}/users/${follower.id}`; - - autoAccept = oldAccount.movedToUri === newUri && await this.followingsRepository.exist({ - where: { - followeeId: followee.id, - followerId: oldAccount.id, - }, - }); - } catch { - /* skip if any error happens */ + if (followee.isLocked && !autoAccept) { + if (this.userEntityService.isRemoteUser(follower)) { + await this.apPersonService.updatePerson(follower.uri); + follower = await this.apPersonService.resolvePerson(follower.uri); + } + if (follower.alsoKnownAs) { + for (const oldUri of follower.alsoKnownAs) { + try { + await this.apPersonService.updatePerson(oldUri); + const oldAccount = await this.apPersonService.resolvePerson(oldUri); + const newUri = this.userEntityService.isRemoteUser(follower) ? follower.uri : `${this.config.url}/users/${follower.id}`; + + autoAccept = oldAccount.movedToUri === newUri && await this.followingsRepository.exist({ + where: { + followeeId: followee.id, + followerId: oldAccount.id, + }, + }); + } catch { + /* skip if any error happens */ + } } } } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 3e86d3b44dfe..6c225de33347 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -527,6 +527,8 @@ export class ApPersonService implements OnModuleInit { await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err)); + this.cacheService.uriPersonCache.set(uri, Object.assign(exist, updates)); + // Copy blocking and muting if we know its moving for the first time. if (!exist.movedToUri && updates.movedToUri) { try { From 83d70f330029de1784b5b66711ebaf093bc2b2d4 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sat, 15 Apr 2023 02:20:26 -0400 Subject: [PATCH 17/90] decrease followersCount if followed by the old account --- packages/backend/src/core/AccountMoveService.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 1b89512d01b0..8056803dfe7d 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -243,6 +243,12 @@ export class AccountMoveService { // Decrease following counts of local followers by 1. await this.usersRepository.decrement({ id: In(localFollowerIds) }, 'followingCount', 1); + // Decrease follower counts of local followees by 1. + const oldFollowings = await this.followingsRepository.findBy({ followerId: oldAccount.id }); + if (oldFollowings.length > 0) { + await this.usersRepository.decrement({ id: In(oldFollowings.map(following => following.followeeId)) }, 'followersCount', 1); + } + // Update instance stats by decreasing remote followers count by the number of local followers who were following the old account. if (this.userEntityService.isRemoteUser(oldAccount)) { this.federatedInstanceService.fetch(oldAccount.host).then(async i => { From 6a2752ef7346568679347e0cec1da7c84fe5d5c8 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sat, 15 Apr 2023 03:37:26 -0400 Subject: [PATCH 18/90] adjust following and followers counts when unfollowing --- .../backend/src/core/UserFollowingService.ts | 104 ++++++++++++------ 1 file changed, 68 insertions(+), 36 deletions(-) diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index d9799749967f..b7c75366515b 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -23,6 +23,7 @@ import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; import Logger from '../logger.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; +import { IsNull } from 'typeorm'; const logger = new Logger('following/create'); @@ -330,7 +331,7 @@ export class UserFollowingService implements OnModuleInit { } }); - if (following === null) { + if (following === null || !following.follower || !following.followee) { logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); return; } @@ -339,10 +340,7 @@ export class UserFollowingService implements OnModuleInit { this.cacheService.userFollowingsCache.refresh(follower.id); - // Neither followee nor follower has moved. - if (!following.followee?.movedToUri && !following.follower?.movedToUri) { - this.decrementFollowing(follower, followee); - } + this.decrementFollowing(following.follower, following.followee); // Publish unfollow event if (!silent && this.userEntityService.isLocalUser(follower)) { @@ -374,37 +372,74 @@ export class UserFollowingService implements OnModuleInit { @bindThis private async decrementFollowing( - follower: { id: User['id']; host: User['host']; }, - followee: { id: User['id']; host: User['host']; }, + follower: User, + followee: User, ): Promise { this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id }); - //#region Decrement following / followers counts - await Promise.all([ - this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1), - this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1), - ]); - //#endregion + // Neither followee nor follower has moved. + if (!follower.movedToUri && !followee.movedToUri) { + //#region Decrement following / followers counts + await Promise.all([ + this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1), + this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1), + ]); + //#endregion - //#region Update instance stats - if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - this.federatedInstanceService.fetch(follower.host).then(async i => { - this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { - this.instanceChart.updateFollowing(i.host, false); - } - }); - } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - this.federatedInstanceService.fetch(followee.host).then(async i => { - this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { - this.instanceChart.updateFollowers(i.host, false); - } - }); - } - //#endregion + //#region Update instance stats + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { + this.federatedInstanceService.fetch(follower.host).then(async i => { + this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowing(i.host, false); + } + }); + } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { + this.federatedInstanceService.fetch(followee.host).then(async i => { + this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowers(i.host, false); + } + }); + } + //#endregion - this.perUserFollowingChart.update(follower, followee, false); + this.perUserFollowingChart.update(follower, followee, false); + } else { + // Adjust following/followers counts + for (const user of [follower, followee]) { + if (user.movedToUri) continue; // No need to update if the user has already moved. + + const nonMovedFollowees = await this.followingsRepository.count({ + relations: { + followee: true, + }, + where: { + followerId: user.id, + followee: { + movedToUri: IsNull(), + } + } + }); + const nonMovedFollowers = await this.followingsRepository.count({ + relations: { + follower: true, + }, + where: { + followeeId: user.id, + follower: { + movedToUri: IsNull(), + } + } + }); + await this.usersRepository.update( + { id: user.id }, + { followingCount: nonMovedFollowees, followersCount: nonMovedFollowers }, + ); + } + + // TODO: adjust charts + } } @bindThis @@ -618,14 +653,11 @@ export class UserFollowingService implements OnModuleInit { } }); - if (!following) return; + if (!following || !following.followee || !following.follower) return; await this.followingsRepository.delete(following.id); - // Neither followee nor follower has moved. - if (!following.followee?.movedToUri && !following.follower?.movedToUri) { - this.decrementFollowing(follower, followee); - } + this.decrementFollowing(following.follower, following.followee); } /** From 71e9b2298ebf29a8148bf467301a37bb421cb343 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sat, 15 Apr 2023 05:11:31 -0400 Subject: [PATCH 19/90] fix copying mutings --- packages/backend/src/core/AccountMoveService.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 8056803dfe7d..bbcb8c66cc6d 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -185,7 +185,12 @@ export class AccountMoveService { @bindThis public async copyMutings(src: ThinUser, dst: ThinUser): Promise { // Insert new mutings with the same values except mutee - const mutings = await this.mutingsRepository.findBy({ muteeId: src.id }); + const mutings = await this.mutingsRepository.find({ + relations: { + muter: true, + }, + where: { muteeId: src.id } + }); const newMuting: Partial[] = []; for (const muting of mutings) { newMuting.push({ @@ -196,7 +201,7 @@ export class AccountMoveService { muteeId: dst.id, }); } - this.mutingsRepository.insert(mutings); // no need to wait + await this.mutingsRepository.insert(mutings); for (const mute of mutings) { if (mute.muter) this.cacheService.userMutingsCache.refresh(mute.muter.id); } From 21677aa30d3b45aebea83b2392e2fc6960ac1bb0 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sat, 15 Apr 2023 05:30:39 -0400 Subject: [PATCH 20/90] prohibit moved account from moving again --- packages/backend/src/server/api/endpoints/i/known-as.ts | 5 ++--- packages/backend/src/server/api/endpoints/i/move.ts | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/i/known-as.ts b/packages/backend/src/server/api/endpoints/i/known-as.ts index 7aa401e9bb78..dc96e5e2eb64 100644 --- a/packages/backend/src/server/api/endpoints/i/known-as.ts +++ b/packages/backend/src/server/api/endpoints/i/known-as.ts @@ -7,7 +7,6 @@ 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 = { @@ -15,6 +14,7 @@ export const meta = { secure: true, requireCredential: true, + prohibitMoved: true, limit: { duration: ms('1day'), @@ -46,7 +46,6 @@ export const paramDef = { @Injectable() export default class extends Endpoint { constructor( - private userEntityService: UserEntityService, private remoteUserResolveService: RemoteUserResolveService, private apiLoggerService: ApiLoggerService, private accountMoveService: AccountMoveService, @@ -76,7 +75,7 @@ export default class extends Endpoint { const toUrl = this.accountMoveService.getUserUri(knownAs); if (!toUrl) throw new ApiError(meta.errors.uriNull); - updates.alsoKnownAs = updates.alsoKnownAs?.concat([toUrl]) ?? [toUrl]; + 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/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index 972dd6076d16..a13e6cda41be 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -19,6 +19,7 @@ export const meta = { secure: true, requireCredential: true, + prohibitMoved: true, limit: { duration: ms('1day'), max: 5, From 8d6f50c4c30de04b30cb52c9076790a13d74bd46 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sat, 15 Apr 2023 06:49:01 -0400 Subject: [PATCH 21/90] fix move service --- .../backend/src/core/AccountMoveService.ts | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index bbcb8c66cc6d..e89bdf1af58a 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, In } from 'typeorm'; +import { IsNull, In, MoreThan, Not } from 'typeorm'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; @@ -138,6 +138,7 @@ export class AccountMoveService { ]); // follow the new account and unfollow the old one + const proxy = await this.proxyAccountService.fetch(); const followings = await this.followingsRepository.find({ relations: { follower: true, @@ -145,6 +146,7 @@ export class AccountMoveService { where: { followeeId: src.id, followerHost: IsNull(), // follower is local + followerId: proxy ? Not(proxy.id) : undefined, }, }); const followJobs: RelationshipJobData[] = []; @@ -185,27 +187,14 @@ export class AccountMoveService { @bindThis public async copyMutings(src: ThinUser, dst: ThinUser): Promise { // Insert new mutings with the same values except mutee - const mutings = await this.mutingsRepository.find({ - relations: { - muter: true, - }, - where: { muteeId: src.id } - }); - const newMuting: Partial[] = []; - for (const muting of mutings) { - newMuting.push({ - id: this.idService.genId(), - createdAt: new Date(), - expiresAt: muting.expiresAt, - muterId: muting.muterId, - muteeId: dst.id, - }); - } - await this.mutingsRepository.insert(mutings); - for (const mute of mutings) { - if (mute.muter) this.cacheService.userMutingsCache.refresh(mute.muter.id); + const mutings = await this.mutingsRepository.findBy({ muteeId: src.id, expiresAt: MoreThan(new Date()) }); + const muteIds = mutings.map(mute => mute.id); + if (muteIds.length > 0) { + await this.mutingsRepository.update({ id: In(muteIds) }, { muteeId: dst.id }); + for (const muterId of mutings.map(mute => mute.muterId)) { + this.cacheService.userMutingsCache.refresh(muterId); + } } - // no need to unmute the old account because it may be still functional } @bindThis From e900a8787ee694a0b1a1a9e1623159ffc844074f Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sat, 15 Apr 2023 07:19:28 -0400 Subject: [PATCH 22/90] allow app creation after moving --- packages/backend/src/server/api/endpoints/app/create.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts index e5c8d08fb394..c1d0a9dd7485 100644 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ b/packages/backend/src/server/api/endpoints/app/create.ts @@ -12,8 +12,6 @@ export const meta = { requireCredential: false, - prohibitMoved: true, - res: { type: 'object', optional: false, nullable: false, From 741fefbe584e5f70b6b2b6c2c0ff5010bfc4af72 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sat, 15 Apr 2023 07:27:35 -0400 Subject: [PATCH 23/90] fix lint --- packages/backend/src/core/UserFollowingService.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index b7c75366515b..8a1026f6e307 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -87,7 +87,7 @@ export class UserFollowingService implements OnModuleInit { @bindThis public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string, silent = false): Promise { - let [follower, followee] = await Promise.all([ + const [follower, followee] = await Promise.all([ this.usersRepository.findOneByOrFail({ id: _follower.id }), this.usersRepository.findOneByOrFail({ id: _followee.id }), ]); @@ -142,16 +142,17 @@ export class UserFollowingService implements OnModuleInit { // Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account. if (followee.isLocked && !autoAccept) { - if (this.userEntityService.isRemoteUser(follower)) { - await this.apPersonService.updatePerson(follower.uri); - follower = await this.apPersonService.resolvePerson(follower.uri); + let movedFollower = follower; + if (this.userEntityService.isRemoteUser(movedFollower)) { + await this.apPersonService.updatePerson(movedFollower.uri); + movedFollower = await this.apPersonService.resolvePerson(movedFollower.uri); } - if (follower.alsoKnownAs) { - for (const oldUri of follower.alsoKnownAs) { + if (movedFollower.alsoKnownAs) { + for (const oldUri of movedFollower.alsoKnownAs) { try { await this.apPersonService.updatePerson(oldUri); const oldAccount = await this.apPersonService.resolvePerson(oldUri); - const newUri = this.userEntityService.isRemoteUser(follower) ? follower.uri : `${this.config.url}/users/${follower.id}`; + const newUri = this.userEntityService.isRemoteUser(movedFollower) ? movedFollower.uri : `${this.config.url}/users/${movedFollower.id}`; autoAccept = oldAccount.movedToUri === newUri && await this.followingsRepository.exist({ where: { From 3ccf1d530736ded7ff952b2a13a26f9c7ae4af99 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sat, 15 Apr 2023 08:54:14 -0400 Subject: [PATCH 24/90] remove unnecessary field --- CHANGELOG.md | 4 ++-- packages/backend/src/core/AccountMoveService.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcd37e22c258..ca007feadfbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,8 @@ ## 13.x.x (unreleased) ### General -- +- アカウントの引っ越し(フォロワー引き継ぎ)に対応 + * 一度引っ越したアカウントは利用に制限がかかります ### Client - コントロールパネルのカスタム絵文字ページおよびaboutのカスタム絵文字の検索インプットで、`:emojiname1::emojiname2:`のように検索して絵文字を検索できるように @@ -105,7 +106,6 @@ - チャンネルをお気に入りに登録できるように - タイムラインのアンテナ選択などでは、フォローしているアンテナの代わりにお気に入りしたアンテナが表示されるようになっています。チャンネルをお気に入りに登録するには、当該チャンネルのページ→概要→⭐️のボタンを押します。 - チャンネルにノートをピン留めできるように -- アカウントの引っ越し(フォロワー引き継ぎ)に対応 ### Client - 投稿フォームのデザインを改善 diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index e89bdf1af58a..5aaeac0f48cd 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -17,7 +17,6 @@ import { UserFollowingService } from '@/core/UserFollowingService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; @@ -49,7 +48,6 @@ export class AccountMoveService { @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, - private idService: IdService, private userEntityService: UserEntityService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, @@ -209,7 +207,7 @@ export class AccountMoveService { await this.userListJoiningsRepository.update( { userId: src.id }, - { userId: dst.id, user: dst } + { userId: dst.id } ); // Have the proxy account follow the new account in the same way as UserListService.push From 7f43e76a898c676f058fd96e3acb358784499b84 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sat, 15 Apr 2023 09:19:58 -0400 Subject: [PATCH 25/90] fix cache update --- packages/backend/src/core/activitypub/models/ApPersonService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 6c225de33347..7e5a03b10395 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -527,7 +527,7 @@ export class ApPersonService implements OnModuleInit { await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err)); - this.cacheService.uriPersonCache.set(uri, Object.assign(exist, updates)); + this.cacheService.uriPersonCache.set(uri, { ...exist, ...updates }); // Copy blocking and muting if we know its moving for the first time. if (!exist.movedToUri && updates.movedToUri) { From 3f8497d4f740f00e805b9989da605decbffd7eb0 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Mon, 17 Apr 2023 11:19:28 -0400 Subject: [PATCH 26/90] add e2e test --- .../backend/src/core/AccountMoveService.ts | 5 +- packages/backend/test/e2e/move.ts | 312 ++++++++++++++++++ 2 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 packages/backend/test/e2e/move.ts diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 5aaeac0f48cd..2babcab55588 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -185,7 +185,10 @@ export class AccountMoveService { @bindThis public async copyMutings(src: ThinUser, dst: ThinUser): Promise { // Insert new mutings with the same values except mutee - const mutings = await this.mutingsRepository.findBy({ muteeId: src.id, expiresAt: MoreThan(new Date()) }); + const mutings = await this.mutingsRepository.findBy([ + { muteeId: src.id, expiresAt: IsNull() }, + { muteeId: src.id, expiresAt: MoreThan(new Date()) }, + ]); const muteIds = mutings.map(mute => mute.id); if (muteIds.length > 0) { await this.mutingsRepository.update({ id: In(muteIds) }, { muteeId: dst.id }); diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts new file mode 100644 index 000000000000..6f581a00efa0 --- /dev/null +++ b/packages/backend/test/e2e/move.ts @@ -0,0 +1,312 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { signup, startServer, initTestDb, api, sleep } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; +import { loadConfig } from '@/config.js'; +import { Blocking, BlockingsRepository, Following, FollowingsRepository, Muting, MutingsRepository, User, UsersRepository } from '@/models/index.js'; +import { jobQueue } from '@/boot/common.js'; +import rndstr from 'rndstr'; +import { uploadFile } from '../utils.js'; + +describe('Account Move', () => { + let app: INestApplicationContext; + let url: URL; + + let root: any; + let alice: any; + let bob: any; + let carol: any; + let dave: any; + let eve: any; + + let Users: UsersRepository; + let Followings: FollowingsRepository; + let Blockings: BlockingsRepository; + let Mutings: MutingsRepository; + + beforeAll(async () => { + app = await startServer(); + await jobQueue(); + const config = loadConfig(); + url = new URL(config.url); + const connection = await initTestDb(false); + root = await signup({ username: 'root' }); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + dave = await signup({ username: 'dave' }); + eve = await signup({ username: 'eve' }); + Users = connection.getRepository(User); + Followings = connection.getRepository(Following); + Blockings = connection.getRepository(Blocking); + Mutings = connection.getRepository(Muting); + }, 1000 * 60 * 2); + + + afterAll(async () => { + await app.close(); + }); + + describe('Create Alias', () => { + afterEach(async () => { + await Users.update(bob.id, { alsoKnownAs: null }); + }, 1000 * 10); + + test('Unable to add a nonexisting local account to alsoKnownAs', async () => { + const res = await api('/i/known-as', { + alsoKnownAs: `@nonexist@${url.hostname}`, + }, bob); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'NO_SUCH_USER'); + assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); + }); + + 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}`, + }, bob); + + const newAlice = await Users.findOneByOrFail({ id: bob.id }); + assert.strictEqual(newAlice.alsoKnownAs?.length, 2); + assert.strictEqual(newAlice.alsoKnownAs[0], `${url.origin}/users/${alice.id}`); + assert.strictEqual(newAlice.alsoKnownAs[1], `${url.origin}/users/${carol.id}`); + }); + + test('Unable to create an alias without the second @', async () => { + const res1 = await api('/i/known-as', { + 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' + }, bob); + + assert.strictEqual(res2.status, 400); + assert.strictEqual(res2.body.error.code, 'NO_SUCH_USER'); + assert.strictEqual(res2.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); + }); + }) + + describe('Local to Local', () => { + let antennaId = ''; + + beforeAll(async () => { + await api('/i/known-as', { + alsoKnownAs: `@alice@${url.hostname}`, + }, root); + const list = await api('/users/lists/create', { + name: rndstr('0-9a-z', 8), + }, root); + await api('/users/lists/push', { + listId: list.body.id, + userId: alice.id, + }, root); + + await api('/following/create', { + userId: root.id, + }, alice); + await api('/following/create', { + userId: eve.id, + }, alice); + const antenna = await api('/antennas/create', { + name: rndstr('0-9a-z', 8), + src: 'home', + keywords: [rndstr('0-9a-z', 8)], + excludeKeywords: [], + users: [], + caseSensitive: false, + withReplies: false, + withFile: false, + notify: false, + }, alice); + antennaId = antenna.body.id; + + await api('/i/known-as', { + alsoKnownAs: `@alice@${url.hostname}`, + }, bob); + + await api('/following/create', { + userId: alice.id, + }, carol); + + await api('/mute/create', { + userId: alice.id, + }, dave); + await api('/blocking/create', { + userId: alice.id, + }, dave); + await api('/following/create', { + userId: eve.id, + }, dave); + + await api('/following/create', { + userId: dave.id, + }, eve); + }, 1000 * 10); + + test('Prohibit the root account from moving', async () => { + const res = await api('/i/move', { + moveToAccount: `@bob@${url.hostname}` + }, root); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'NOT_ROOT_FORBIDDEN'); + assert.strictEqual(res.body.error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24'); + }); + + test('Unable to move to a nonexisting local account', async () => { + const res = await api('/i/move', { + moveToAccount: `@nonexist@${url.hostname}`, + }, alice); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'NO_SUCH_MOVE_TARGET'); + assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f76c202ba4'); + }); + + test('Unable to move if alsoKnownAs is invalid', async () => { + const res = await api('/i/move', { + moveToAccount: `@carol@${url.hostname}`, + }, alice); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'REMOTE_ACCOUNT_FORBIDS'); + assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); + }); + + test('Relationships have been properly migrated', async () => { + const move = await api('/i/move', { + moveToAccount: `@bob@${url.hostname}`, + }, alice); + + assert.strictEqual(move.status, 200); + + await sleep(1000 * 1); // wait for jobs to finish + + const followings = await api('/users/following', { + userId: carol.id, + }, carol) + assert.strictEqual(followings.status, 200); + assert.strictEqual(followings.body.length, 2); + assert.strictEqual(followings.body[0].followeeId, bob.id); + assert.strictEqual(followings.body[1].followeeId, alice.id); + + const blockings = await api('/blocking/list', {}, dave); + assert.strictEqual(blockings.status, 200); + assert.strictEqual(blockings.body.length, 2); + assert.strictEqual(blockings.body[0].blockeeId, bob.id); + assert.strictEqual(blockings.body[1].blockeeId, alice.id); + + const mutings = await api('/mute/list', {}, dave); + assert.strictEqual(mutings.status, 200); + assert.strictEqual(mutings.body.length, 1); + assert.strictEqual(mutings.body[0].muteeId, bob.id); + + const lists = await api('/users/lists/list', {}, root); + assert.strictEqual(lists.status, 200); + assert.strictEqual(lists.body[0].userIds.length, 1); + assert.strictEqual(lists.body[0].userIds[0], bob.id); + }); + + test('Follow and follower counts are properly adjusted', async () => { + await api('/following/create', { + userId: alice.id, + }, eve); + const newAlice = await Users.findOneByOrFail({ id: alice.id }); + const newCarol = await Users.findOneByOrFail({ id: carol.id }); + let newEve = await Users.findOneByOrFail({ id: eve.id }); + assert.strictEqual(newAlice.movedToUri, `${url.origin}/users/${bob.id}`); + assert.strictEqual(newAlice.followingCount, 0); + assert.strictEqual(newAlice.followersCount, 0); + assert.strictEqual(newCarol.followingCount, 1); + assert.strictEqual(newEve.followingCount, 1); + assert.strictEqual(newEve.followersCount, 1); + + await api('/following/delete', { + userId: alice.id, + }, eve); + newEve = await Users.findOneByOrFail({ id: eve.id }); + assert.strictEqual(newEve.followingCount, 1); + assert.strictEqual(newEve.followersCount, 1); + }); + + test.each([ + '/antennas/create', + '/channels/create', + '/channels/favorite', + '/channels/follow', + '/channels/unfavorite', + '/channels/unfollow', + '/clips/add-note', + '/clips/create', + '/clips/favorite', + '/clips/remove-note', + '/clips/unfavorite', + '/clips/update', + '/drive/files/upload-from-url', + '/flash/create', + '/flash/like', + '/flash/unlike', + '/flash/update', + '/following/create', + '/gallery/posts/create', + '/gallery/posts/like', + '/gallery/posts/unlike', + '/gallery/posts/update', + '/i/known-as', + '/i/move', + '/notes/create', + '/notes/polls/vote', + '/notes/reactions/create', + '/pages/create', + '/pages/like', + '/pages/unlike', + '/pages/update', + '/users/lists/create', + '/users/lists/pull', + '/users/lists/push', + '/users/lists/update', + ])('Prohibit access after moving: %s', async (endpoint) => { + const res = await api(endpoint, {}, alice); + assert.strictEqual(res.status, 403); + assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + }); + + test('Prohibit access after moving: /antennas/update', async () => { + const res = await api('/antennas/update', { + antennaId, + name: rndstr('0-9a-z', 8), + src: 'users', + keywords: [rndstr('0-9a-z', 8)], + excludeKeywords: [], + users: [eve.id], + caseSensitive: false, + withReplies: false, + withFile: false, + notify: false, + }, alice); + + assert.strictEqual(res.status, 403); + assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + }); + + test('Prohibit access after moving: /drive/files/create', async () => { + const res = await uploadFile(alice); + + assert.strictEqual(res.status, 403); + assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + }); + }); +}); From bc7e8010634b96da6d96ff351815e37c3717df2a Mon Sep 17 00:00:00 2001 From: Namekuji Date: Mon, 17 Apr 2023 11:44:54 -0400 Subject: [PATCH 27/90] add e2e test of accepting the new account automatically --- .../backend/src/core/UserFollowingService.ts | 1 + packages/backend/test/e2e/move.ts | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 8a1026f6e307..2f52d91b4d21 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -160,6 +160,7 @@ export class UserFollowingService implements OnModuleInit { followerId: oldAccount.id, }, }); + if (autoAccept) break; } catch { /* skip if any error happens */ } diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index 6f581a00efa0..79d2496aa123 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -19,6 +19,7 @@ describe('Account Move', () => { let carol: any; let dave: any; let eve: any; + let frank: any; let Users: UsersRepository; let Followings: FollowingsRepository; @@ -37,6 +38,7 @@ describe('Account Move', () => { carol = await signup({ username: 'carol' }); dave = await signup({ username: 'dave' }); eve = await signup({ username: 'eve' }); + frank = await signup({ username: 'frank' }); Users = connection.getRepository(User); Followings = connection.getRepository(Following); Blockings = connection.getRepository(Blocking); @@ -151,6 +153,16 @@ describe('Account Move', () => { await api('/following/create', { userId: dave.id, }, eve); + + await api('/i/update', { + isLocked: true, + }, frank); + await api('/following/create', { + userId: frank.id, + }, alice); + await api('/following/requests/accept', { + userId: alice.id, + }, frank); }, 1000 * 10); test('Prohibit the root account from moving', async () => { @@ -239,6 +251,19 @@ describe('Account Move', () => { assert.strictEqual(newEve.followersCount, 1); }); + test('A locked account automatically accept the follow request if it had already accepted the old account.', async () => { + await api('/following/create', { + userId: frank.id, + }, bob); + const followers = await api('/users/followers', { + userId: frank.id, + }, frank); + + assert.strictEqual(followers.status, 200); + assert.strictEqual(followers.body.length, 2); + assert.strictEqual(followers.body[0].followerId, bob.id); + }); + test.each([ '/antennas/create', '/channels/create', From 7f287e0c3a718e5c299b9958707a762c7e70442f Mon Sep 17 00:00:00 2001 From: Namekuji Date: Mon, 17 Apr 2023 12:03:26 -0400 Subject: [PATCH 28/90] force follow if any error happens --- packages/backend/src/core/AccountMoveService.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 2babcab55588..6d12f682f159 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -129,11 +129,15 @@ export class AccountMoveService { @bindThis public async move(src: User, dst: User): Promise { // Copy blockings and mutings, and update lists - await Promise.all([ - this.copyBlocking(src, dst), - this.copyMutings(src, dst), - this.updateLists(src, dst), - ]); + try { + await Promise.all([ + this.copyBlocking(src, dst), + this.copyMutings(src, dst), + this.updateLists(src, dst), + ]); + } catch { + /* skip if any error happens */ + } // follow the new account and unfollow the old one const proxy = await this.proxyAccountService.fetch(); From ad1bacc2e5f94926efe7d6441fb70549cf4a89e5 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Mon, 17 Apr 2023 13:32:55 -0400 Subject: [PATCH 29/90] remove unnecessary joins --- .../backend/src/core/AccountMoveService.ts | 33 ++++++++----------- .../src/core/activitypub/ApInboxService.ts | 6 ++-- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 6d12f682f159..55874908eed6 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -141,24 +141,22 @@ export class AccountMoveService { // follow the new account and unfollow the old one const proxy = await this.proxyAccountService.fetch(); - const followings = await this.followingsRepository.find({ - relations: { - follower: true, - }, - where: { + const followings = await this.followingsRepository.findBy({ followeeId: src.id, followerHost: IsNull(), // follower is local followerId: proxy ? Not(proxy.id) : undefined, - }, }); const followJobs: RelationshipJobData[] = []; for (const following of followings) { - if (!following.follower) continue; - followJobs.push({ from: { id: following.follower.id }, to: { id: dst.id } }); + followJobs.push({ from: { id: following.followerId }, to: { id: dst.id } }); } // Decrease following count instead of unfollowing. - await this.adjustFollowingCounts(followJobs.map(job => job.from.id), src); + try { + await this.adjustFollowingCounts(followJobs.map(job => job.from.id), src); + } catch { + /* skip if any error happens */ + } // Should be queued because this can cause a number of follow per one move. this.queueService.createFollowJob(followJobs); @@ -168,19 +166,14 @@ export class AccountMoveService { public async copyBlocking(src: ThinUser, dst: ThinUser): Promise { // Followers shouldn't overlap with blockers, but the destination account, different from the blockee (i.e., old account), may have followed the local user before moving. // So block the destination account here. - const blockings = await this.blockingsRepository.find({ // FIXME: might be expensive - relations: { - blocker: true - }, - where: { - blockeeId: src.id - } - }); + const srcBlockings = await this.blockingsRepository.findBy({ blockeeId: src.id }); + const dstBlockings = await this.blockingsRepository.findBy({ blockeeId: dst.id }); + const blockerIds = dstBlockings.map(blocking => blocking.blockerId); // reblock the destination account const blockJobs: RelationshipJobData[] = []; - for (const blocking of blockings) { - if (!blocking.blocker) continue; - blockJobs.push({ from: { id: blocking.blocker.id }, to: { id: dst.id } }); + for (const blocking of srcBlockings) { + if (blockerIds.includes(blocking.blockerId)) continue; // skip if already blocked + blockJobs.push({ from: { id: blocking.blockerId }, to: { id: dst.id } }); } // no need to unblock the old account because it may be still functional this.queueService.createBlockJob(blockJobs); diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 60539e58ee3e..9df6ffad7cc7 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -765,8 +765,10 @@ export class ApInboxService { } // add target uri to movedToUri in order to indicate that the user has moved - await this.usersRepository.update(oldAccount.id, { movedToUri: targetUri }); - oldAccount.movedToUri = targetUri; + if (oldAccount.movedToUri !== targetUri) { + await this.usersRepository.update(oldAccount.id, { movedToUri: targetUri }); + oldAccount.movedToUri = targetUri; + } // Move! await this.accountMoveService.move(oldAccount, newAccount); From ccfbc89047602e492007f95daa8db6bb52d7c4fb Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 20 Apr 2023 16:45:28 +0000 Subject: [PATCH 30/90] use Array.map instead of for const of --- packages/backend/src/core/AccountMoveService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 55874908eed6..2b74bc8afcd5 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -146,10 +146,10 @@ export class AccountMoveService { followerHost: IsNull(), // follower is local followerId: proxy ? Not(proxy.id) : undefined, }); - const followJobs: RelationshipJobData[] = []; - for (const following of followings) { - followJobs.push({ from: { id: following.followerId }, to: { id: dst.id } }); - } + const followJobs = followings.map(following => ({ + from: { id: following.followerId }, + to: { id: dst.id }, + })) as RelationshipJobData[]; // Decrease following count instead of unfollowing. try { From f27038448c886026bfa14b689864eb2a28f95a89 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 20 Apr 2023 18:03:06 +0000 Subject: [PATCH 31/90] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E3=83=AA=E3=82=B9=E3=83=88=E3=81=AE=E7=A7=BB=E8=A1=8C=E3=81=AF?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=AE=E3=81=BF=E3=82=92=E8=A1=8C=E3=81=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/core/AccountMoveService.ts | 41 +++++++++++++++---- packages/backend/test/e2e/move.ts | 8 ++-- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 2b74bc8afcd5..4db0f928e52a 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -5,10 +5,11 @@ import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { LocalUser } from '@/models/entities/User.js'; -import type { BlockingsRepository, FollowingsRepository, InstancesRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; +import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; import type { User } from '@/models/entities/User.js'; +import { IdService } from '@/core/IdService.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { QueueService } from '@/core/QueueService.js'; @@ -49,6 +50,7 @@ export class AccountMoveService { private instancesRepository: InstancesRepository, private userEntityService: UserEntityService, + private idService: IdService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, private globalEventService: GlobalEventService, @@ -195,20 +197,45 @@ export class AccountMoveService { } } + /** + * Update lists while moving accounts. + * - No removal of the old account from the lists + * - Users number limit is not checked + * + * @param src ThinUser (old account) + * @param dst User (new account) + * @returns Promise + */ @bindThis public async updateLists(src: ThinUser, dst: User): Promise { // Return if there is no list to be updated. - const exists = await this.userListJoiningsRepository.exist({ + const oldJoinings = await this.userListJoiningsRepository.find({ where: { userId: src.id, }, }); - if (!exists) return; + if (oldJoinings.length === 0) return; + + const newJoinings: Map = new Map(); + + // 重複しないようにIDを生成 + const genId = () => { + let id: string; + do { + id = this.idService.genId(); + } while (newJoinings.has(id)); + return id; + }; + for (const joining of oldJoinings) { + newJoinings.set(genId(), { + createdAt: new Date(), + userId: dst.id, + userListId: joining.userListId, + }); + } - await this.userListJoiningsRepository.update( - { userId: src.id }, - { userId: dst.id } - ); + const arrayToInsert = Array.from(newJoinings.entries()).map(entry => ({ ...entry[1], id: entry[0] })); + await this.userListJoiningsRepository.insert(arrayToInsert); // Have the proxy account follow the new account in the same way as UserListService.push if (this.userEntityService.isRemoteUser(dst)) { diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index 79d2496aa123..7330d50ef345 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -45,7 +45,6 @@ describe('Account Move', () => { Mutings = connection.getRepository(Muting); }, 1000 * 60 * 2); - afterAll(async () => { await app.close(); }); @@ -206,7 +205,7 @@ describe('Account Move', () => { const followings = await api('/users/following', { userId: carol.id, - }, carol) + }, carol); assert.strictEqual(followings.status, 200); assert.strictEqual(followings.body.length, 2); assert.strictEqual(followings.body[0].followeeId, bob.id); @@ -225,8 +224,9 @@ describe('Account Move', () => { const lists = await api('/users/lists/list', {}, root); assert.strictEqual(lists.status, 200); - assert.strictEqual(lists.body[0].userIds.length, 1); - assert.strictEqual(lists.body[0].userIds[0], bob.id); + assert.strictEqual(lists.body[0].userIds.length, 2); + assert.ok(lists.body[0].userIds.find((id: string) => id === bob.id)); + assert.ok(lists.body[0].userIds.find((id: string) => id === alice.id)); }); test('Follow and follower counts are properly adjusted', async () => { From c1151377960ee2838d462ad212ff2697301ff688 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 20 Apr 2023 19:07:33 +0000 Subject: [PATCH 32/90] nanka iroiro --- .../src/core/entities/UserEntityService.ts | 2 +- .../src/server/api/endpoints/i/known-as.ts | 9 ++-- packages/backend/test/e2e/move.ts | 53 +++++++++++++++++-- packages/misskey-js/src/entities.ts | 2 +- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 2c67cb772b7d..6678f9a8a9f6 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -369,7 +369,7 @@ export class UserEntityService implements OnModuleInit { ...(opts.detail ? { url: profile!.url, uri: user.uri, - movedToUri: user.movedToUri ? await this.apPersonService.resolvePerson(user.movedToUri) : null, + movedToUri: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.uri).catch(() => null) : null, alsoKnownAs: user.alsoKnownAs, createdAt: user.createdAt.toISOString(), updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, diff --git a/packages/backend/src/server/api/endpoints/i/known-as.ts b/packages/backend/src/server/api/endpoints/i/known-as.ts index dc96e5e2eb64..d63e4a9716ee 100644 --- a/packages/backend/src/server/api/endpoints/i/known-as.ts +++ b/packages/backend/src/server/api/endpoints/i/known-as.ts @@ -32,6 +32,11 @@ export const meta = { 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; @@ -51,9 +56,6 @@ export default class extends Endpoint { 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; @@ -71,6 +73,7 @@ export default class extends Endpoint { 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); diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index 7330d50ef345..ee478dea0cd0 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -54,6 +54,49 @@ describe('Account Move', () => { await Users.update(bob.id, { alsoKnownAs: null }); }, 1000 * 10); + test('Able to create an alias', async () => { + await api('/i/known-as', { + alsoKnownAs: `@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}`); + }); + + test('Able to set remote user (but may fail)', async () => { + const res = await api('/i/known-as', { + alsoKnownAs: `@syuilo@example.com`, + }, bob); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'NO_SUCH_USER'); + 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}`, + }, 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}`); + }); + + test('Unable to add itself', async () => { + const res = await api('/i/known-as', { + alsoKnownAs: `@bob@${url.hostname}`, + }, bob); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'FORBIDDEN_TO_SET_YOURSELF'); + assert.strictEqual(res.body.error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4'); + }); + test('Unable to add a nonexisting local account to alsoKnownAs', async () => { const res = await api('/i/known-as', { alsoKnownAs: `@nonexist@${url.hostname}`, @@ -72,10 +115,10 @@ describe('Account Move', () => { alsoKnownAs: `@carol@${url.hostname}`, }, bob); - const newAlice = await Users.findOneByOrFail({ id: bob.id }); - assert.strictEqual(newAlice.alsoKnownAs?.length, 2); - assert.strictEqual(newAlice.alsoKnownAs[0], `${url.origin}/users/${alice.id}`); - assert.strictEqual(newAlice.alsoKnownAs[1], `${url.origin}/users/${carol.id}`); + const newBob = await Users.findOneByOrFail({ id: bob.id }); + assert.strictEqual(newBob.alsoKnownAs?.length, 2); + assert.strictEqual(newBob.alsoKnownAs[0], `${url.origin}/users/${alice.id}`); + assert.strictEqual(newBob.alsoKnownAs[1], `${url.origin}/users/${carol.id}`); }); test('Unable to create an alias without the second @', async () => { @@ -95,7 +138,7 @@ describe('Account Move', () => { assert.strictEqual(res2.body.error.code, 'NO_SUCH_USER'); assert.strictEqual(res2.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); }); - }) + }); describe('Local to Local', () => { let antennaId = ''; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 34857f431fc6..9efc062eac54 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -15,7 +15,6 @@ export type UserLite = { avatarUrl: string; avatarBlurhash: string; alsoKnownAs: string[]; - movedToUri: any; emojis: { name: string; url: string; @@ -58,6 +57,7 @@ export type UserDetailed = UserLite & { lang: string | null; lastFetchedAt?: DateString; location: string | null; + movedToUri: string; notesCount: number; pinnedNoteIds: ID[]; pinnedNotes: Note[]; From b01a6cbbb78d38f8d6105d29055a33a2b52a9bdd Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 20 Apr 2023 19:22:24 +0000 Subject: [PATCH 33/90] fix misskey-js? --- packages/misskey-js/etc/misskey-js.api.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 2fae84c17186..67d12000b865 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2348,7 +2348,6 @@ type LiteInstanceMetadata = { imageUrl: string; }[]; translatorAvailable: boolean; - serverRules: string[]; }; // @public (undocumented) From 6b82d3a1d4fcddec29da1b4b9cfc8f60625e5359 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 20 Apr 2023 20:02:51 +0000 Subject: [PATCH 34/90] :v: --- .../src/server/api/endpoints/i/move.ts | 13 ++++------- packages/backend/test/e2e/endpoints.ts | 23 ++++++++++++++++++- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index a13e6cda41be..c6427a6246fd 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -26,11 +26,6 @@ export const meta = { }, errors: { - noSuchMoveTarget: { - message: 'No such move target.', - code: 'NO_SUCH_MOVE_TARGET', - id: 'b5c90186-4ab0-49c8-9bba-a1f76c202ba4', - }, destinationAccountForbids: { message: 'Destination account doesn\'t have proper \'Known As\' alias. Did you remember to set it?', @@ -89,25 +84,25 @@ export default class extends Endpoint { ) { 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); + if (!unfiltered) throw new ApiError(meta.errors.noSuchUser); // 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.noSuchMoveTarget); + if (!unfiltered.includes('@')) throw new ApiError(meta.errors.noSuchUser); const userAddress = unfiltered.split('@'); // retrieve the destination account let moveTo = 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.noSuchMoveTarget); + throw new ApiError(meta.errors.noSuchUser); }); const destination = await this.getterService.getUser(moveTo.id); const newUri = this.accountMoveService.getUserUri(destination); diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index c662b16f1853..86c8bfb5a4da 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -4,8 +4,9 @@ import * as assert from 'assert'; // node-fetch only supports it's own Blob yet // https://github.com/node-fetch/node-fetch/pull/1664 import { Blob } from 'node-fetch'; -import { startServer, signup, post, api, uploadFile, simpleGet } from '../utils.js'; +import { startServer, signup, post, api, uploadFile, simpleGet, initTestDb } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import { User } from '@/models/index.js'; describe('Endpoints', () => { let app: INestApplicationContext; @@ -289,6 +290,16 @@ describe('Endpoints', () => { }, bob); assert.strictEqual(res.status, 200); + + const connection = await initTestDb(false); + const Users = connection.getRepository(User); + const newBob = await Users.findOneByOrFail({ id: bob.id }); + assert.strictEqual(newBob.followersCount, 0); + assert.strictEqual(newBob.followingCount, 1); + const newAlice = await Users.findOneByOrFail({ id: alice.id }); + assert.strictEqual(newAlice.followersCount, 1); + assert.strictEqual(newAlice.followingCount, 0); + connection.destroy(); }); test('既にフォローしている場合は怒る', async () => { @@ -341,6 +352,16 @@ describe('Endpoints', () => { }, bob); assert.strictEqual(res.status, 200); + + const connection = await initTestDb(false); + const Users = connection.getRepository(User); + const newBob = await Users.findOneByOrFail({ id: bob.id }); + assert.strictEqual(newBob.followersCount, 0); + assert.strictEqual(newBob.followingCount, 0); + const newAlice = await Users.findOneByOrFail({ id: alice.id }); + assert.strictEqual(newAlice.followersCount, 0); + assert.strictEqual(newAlice.followingCount, 0); + connection.destroy(); }); test('フォローしていない場合は怒る', async () => { From 51a867473bfa9a6e2cf61a8be5f5bb7a435080e5 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 20 Apr 2023 20:50:19 +0000 Subject: [PATCH 35/90] =?UTF-8?q?=E7=A7=BB=E8=A1=8C=E3=82=92=E8=A1=8C?= =?UTF-8?q?=E3=81=A3=E3=81=9F=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=8B=E3=82=89=E3=81=AE=E3=83=95=E3=82=A9=E3=83=AD=E3=83=BC?= =?UTF-8?q?=E3=83=AA=E3=82=AF=E3=82=A8=E3=82=B9=E3=83=88=E3=81=AE=E8=87=AA?= =?UTF-8?q?=E5=8B=95=E8=A8=B1=E5=8F=AF=E3=82=92=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/core/UserFollowingService.ts | 26 +++++++++++++++---- .../activitypub/models/ApPersonService.ts | 6 ++--- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 2f52d91b4d21..8b5d9fdb1bd3 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -143,16 +143,32 @@ export class UserFollowingService implements OnModuleInit { // Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account. if (followee.isLocked && !autoAccept) { let movedFollower = follower; + if (this.userEntityService.isRemoteUser(movedFollower)) { - await this.apPersonService.updatePerson(movedFollower.uri); - movedFollower = await this.apPersonService.resolvePerson(movedFollower.uri); + if ((new Date()).getTime() - (movedFollower.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { + await this.apPersonService.updatePerson(movedFollower.uri); + } + movedFollower = await this.apPersonService.fetchPerson(movedFollower.uri) ?? follower; } + if (movedFollower.alsoKnownAs) { for (const oldUri of movedFollower.alsoKnownAs) { try { - await this.apPersonService.updatePerson(oldUri); - const oldAccount = await this.apPersonService.resolvePerson(oldUri); - const newUri = this.userEntityService.isRemoteUser(movedFollower) ? movedFollower.uri : `${this.config.url}/users/${movedFollower.id}`; + let oldAccount = await this.apPersonService.fetchPerson(oldUri); + if (!oldAccount) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー + + let newUri: string; + + if (this.userEntityService.isRemoteUser(movedFollower)) { + if ((new Date()).getTime() - (oldAccount.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { + await this.apPersonService.updatePerson(oldUri); + } + + oldAccount = await this.apPersonService.fetchPerson(oldUri) ?? oldAccount; + newUri = movedFollower.uri; + } else { + newUri = `${this.config.url}/users/${movedFollower.id}`; + } autoAccept = oldAccount.movedToUri === newUri && await this.followingsRepository.exist({ where: { diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 7e5a03b10395..515fafb2a02b 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -205,12 +205,12 @@ export class ApPersonService implements OnModuleInit { } /** - * Personをフェッチします。 + * uriからUser(Person)をフェッチします。 * - * Misskeyに対象のPersonが登録されていればそれを返します。 + * Misskeyに対象のPersonが登録されていればそれを返し、登録がなければnullを返します。 */ @bindThis - public async fetchPerson(uri: string, resolver?: Resolver): Promise { + public async fetchPerson(uri: string): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); const cached = this.cacheService.uriPersonCache.get(uri); From 0995c36c1ac240fd48f1aa371a561307d091c292 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 20 Apr 2023 20:55:00 +0000 Subject: [PATCH 36/90] =?UTF-8?q?newUri=E3=82=92=E5=A4=96=E3=81=AB?= =?UTF-8?q?=E5=87=BA=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/UserFollowingService.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 8b5d9fdb1bd3..5e966f814ee8 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -151,13 +151,14 @@ export class UserFollowingService implements OnModuleInit { movedFollower = await this.apPersonService.fetchPerson(movedFollower.uri) ?? follower; } + const newUri = this.userEntityService.isLocalUser(movedFollower) ? movedFollower.uri : `${this.config.url}/users/${movedFollower.id}`; + if (movedFollower.alsoKnownAs) { for (const oldUri of movedFollower.alsoKnownAs) { try { let oldAccount = await this.apPersonService.fetchPerson(oldUri); if (!oldAccount) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー - let newUri: string; if (this.userEntityService.isRemoteUser(movedFollower)) { if ((new Date()).getTime() - (oldAccount.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { @@ -165,9 +166,6 @@ export class UserFollowingService implements OnModuleInit { } oldAccount = await this.apPersonService.fetchPerson(oldUri) ?? oldAccount; - newUri = movedFollower.uri; - } else { - newUri = `${this.config.url}/users/${movedFollower.id}`; } autoAccept = oldAccount.movedToUri === newUri && await this.followingsRepository.exist({ From 6b75bd33b94e94cb40f468eaf71c561866111d22 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 20 Apr 2023 20:55:41 +0000 Subject: [PATCH 37/90] =?UTF-8?q?newUri=E3=82=92=E5=A4=96=E3=81=AB?= =?UTF-8?q?=E5=87=BA=E3=81=992?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/UserFollowingService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 5e966f814ee8..78f5d783919f 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -151,9 +151,9 @@ export class UserFollowingService implements OnModuleInit { movedFollower = await this.apPersonService.fetchPerson(movedFollower.uri) ?? follower; } - const newUri = this.userEntityService.isLocalUser(movedFollower) ? movedFollower.uri : `${this.config.url}/users/${movedFollower.id}`; - if (movedFollower.alsoKnownAs) { + const newUri = this.userEntityService.isLocalUser(movedFollower) ? movedFollower.uri : `${this.config.url}/users/${movedFollower.id}`; + for (const oldUri of movedFollower.alsoKnownAs) { try { let oldAccount = await this.apPersonService.fetchPerson(oldUri); From 0f3491666a543fed533ddcde07045e9b144bfe2d Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 20 Apr 2023 20:55:49 +0000 Subject: [PATCH 38/90] clean up --- packages/backend/src/core/UserFollowingService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 78f5d783919f..381d464d5153 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -159,7 +159,6 @@ export class UserFollowingService implements OnModuleInit { let oldAccount = await this.apPersonService.fetchPerson(oldUri); if (!oldAccount) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー - if (this.userEntityService.isRemoteUser(movedFollower)) { if ((new Date()).getTime() - (oldAccount.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { await this.apPersonService.updatePerson(oldUri); From 0d78cacffc292b5ef41529d551a42308c5caee52 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Thu, 20 Apr 2023 19:13:35 -0400 Subject: [PATCH 39/90] fix newUri --- packages/backend/src/core/UserFollowingService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 381d464d5153..4f6f241b3d0d 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -152,8 +152,8 @@ export class UserFollowingService implements OnModuleInit { } if (movedFollower.alsoKnownAs) { - const newUri = this.userEntityService.isLocalUser(movedFollower) ? movedFollower.uri : `${this.config.url}/users/${movedFollower.id}`; - + const newUri = this.userEntityService.isRemoteUser(movedFollower) ? movedFollower.uri : `${this.config.url}/users/${movedFollower.id}`; + for (const oldUri of movedFollower.alsoKnownAs) { try { let oldAccount = await this.apPersonService.fetchPerson(oldUri); From 2a0efffa3e5bfc1586b3a38b4fab61b3ad70686b Mon Sep 17 00:00:00 2001 From: Namekuji Date: Thu, 20 Apr 2023 19:16:02 -0400 Subject: [PATCH 40/90] prevent moving if the destination account has already moved --- .../backend/src/core/AccountMoveService.ts | 4 +- .../src/core/activitypub/ApInboxService.ts | 3 ++ .../src/server/api/endpoints/i/move.ts | 4 +- packages/backend/test/e2e/move.ts | 46 ++++++++++++------- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 4db0f928e52a..8be7b04b678a 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -78,7 +78,7 @@ export class AccountMoveService { // add movedToUri to indicate that the user has moved const update = {} as Partial; - update.alsoKnownAs = src.alsoKnownAs?.concat([dstUri]) ?? [dstUri]; + update.alsoKnownAs = src.alsoKnownAs?.includes(dstUri) ? src.alsoKnownAs : src.alsoKnownAs?.concat([dstUri]) ?? [dstUri]; update.movedToUri = dstUri; await this.usersRepository.update(src.id, update); src = Object.assign(src, update); @@ -226,7 +226,7 @@ export class AccountMoveService { } while (newJoinings.has(id)); return id; }; - for (const joining of oldJoinings) { + for (const joining of oldJoinings) { newJoinings.set(genId(), { createdAt: new Date(), userId: dst.id, diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 9df6ffad7cc7..2da290bfea1d 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -760,6 +760,9 @@ export class ApInboxService { } else if (!newAccount.alsoKnownAs?.includes(this.accountMoveService.getUserUri(oldAccount))) { isValidMove = false; } + if (newAccount.movedToUri) { + isValidMove = false; + } if (!isValidMove) { return 'skip: destination account invalid'; } diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index c6427a6246fd..256fca2cf0c5 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -28,7 +28,7 @@ export const meta = { errors: { destinationAccountForbids: { message: - 'Destination account doesn\'t have proper \'Known As\' alias. Did you remember to set it?', + 'Destination account doesn\'t have proper \'Known As\' alias, or has already moved.', code: 'REMOTE_ACCOUNT_FORBIDS', id: 'b5c90186-4ab0-49c8-9bba-a1f766282ba4', }, @@ -125,7 +125,7 @@ export default class extends Endpoint { } // abort if unintended - if (!allowed) throw new ApiError(meta.errors.destinationAccountForbids); + if (!allowed || moveTo.movedToUri) throw new ApiError(meta.errors.destinationAccountForbids); return await this.accountMoveService.moveFromLocal(me, moveTo); }); diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index ee478dea0cd0..f06381947517 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -1,13 +1,12 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, startServer, initTestDb, api, sleep } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import rndstr from 'rndstr'; import { loadConfig } from '@/config.js'; -import { Blocking, BlockingsRepository, Following, FollowingsRepository, Muting, MutingsRepository, User, UsersRepository } from '@/models/index.js'; +import { User, UsersRepository } from '@/models/index.js'; import { jobQueue } from '@/boot/common.js'; -import rndstr from 'rndstr'; -import { uploadFile } from '../utils.js'; +import { uploadFile, signup, startServer, initTestDb, api, sleep } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('Account Move', () => { let app: INestApplicationContext; @@ -22,9 +21,6 @@ describe('Account Move', () => { let frank: any; let Users: UsersRepository; - let Followings: FollowingsRepository; - let Blockings: BlockingsRepository; - let Mutings: MutingsRepository; beforeAll(async () => { app = await startServer(); @@ -40,9 +36,6 @@ describe('Account Move', () => { eve = await signup({ username: 'eve' }); frank = await signup({ username: 'frank' }); Users = connection.getRepository(User); - Followings = connection.getRepository(Following); - Blockings = connection.getRepository(Blocking); - Mutings = connection.getRepository(Muting); }, 1000 * 60 * 2); afterAll(async () => { @@ -66,7 +59,7 @@ describe('Account Move', () => { test('Able to set remote user (but may fail)', async () => { const res = await api('/i/known-as', { - alsoKnownAs: `@syuilo@example.com`, + alsoKnownAs: '@syuilo@example.com', }, bob); assert.strictEqual(res.status, 400); @@ -123,7 +116,7 @@ describe('Account Move', () => { test('Unable to create an alias without the second @', async () => { const res1 = await api('/i/known-as', { - alsoKnownAs: '@alice' + alsoKnownAs: '@alice', }, bob); assert.strictEqual(res1.status, 400); @@ -131,7 +124,7 @@ describe('Account Move', () => { assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); const res2 = await api('/i/known-as', { - alsoKnownAs: 'alice' + alsoKnownAs: 'alice', }, bob); assert.strictEqual(res2.status, 400); @@ -209,7 +202,7 @@ describe('Account Move', () => { test('Prohibit the root account from moving', async () => { const res = await api('/i/move', { - moveToAccount: `@bob@${url.hostname}` + moveToAccount: `@bob@${url.hostname}`, }, root); assert.strictEqual(res.status, 400); @@ -223,8 +216,8 @@ describe('Account Move', () => { }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.code, 'NO_SUCH_MOVE_TARGET'); - assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f76c202ba4'); + assert.strictEqual(res.body.error.code, 'NO_SUCH_USER'); + assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); }); test('Unable to move if alsoKnownAs is invalid', async () => { @@ -272,6 +265,25 @@ describe('Account Move', () => { assert.ok(lists.body[0].userIds.find((id: string) => id === alice.id)); }); + test('Unable to move if the destination account has already moved.', async () => { + await api('/i/move', { + moveToAccount: `@bob@${url.hostname}`, + }, alice); + + const newAlice = await Users.findOneByOrFail({ id: alice.id }); + assert.strictEqual(newAlice.movedToUri, `${url.origin}/users/${bob.id}`); + assert.strictEqual(newAlice.alsoKnownAs?.length, 1); + assert.strictEqual(newAlice.alsoKnownAs[0], `${url.origin}/users/${bob.id}`); + + const res = await api('/i/move', { + moveToAccount: `@alice@${url.hostname}`, + }, bob); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'REMOTE_ACCOUNT_FORBIDS'); + assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); + }); + test('Follow and follower counts are properly adjusted', async () => { await api('/following/create', { userId: alice.id, From 55f9112eed264c38b933055d64c045e464479004 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Thu, 20 Apr 2023 22:03:18 -0400 Subject: [PATCH 41/90] 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 | 54 +++++++----- packages/misskey-js/etc/misskey-js.api.md | 8 +- packages/misskey-js/src/api.types.ts | 2 +- 9 files changed, 144 insertions(+), 180 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 8be7b04b678a..6c04154a3041 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 e4e594ec5486..6dc1313e59aa 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 e7051abc2612..acd7f7ec3e67 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 d63e4a9716ee..000000000000 --- 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 97699f3bef4d..e59b759b582d 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 f06381947517..aa5c932f10be 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 612391b00bab..2260ee0f631e 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 }}