copy block and mute then create follow and unfollow jobs
This commit is contained in:
parent
38fdc73d01
commit
8c031e9f42
|
@ -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 { User } from '@/models/entities/User.js';
|
||||
import type { FollowingsRepository, UsersRepository } from '@/models/index.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 { 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<unknown> {
|
||||
// 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<unknown> {
|
||||
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<true, true>(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<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);
|
||||
|
||||
// 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
|
||||
|
||||
// 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}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
this.usersRepository.update(oldAccount.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 */
|
||||
}
|
||||
}
|
||||
// Move!
|
||||
await this.accountMoveService.move(oldAccount, newAccount);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
|
|
@ -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<typeof meta, typeof paramDef> {
|
|||
// 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];
|
||||
}
|
||||
|
|
|
@ -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<typeof meta, typeof paramDef> {
|
|||
// 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<typeof meta, typeof paramDef> {
|
|||
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);
|
||||
moveTo = await this.apPersonService.resolvePerson(moveTo.uri);
|
||||
|
||||
let allowed = false;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue