copy block and mute and update lists when detecting an account has moved
This commit is contained in:
		
							parent
							
								
									8c031e9f42
								
							
						
					
					
						commit
						91fcad0c85
					
				|  | @ -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,43 +119,13 @@ export class AccountMoveService { | |||
| 	@bindThis | ||||
| 	public async move(src: User, dst: User): Promise<void> { | ||||
| 		// 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); | ||||
| 		await this.copyBlocking(src, dst); | ||||
| 
 | ||||
| 		// Copy mutings:
 | ||||
| 		// Insert new mutings with the same values except mutee
 | ||||
| 		const mutings = await this.mutingsRepository.findBy({ muteeId: src.id }); | ||||
| 		const newMuting: Partial<Muting>[] = []; | ||||
| 		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
 | ||||
| 		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({ | ||||
|  | @ -166,8 +141,8 @@ export class AccountMoveService { | |||
| 		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 }); | ||||
| 			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.
 | ||||
|  | @ -175,6 +150,69 @@ export class AccountMoveService { | |||
| 		this.queueService.createUnfollowJob(unfollowJobs); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async copyBlocking(src: ThinUser, dst: ThinUser): Promise<void> { | ||||
| 		// 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 | ||||
| 			} | ||||
| 		}); | ||||
| 		// 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 } }); | ||||
| 		} | ||||
| 		// no need to unblock the old account because it may be still functional
 | ||||
| 		this.queueService.createBlockJob(blockJobs); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async copyMutings(src: ThinUser, dst: ThinUser): Promise<void> { | ||||
| 		// Insert new mutings with the same values except mutee
 | ||||
| 		const mutings = await this.mutingsRepository.findBy({ muteeId: src.id }); | ||||
| 		const newMuting: Partial<Muting>[] = []; | ||||
| 		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
 | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async updateLists(src: ThinUser, dst: User): Promise<void> { | ||||
| 		// 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 } }]); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public getUserUri(user: User): string { | ||||
| 			return this.userEntityService.isRemoteUser(user) | ||||
|  |  | |||
|  | @ -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); | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 */ | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|  |  | |||
|  | @ -109,7 +109,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | |||
| 				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); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue