diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index eceb972a91..94fe9e97ef 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -402,7 +402,8 @@ export class NoteEntityService implements OnModuleInit { bufferedReactions: Map; pairs: ([MiUser['id'], string])[] }> | null; myReactions: Map; packedFiles: Map | null>; - packedUsers: Map> + packedUsers: Map>; + mentionHandles: Record; }; }, ): Promise> { @@ -443,6 +444,9 @@ export class NoteEntityService implements OnModuleInit { const packedFiles = options?._hint_?.packedFiles; const packedUsers = options?._hint_?.packedUsers; + // Do not await - defer until the awaitAll below + const mentionHandles = this.getUserHandles(note.mentions, options?._hint_?.mentionHandles); + const packed: Packed<'Note'> = await awaitAll({ id: note.id, createdAt: this.idService.parse(note.id).date.toISOString(), @@ -476,7 +480,8 @@ export class NoteEntityService implements OnModuleInit { allowRenoteToExternal: channel.allowRenoteToExternal, userId: channel.userId, } : undefined, - mentions: note.mentions && note.mentions.length > 0 ? note.mentions : undefined, + mentions: note.mentions.length > 0 ? note.mentions : undefined, + mentionHandles: note.mentions.length > 0 ? mentionHandles : undefined, uri: note.uri ?? undefined, url: note.url ?? undefined, poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, @@ -594,6 +599,22 @@ export class NoteEntityService implements OnModuleInit { const packedUsers = await this.userEntityService.packMany(users, me) .then(users => new Map(users.map(u => [u.id, u]))); + // Recursively add all mentioned users from all notes + replies + renotes + const allMentionedUsers = notes.reduce((users, note) => { + function add(n: MiNote) { + for (const user of n.mentions) { + users.add(user); + } + + if (n.reply) add(n.reply); + if (n.renote) add(n.renote); + } + + add(note); + return users; + }, new Set()); + const mentionHandles = await this.getUserHandles(Array.from(allMentionedUsers)); + return await Promise.all(notes.map(n => this.pack(n, me, { ...options, _hint_: { @@ -601,6 +622,7 @@ export class NoteEntityService implements OnModuleInit { myReactions: myReactionsMap, packedFiles, packedUsers, + mentionHandles, }, }))); } @@ -636,4 +658,36 @@ export class NoteEntityService implements OnModuleInit { relations: ['user'], }); } + + private async getUserHandles(userIds: string[], hint?: Record): Promise> { + if (userIds.length < 1) return {}; + + // Hint is provided by packMany to avoid N+1 queries. + // It should already include all existing mentioned users. + if (hint) { + const handles = {} as Record; + for (const id of userIds) { + handles[id] = hint[id]; + } + return handles; + } + + const users = await this.usersRepository.find({ + select: { + id: true, + username: true, + host: true, + }, + where: { + id: In(userIds), + }, + }); + + return users.reduce((map, user) => { + map[user.id] = user.host + ? `@${user.username}@${user.host}` + : `@${user.username}`; + return map; + }, {} as Record); + } } diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index 16e240ab11..b19c8f7c06 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -85,6 +85,16 @@ export const packedNoteSchema = { format: 'id', }, }, + mentionHandles: { + type: 'object', + optional: true, nullable: false, + additionalProperties: { + anyOf: [{ + type: 'string', + }], + optional: true, nullable: false, + }, + }, visibleUserIds: { type: 'array', optional: true, nullable: false, diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 5ac3fb26f5..2b4c39c280 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4668,6 +4668,9 @@ export type components = { /** @enum {string} */ visibility: 'public' | 'home' | 'followers' | 'specified'; mentions?: string[]; + mentionHandles?: { + [key: string]: string; + }; visibleUserIds?: string[]; fileIds?: string[]; files?: components['schemas']['DriveFile'][];