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
This commit is contained in:
114
packages/backend/src/core/AccountMoveService.ts
Normal file
114
packages/backend/src/core/AccountMoveService.ts
Normal file
@@ -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<unknown> {
|
||||
// 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<User>;
|
||||
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<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
|
||||
}
|
||||
});
|
||||
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<User>): Promise<unknown> {
|
||||
await this.usersRepository.update(me.id, updates);
|
||||
|
||||
// Publish meUpdated event
|
||||
const iObj = await this.userEntityService.pack<true, true>(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;
|
||||
}
|
||||
}
|
@@ -29,7 +29,7 @@ export class AccountUpdateService {
|
||||
public async publishToFollowers(userId: User['id']) {
|
||||
const user = await this.usersRepository.findOneBy({ id: userId });
|
||||
if (user == null) throw new Error('user not found');
|
||||
|
||||
|
||||
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user));
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AccountMoveService } from './AccountMoveService.js';
|
||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||
import { AiService } from './AiService.js';
|
||||
import { AntennaService } from './AntennaService.js';
|
||||
@@ -119,6 +120,7 @@ import type { Provider } from '@nestjs/common';
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
|
||||
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
|
||||
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
|
||||
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
|
||||
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
|
||||
@@ -242,6 +244,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
],
|
||||
providers: [
|
||||
LoggerService,
|
||||
AccountMoveService,
|
||||
AccountUpdateService,
|
||||
AiService,
|
||||
AntennaService,
|
||||
@@ -359,6 +362,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
$LoggerService,
|
||||
$AccountMoveService,
|
||||
$AccountUpdateService,
|
||||
$AiService,
|
||||
$AntennaService,
|
||||
@@ -477,6 +481,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
exports: [
|
||||
QueueModule,
|
||||
LoggerService,
|
||||
AccountMoveService,
|
||||
AccountUpdateService,
|
||||
AiService,
|
||||
AntennaService,
|
||||
@@ -593,6 +598,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
$LoggerService,
|
||||
$AccountMoveService,
|
||||
$AccountUpdateService,
|
||||
$AiService,
|
||||
$AntennaService,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { In, IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
@@ -22,7 +22,7 @@ import { QueueService } from '@/core/QueueService.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 { getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.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';
|
||||
import { ApNoteService } from './models/ApNoteService.js';
|
||||
import { ApLoggerService } from './ApLoggerService.js';
|
||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||
@@ -31,7 +31,7 @@ import { ApAudienceService } from './ApAudienceService.js';
|
||||
import { ApPersonService } from './models/ApPersonService.js';
|
||||
import { ApQuestionService } from './models/ApQuestionService.js';
|
||||
import type { Resolver } from './ApResolverService.js';
|
||||
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate } from './type.js';
|
||||
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApInboxService {
|
||||
@@ -80,7 +80,7 @@ export class ApInboxService {
|
||||
) {
|
||||
this.logger = this.apLoggerService.logger;
|
||||
}
|
||||
|
||||
|
||||
@bindThis
|
||||
public async performActivity(actor: RemoteUser, activity: IObject) {
|
||||
if (isCollectionOrOrderedCollection(activity)) {
|
||||
@@ -139,6 +139,8 @@ export class ApInboxService {
|
||||
await this.block(actor, activity);
|
||||
} else if (isFlag(activity)) {
|
||||
await this.flag(actor, activity);
|
||||
} else if (isMove(activity)) {
|
||||
await this.move(actor, activity);
|
||||
} else {
|
||||
this.logger.warn(`unrecognized activity type: ${activity.type}`);
|
||||
}
|
||||
@@ -147,15 +149,15 @@ export class ApInboxService {
|
||||
@bindThis
|
||||
private async follow(actor: RemoteUser, activity: IFollow): Promise<string> {
|
||||
const followee = await this.apDbResolverService.getUserFromApId(activity.object);
|
||||
|
||||
|
||||
if (followee == null) {
|
||||
return 'skip: followee not found';
|
||||
}
|
||||
|
||||
|
||||
if (followee.host != null) {
|
||||
return 'skip: フォローしようとしているユーザーはローカルユーザーではありません';
|
||||
}
|
||||
|
||||
|
||||
await this.userFollowingService.follow(actor, followee, activity.id);
|
||||
return 'ok';
|
||||
}
|
||||
@@ -183,16 +185,16 @@ export class ApInboxService {
|
||||
const uri = activity.id ?? activity;
|
||||
|
||||
this.logger.info(`Accept: ${uri}`);
|
||||
|
||||
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
|
||||
|
||||
const object = await resolver.resolve(activity.object).catch(err => {
|
||||
this.logger.error(`Resolution failed: ${err}`);
|
||||
throw err;
|
||||
});
|
||||
|
||||
|
||||
if (isFollow(object)) return await this.acceptFollow(actor, object);
|
||||
|
||||
|
||||
return `skip: Unknown Accept type: ${getApType(object)}`;
|
||||
}
|
||||
|
||||
@@ -225,18 +227,18 @@ export class ApInboxService {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
|
||||
|
||||
if (activity.target == null) {
|
||||
throw new Error('target is null');
|
||||
}
|
||||
|
||||
|
||||
if (activity.target === actor.featured) {
|
||||
const note = await this.apNoteService.resolveNote(activity.object);
|
||||
if (note == null) throw new Error('note not found');
|
||||
await this.notePiningService.addPinned(actor, note.id);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
throw new Error(`unknown target: ${activity.target}`);
|
||||
}
|
||||
|
||||
@@ -405,10 +407,10 @@ export class ApInboxService {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
|
||||
|
||||
// 削除対象objectのtype
|
||||
let formerType: string | undefined;
|
||||
|
||||
|
||||
if (typeof activity.object === 'string') {
|
||||
// typeが不明だけど、どうせ消えてるのでremote resolveしない
|
||||
formerType = undefined;
|
||||
@@ -420,19 +422,19 @@ export class ApInboxService {
|
||||
formerType = toSingle(object.type);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const uri = getApId(activity.object);
|
||||
|
||||
|
||||
// type不明でもactorとobjectが同じならばそれはPersonに違いない
|
||||
if (!formerType && actor.uri === uri) {
|
||||
formerType = 'Person';
|
||||
}
|
||||
|
||||
|
||||
// それでもなかったらおそらくNote
|
||||
if (!formerType) {
|
||||
formerType = 'Note';
|
||||
}
|
||||
|
||||
|
||||
if (validPost.includes(formerType)) {
|
||||
return await this.deleteNote(actor, uri);
|
||||
} else if (validActor.includes(formerType)) {
|
||||
@@ -445,44 +447,44 @@ export class ApInboxService {
|
||||
@bindThis
|
||||
private async deleteActor(actor: RemoteUser, uri: string): Promise<string> {
|
||||
this.logger.info(`Deleting the Actor: ${uri}`);
|
||||
|
||||
|
||||
if (actor.uri !== uri) {
|
||||
return `skip: delete actor ${actor.uri} !== ${uri}`;
|
||||
}
|
||||
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: actor.id });
|
||||
if (user == null) {
|
||||
return 'skip: actor not found';
|
||||
} else if (user.isDeleted) {
|
||||
return 'skip: already deleted';
|
||||
}
|
||||
|
||||
|
||||
const job = await this.queueService.createDeleteAccountJob(actor);
|
||||
|
||||
|
||||
await this.usersRepository.update(actor.id, {
|
||||
isDeleted: true,
|
||||
});
|
||||
|
||||
|
||||
return `ok: queued ${job.name} ${job.id}`;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async deleteNote(actor: RemoteUser, uri: string): Promise<string> {
|
||||
this.logger.info(`Deleting the Note: ${uri}`);
|
||||
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
|
||||
|
||||
try {
|
||||
const note = await this.apDbResolverService.getNoteFromApId(uri);
|
||||
|
||||
|
||||
if (note == null) {
|
||||
return 'message not found';
|
||||
}
|
||||
|
||||
|
||||
if (note.userId !== actor.id) {
|
||||
return '投稿を削除しようとしているユーザーは投稿の作成者ではありません';
|
||||
}
|
||||
|
||||
|
||||
await this.noteDeleteService.delete(actor, note);
|
||||
return 'ok: note deleted';
|
||||
} finally {
|
||||
@@ -536,23 +538,23 @@ export class ApInboxService {
|
||||
@bindThis
|
||||
private async rejectFollow(actor: RemoteUser, activity: IFollow): Promise<string> {
|
||||
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
|
||||
|
||||
|
||||
const follower = await this.apDbResolverService.getUserFromApId(activity.actor);
|
||||
|
||||
|
||||
if (follower == null) {
|
||||
return 'skip: follower not found';
|
||||
}
|
||||
|
||||
|
||||
if (!this.userEntityService.isLocalUser(follower)) {
|
||||
return 'skip: follower is not a local user';
|
||||
}
|
||||
|
||||
|
||||
// relay
|
||||
const match = activity.id?.match(/follow-relay\/(\w+)/);
|
||||
if (match) {
|
||||
return await this.relayService.relayRejected(match[1]);
|
||||
}
|
||||
|
||||
|
||||
await this.userFollowingService.remoteReject(actor, follower);
|
||||
return 'ok';
|
||||
}
|
||||
@@ -562,18 +564,18 @@ export class ApInboxService {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
|
||||
|
||||
if (activity.target == null) {
|
||||
throw new Error('target is null');
|
||||
}
|
||||
|
||||
|
||||
if (activity.target === actor.featured) {
|
||||
const note = await this.apNoteService.resolveNote(activity.object);
|
||||
if (note == null) throw new Error('note not found');
|
||||
await this.notePiningService.removePinned(actor, note.id);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
throw new Error(`unknown target: ${activity.target}`);
|
||||
}
|
||||
|
||||
@@ -582,24 +584,24 @@ export class ApInboxService {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
|
||||
|
||||
const uri = activity.id ?? activity;
|
||||
|
||||
|
||||
this.logger.info(`Undo: ${uri}`);
|
||||
|
||||
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
|
||||
|
||||
const object = await resolver.resolve(activity.object).catch(e => {
|
||||
this.logger.error(`Resolution failed: ${e}`);
|
||||
throw e;
|
||||
});
|
||||
|
||||
|
||||
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);
|
||||
if (isAnnounce(object)) return await this.undoAnnounce(actor, object);
|
||||
if (isAccept(object)) return await this.undoAccept(actor, object);
|
||||
|
||||
|
||||
return `skip: unknown object type ${getApType(object)}`;
|
||||
}
|
||||
|
||||
@@ -609,17 +611,17 @@ export class ApInboxService {
|
||||
if (follower == null) {
|
||||
return 'skip: follower not found';
|
||||
}
|
||||
|
||||
|
||||
const following = await this.followingsRepository.findOneBy({
|
||||
followerId: follower.id,
|
||||
followeeId: actor.id,
|
||||
});
|
||||
|
||||
|
||||
if (following) {
|
||||
await this.userFollowingService.unfollow(follower, actor);
|
||||
return 'ok: unfollowed';
|
||||
}
|
||||
|
||||
|
||||
return 'skip: フォローされていない';
|
||||
}
|
||||
|
||||
@@ -708,16 +710,16 @@ export class ApInboxService {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
return 'skip: invalid actor';
|
||||
}
|
||||
|
||||
|
||||
this.logger.debug('Update');
|
||||
|
||||
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
|
||||
|
||||
const object = await resolver.resolve(activity.object).catch(e => {
|
||||
this.logger.error(`Resolution failed: ${e}`);
|
||||
throw e;
|
||||
});
|
||||
|
||||
|
||||
if (isActor(object)) {
|
||||
await this.apPersonService.updatePerson(actor.uri!, resolver, object);
|
||||
return 'ok: Person updated';
|
||||
@@ -728,4 +730,55 @@ export class ApInboxService {
|
||||
return `skip: Unknown type: ${getApType(object)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async move(actor: RemoteUser, activity: IMove): Promise<string> {
|
||||
// fetch the new and old accounts
|
||||
const targetUri = getApHrefNullable(activity.target);
|
||||
if (!targetUri) return 'skip: invalid activity target';
|
||||
const new_acc = await this.apPersonService.resolvePerson(targetUri);
|
||||
const old_acc = 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);
|
||||
|
||||
// check if alsoKnownAs of the new account is valid
|
||||
let isValidMove = true;
|
||||
if (old_acc.uri) {
|
||||
if (!new_acc.alsoKnownAs?.includes(old_acc.uri)) {
|
||||
isValidMove = false;
|
||||
}
|
||||
} else if (!new_acc.alsoKnownAs?.includes(old_acc.id)) {
|
||||
isValidMove = false;
|
||||
}
|
||||
if (!isValidMove) {
|
||||
return 'skip: accounts 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
|
||||
}
|
||||
});
|
||||
followings.forEach(async (following) => {
|
||||
if (!following.follower) return;
|
||||
try {
|
||||
await this.userFollowingService.follow(following.follower, new_acc);
|
||||
await this.userFollowingService.unfollow(following.follower, old_acc);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
});
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { LdSignatureService } from './LdSignatureService.js';
|
||||
import { ApMfmService } from './ApMfmService.js';
|
||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
||||
import type { IIdentifier } from './models/identifier.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -292,6 +292,22 @@ export class ApRendererService {
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderMove(
|
||||
src: { id: User['id']; host: User['host']; uri: User['host'] },
|
||||
dst: { id: User['id']; host: User['host']; uri: User['host'] },
|
||||
): IMove {
|
||||
const actor = this.userEntityService.isLocalUser(src) ? `${this.config.url}/users/${src.id}` : src.uri!;
|
||||
const target = this.userEntityService.isLocalUser(dst) ? `${this.config.url}/users/${dst.id}` : dst.uri!;
|
||||
return {
|
||||
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
|
||||
actor,
|
||||
type: 'Move',
|
||||
object: actor,
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async renderNote(note: Note, dive = true): Promise<IPost> {
|
||||
const getPromisedFiles = async (ids: string[]) => {
|
||||
@@ -498,6 +514,14 @@ export class ApRendererService {
|
||||
attachment: attachment.length ? attachment : undefined,
|
||||
} as any;
|
||||
|
||||
if (user.movedToUri) {
|
||||
person.movedTo = user.movedToUri;
|
||||
}
|
||||
|
||||
if (user.alsoKnownAs) {
|
||||
person.alsoKnownAs = user.alsoKnownAs;
|
||||
}
|
||||
|
||||
if (profile.birthday) {
|
||||
person['vcard:bday'] = profile.birthday;
|
||||
}
|
||||
|
@@ -281,6 +281,8 @@ export class ApPersonService implements OnModuleInit {
|
||||
lastFetchedAt: new Date(),
|
||||
name: truncate(person.name, nameLength),
|
||||
isLocked: !!person.manuallyApprovesFollowers,
|
||||
movedToUri: person.movedTo,
|
||||
alsoKnownAs: person.alsoKnownAs,
|
||||
isExplorable: !!person.discoverable,
|
||||
username: person.preferredUsername,
|
||||
usernameLower: person.preferredUsername!.toLowerCase(),
|
||||
@@ -473,6 +475,8 @@ export class ApPersonService implements OnModuleInit {
|
||||
isBot: getApType(object) === 'Service',
|
||||
isCat: (person as any).isCat === true,
|
||||
isLocked: !!person.manuallyApprovesFollowers,
|
||||
movedToUri: person.movedTo ?? null,
|
||||
alsoKnownAs: person.alsoKnownAs ?? null,
|
||||
isExplorable: !!person.discoverable,
|
||||
} as Partial<User>;
|
||||
|
||||
|
@@ -157,6 +157,8 @@ export interface IActor extends IObject {
|
||||
name?: string;
|
||||
preferredUsername?: string;
|
||||
manuallyApprovesFollowers?: boolean;
|
||||
movedTo?: string;
|
||||
alsoKnownAs?: string[];
|
||||
discoverable?: boolean;
|
||||
inbox: string;
|
||||
sharedInbox?: string; // 後方互換性のため
|
||||
@@ -300,6 +302,11 @@ export interface IFlag extends IActivity {
|
||||
type: 'Flag';
|
||||
}
|
||||
|
||||
export interface IMove extends IActivity {
|
||||
type: 'Move';
|
||||
target: IObject | string;
|
||||
}
|
||||
|
||||
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
|
||||
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
|
||||
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
|
||||
@@ -314,3 +321,4 @@ export const isLike = (object: IObject): object is ILike => getApType(object) ==
|
||||
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
||||
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
||||
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
||||
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
|
||||
|
@@ -15,6 +15,7 @@ import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema,
|
||||
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
import type { AntennaService } from '../AntennaService.js';
|
||||
@@ -25,7 +26,7 @@ import type { PageEntityService } from './PageEntityService.js';
|
||||
|
||||
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
|
||||
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
|
||||
Detailed extends true ?
|
||||
Detailed extends true ?
|
||||
ExpectsMe extends true ? Packed<'MeDetailed'> :
|
||||
ExpectsMe extends false ? Packed<'UserDetailedNotMe'> :
|
||||
Packed<'UserDetailed'> :
|
||||
@@ -47,6 +48,7 @@ function isRemoteUser(user: User | { host: User['host'] }): boolean {
|
||||
|
||||
@Injectable()
|
||||
export class UserEntityService implements OnModuleInit {
|
||||
private apPersonService: ApPersonService;
|
||||
private noteEntityService: NoteEntityService;
|
||||
private driveFileEntityService: DriveFileEntityService;
|
||||
private pageEntityService: PageEntityService;
|
||||
@@ -122,6 +124,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
this.apPersonService = this.moduleRef.get('ApPersonService');
|
||||
this.noteEntityService = this.moduleRef.get('NoteEntityService');
|
||||
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
||||
this.pageEntityService = this.moduleRef.get('PageEntityService');
|
||||
@@ -237,7 +240,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
@bindThis
|
||||
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
||||
|
||||
|
||||
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
||||
`notificationTimeline:${userId}`,
|
||||
'+',
|
||||
@@ -363,6 +366,8 @@ export class UserEntityService implements OnModuleInit {
|
||||
...(opts.detail ? {
|
||||
url: profile!.url,
|
||||
uri: user.uri,
|
||||
movedToUri: user.movedToUri ? await this.apPersonService.resolvePerson(user.movedToUri) : null,
|
||||
alsoKnownAs: user.alsoKnownAs,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
|
||||
|
Reference in New Issue
Block a user