resolve mentioned user handles on the backend

This commit is contained in:
Hazelnoot 2025-05-04 10:08:41 -04:00
parent 17a9b08f54
commit 6c9dcb84ab
3 changed files with 69 additions and 2 deletions

View file

@ -402,7 +402,8 @@ export class NoteEntityService implements OnModuleInit {
bufferedReactions: Map<MiNote['id'], { deltas: Record<string, number>; pairs: ([MiUser['id'], string])[] }> | null; bufferedReactions: Map<MiNote['id'], { deltas: Record<string, number>; pairs: ([MiUser['id'], string])[] }> | null;
myReactions: Map<MiNote['id'], string | null>; myReactions: Map<MiNote['id'], string | null>;
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>; packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>> packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
mentionHandles: Record<string, string | undefined>;
}; };
}, },
): Promise<Packed<'Note'>> { ): Promise<Packed<'Note'>> {
@ -443,6 +444,9 @@ export class NoteEntityService implements OnModuleInit {
const packedFiles = options?._hint_?.packedFiles; const packedFiles = options?._hint_?.packedFiles;
const packedUsers = options?._hint_?.packedUsers; 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({ const packed: Packed<'Note'> = await awaitAll({
id: note.id, id: note.id,
createdAt: this.idService.parse(note.id).date.toISOString(), createdAt: this.idService.parse(note.id).date.toISOString(),
@ -476,7 +480,8 @@ export class NoteEntityService implements OnModuleInit {
allowRenoteToExternal: channel.allowRenoteToExternal, allowRenoteToExternal: channel.allowRenoteToExternal,
userId: channel.userId, userId: channel.userId,
} : undefined, } : 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, uri: note.uri ?? undefined,
url: note.url ?? undefined, url: note.url ?? undefined,
poll: note.hasPoll ? this.populatePoll(note, meId) : 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) const packedUsers = await this.userEntityService.packMany(users, me)
.then(users => new Map(users.map(u => [u.id, u]))); .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<string>());
const mentionHandles = await this.getUserHandles(Array.from(allMentionedUsers));
return await Promise.all(notes.map(n => this.pack(n, me, { return await Promise.all(notes.map(n => this.pack(n, me, {
...options, ...options,
_hint_: { _hint_: {
@ -601,6 +622,7 @@ export class NoteEntityService implements OnModuleInit {
myReactions: myReactionsMap, myReactions: myReactionsMap,
packedFiles, packedFiles,
packedUsers, packedUsers,
mentionHandles,
}, },
}))); })));
} }
@ -636,4 +658,36 @@ export class NoteEntityService implements OnModuleInit {
relations: ['user'], relations: ['user'],
}); });
} }
private async getUserHandles(userIds: string[], hint?: Record<string, string | undefined>): Promise<Record<string, string | undefined>> {
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<string, string | undefined>;
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<string, string | undefined>);
}
} }

View file

@ -85,6 +85,16 @@ export const packedNoteSchema = {
format: 'id', format: 'id',
}, },
}, },
mentionHandles: {
type: 'object',
optional: true, nullable: false,
additionalProperties: {
anyOf: [{
type: 'string',
}],
optional: true, nullable: false,
},
},
visibleUserIds: { visibleUserIds: {
type: 'array', type: 'array',
optional: true, nullable: false, optional: true, nullable: false,

View file

@ -4668,6 +4668,9 @@ export type components = {
/** @enum {string} */ /** @enum {string} */
visibility: 'public' | 'home' | 'followers' | 'specified'; visibility: 'public' | 'home' | 'followers' | 'specified';
mentions?: string[]; mentions?: string[];
mentionHandles?: {
[key: string]: string;
};
visibleUserIds?: string[]; visibleUserIds?: string[];
fileIds?: string[]; fileIds?: string[];
files?: components['schemas']['DriveFile'][]; files?: components['schemas']['DriveFile'][];