summaryrefslogtreecommitdiff
path: root/packages/backend/src/core
diff options
context:
space:
mode:
authorNamekuji <11836635+nmkj-io@users.noreply.github.com>2023-04-08 01:16:26 -0400
committerGitHub <noreply@github.com>2023-04-08 14:16:26 +0900
commit25ebb73756017174c3197376b7485e5e7fcb5fd1 (patch)
tree9f313b388dd38db54c66c66554b10255450832ba /packages/backend/src/core
parentenhance(backend): Redisにチャンネル投稿がない場合はDBから持... (diff)
downloadmisskey-25ebb73756017174c3197376b7485e5e7fcb5fd1.tar.gz
misskey-25ebb73756017174c3197376b7485e5e7fcb5fd1.tar.bz2
misskey-25ebb73756017174c3197376b7485e5e7fcb5fd1.zip
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
Diffstat (limited to 'packages/backend/src/core')
-rw-r--r--packages/backend/src/core/AccountMoveService.ts114
-rw-r--r--packages/backend/src/core/AccountUpdateService.ts2
-rw-r--r--packages/backend/src/core/CoreModule.ts6
-rw-r--r--packages/backend/src/core/activitypub/ApInboxService.ts155
-rw-r--r--packages/backend/src/core/activitypub/ApRendererService.ts26
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts4
-rw-r--r--packages/backend/src/core/activitypub/type.ts8
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts9
8 files changed, 269 insertions, 55 deletions
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<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;
+ }
+}
diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts
index d8ba7b169d..b146fc66be 100644
--- a/packages/backend/src/core/AccountUpdateService.ts
+++ b/packages/backend/src/core/AccountUpdateService.ts
@@ -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));
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index ea6e229610..8775536e4a 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -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,
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index 055bffe731..40d7ba551d 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -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';
+ }
}
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index b250b796d6..0b22aa9bcf 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -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()
@@ -293,6 +293,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[]) => {
if (!ids || ids.length === 0) return [];
@@ -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;
}
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 664e7eb4ea..21797cfcb7 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -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>;
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index 8851946330..625135da6c 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -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';
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index cbe94451cc..e02f7535d4 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -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,