From 25ebb73756017174c3197376b7485e5e7fcb5fd1 Mon Sep 17 00:00:00 2001 From: Namekuji <11836635+nmkj-io@users.noreply.github.com> Date: Sat, 8 Apr 2023 01:16:26 -0400 Subject: feat: account migration (#10507) * add Move activity * add endpoint to move from local to remote * follow move activity coming to inbox * fix move endpoint * add known-as endpoint to create account alias * add migration page * add route to migration page * add move and known-as endpoints * fix dependnecies error * fix new endpoints * fix move activity id * fix refollow * add movedToUri and alsoKnownAs to api * fix moveToUri indicator * fix missing context * add chengelog * rename MkMoved to MkAccountMoved * add missing semicolon * fix targetUri * fix followings query * remove redundant null check --- packages/backend/src/core/AccountMoveService.ts | 114 ++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 packages/backend/src/core/AccountMoveService.ts (limited to 'packages/backend/src/core/AccountMoveService.ts') diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts new file mode 100644 index 0000000000..6a7727c57a --- /dev/null +++ b/packages/backend/src/core/AccountMoveService.ts @@ -0,0 +1,114 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull } from 'typeorm'; + +import { bindThis } from '@/decorators.js'; +import { DI } from '@/di-symbols.js'; +import type { LocalUser } from '@/models/entities/User.js'; +import { User } from '@/models/entities/User.js'; +import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; + +import { GlobalEventService } from '@/core/GlobalEventService.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'; + +@Injectable() +export class AccountMoveService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + private apRendererService: ApRendererService, + private apDeliverManagerService: ApDeliverManagerService, + private globalEventService: GlobalEventService, + private userFollowingService: UserFollowingService, + private accountUpdateService: AccountUpdateService, + private relayService: RelayService, + ) { + } + + /** + * Move a local account to a remote 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'); + if (!dst.uri) throw new Error('destination uri is empty'); + + // 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; + await this.usersRepository.update(src.id, update); + + const srcPerson = await this.apRendererService.renderPerson(src); + const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src)); + await this.apDeliverManagerService.deliverToFollowers(src, updateAct); + this.relayService.deliverToRelays(src, updateAct); + + // Deliver Move activity to the followers of the old account + const moveAct = this.apRendererService.addContext(this.apRendererService.renderMove(src, dst)); + await this.apDeliverManagerService.deliverToFollowers(src, moveAct); + + // Publish meUpdated event + 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 + } + }); + followings.forEach(async (following) => { + if (!following.follower) return; + try { + await this.userFollowingService.follow(following.follower, dst); + await this.userFollowingService.unfollow(following.follower, src); + } catch { + /* empty */ + } + }); + + 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); + + // 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; + } +} -- cgit v1.2.3-freya