mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-27 21:10:20 +01:00
copy block and mute and update lists when detecting an account has moved
This commit is contained in:
parent
8c031e9f42
commit
91fcad0c85
4 changed files with 101 additions and 46 deletions
|
@ -5,8 +5,8 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { LocalUser } from '@/models/entities/User.js';
|
import type { LocalUser } from '@/models/entities/User.js';
|
||||||
import type { BlockingsRepository, FollowingsRepository, Muting, MutingsRepository, UsersRepository } from '@/models/index.js';
|
import type { BlockingsRepository, FollowingsRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { RelationshipJobData } from '@/queue/types.js';
|
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
|
||||||
|
|
||||||
import { User } from '@/models/entities/User.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { CacheService } from '@/core/CacheService';
|
import { CacheService } from '@/core/CacheService';
|
||||||
|
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccountMoveService {
|
export class AccountMoveService {
|
||||||
|
@ -39,6 +40,9 @@ export class AccountMoveService {
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.userListJoiningsRepository)
|
||||||
|
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
|
@ -46,6 +50,7 @@ export class AccountMoveService {
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private userFollowingService: UserFollowingService,
|
private userFollowingService: UserFollowingService,
|
||||||
private accountUpdateService: AccountUpdateService,
|
private accountUpdateService: AccountUpdateService,
|
||||||
|
private proxyAccountService: ProxyAccountService,
|
||||||
private relayService: RelayService,
|
private relayService: RelayService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
|
@ -114,43 +119,13 @@ export class AccountMoveService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async move(src: User, dst: User): Promise<void> {
|
public async move(src: User, dst: User): Promise<void> {
|
||||||
// Copy blockings:
|
// 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.
|
await this.copyBlocking(src, dst);
|
||||||
// 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:
|
// Copy mutings:
|
||||||
// Insert new mutings with the same values except mutee
|
await this.copyMutings(src, dst);
|
||||||
const mutings = await this.mutingsRepository.findBy({ muteeId: src.id });
|
|
||||||
const newMuting: Partial<Muting>[] = [];
|
// Update lists:
|
||||||
for (const muting of mutings) {
|
await this.updateLists(src, dst);
|
||||||
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
|
// follow the new account and unfollow the old one
|
||||||
const followings = await this.followingsRepository.find({
|
const followings = await this.followingsRepository.find({
|
||||||
|
@ -166,8 +141,8 @@ export class AccountMoveService {
|
||||||
const unfollowJobs: RelationshipJobData[] = [];
|
const unfollowJobs: RelationshipJobData[] = [];
|
||||||
for (const following of followings) {
|
for (const following of followings) {
|
||||||
if (!following.follower) continue;
|
if (!following.follower) continue;
|
||||||
followJobs.push({ from: following.follower, to: dst });
|
followJobs.push({ from: { id: following.follower.id }, to: { id: dst.id } });
|
||||||
unfollowJobs.push({ from: following.follower, to: src });
|
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.
|
// 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.
|
// 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);
|
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
|
@bindThis
|
||||||
public getUserUri(user: User): string {
|
public getUserUri(user: User): string {
|
||||||
return this.userEntityService.isRemoteUser(user)
|
return this.userEntityService.isRemoteUser(user)
|
||||||
|
|
|
@ -747,11 +747,11 @@ export class ApInboxService {
|
||||||
|
|
||||||
// update them if they're remote
|
// update them if they're remote
|
||||||
if (newAccount.uri) {
|
if (newAccount.uri) {
|
||||||
await this.apPersonService.updatePerson(newAccount.uri);
|
await this.apPersonService.updatePerson(newAccount.uri);
|
||||||
newAccount = await this.apPersonService.resolvePerson(newAccount.uri);
|
newAccount = await this.apPersonService.resolvePerson(newAccount.uri);
|
||||||
}
|
}
|
||||||
if (oldAccount.uri) {
|
if (oldAccount.uri) {
|
||||||
await this.apPersonService.updatePerson(oldAccount.uri);
|
await this.apPersonService.updatePerson(oldAccount.uri);
|
||||||
oldAccount = await this.apPersonService.resolvePerson(oldAccount.uri);
|
oldAccount = await this.apPersonService.resolvePerson(oldAccount.uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import promiseLimit from 'promise-limit';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { Config } from '@/config.js';
|
||||||
import type { RemoteUser } from '@/models/entities/User.js';
|
import type { RemoteUser } from '@/models/entities/User.js';
|
||||||
import { User } 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
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||||
import type { ApImageService } from './ApImageService.js';
|
import type { ApImageService } from './ApImageService.js';
|
||||||
import type { IActor, IObject } from '../type.js';
|
import type { IActor, IObject } from '../type.js';
|
||||||
|
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
|
|
||||||
const nameLength = 128;
|
const nameLength = 128;
|
||||||
const summaryLength = 2048;
|
const summaryLength = 2048;
|
||||||
|
@ -66,6 +67,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
private usersChart: UsersChart;
|
private usersChart: UsersChart;
|
||||||
private instanceChart: InstanceChart;
|
private instanceChart: InstanceChart;
|
||||||
private apLoggerService: ApLoggerService;
|
private apLoggerService: ApLoggerService;
|
||||||
|
private accountMoveService: AccountMoveService;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -131,6 +133,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
this.usersChart = this.moduleRef.get('UsersChart');
|
this.usersChart = this.moduleRef.get('UsersChart');
|
||||||
this.instanceChart = this.moduleRef.get('InstanceChart');
|
this.instanceChart = this.moduleRef.get('InstanceChart');
|
||||||
this.apLoggerService = this.moduleRef.get('ApLoggerService');
|
this.apLoggerService = this.moduleRef.get('ApLoggerService');
|
||||||
|
this.accountMoveService = this.moduleRef.get('AccountMoveService');
|
||||||
this.logger = this.apLoggerService.logger;
|
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');
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
if (uri.startsWith(this.config.url + '/')) {
|
if (uri.startsWith(`${this.config.url}/`)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region このサーバーに既に登録されているか
|
//#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;
|
return;
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -523,6 +526,20 @@ export class ApPersonService implements OnModuleInit {
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
|
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);
|
throw new ApiError(meta.errors.noSuchMoveTarget);
|
||||||
});
|
});
|
||||||
const destination = await this.getterService.getUser(moveTo.id);
|
const destination = await this.getterService.getUser(moveTo.id);
|
||||||
moveTo.uri = this.accountMoveService.getUserUri(destination)
|
moveTo.uri = this.accountMoveService.getUserUri(destination);
|
||||||
|
|
||||||
// update local db
|
// update local db
|
||||||
await this.apPersonService.updatePerson(moveTo.uri);
|
await this.apPersonService.updatePerson(moveTo.uri);
|
||||||
|
|
Loading…
Reference in a new issue