diff --git a/assets/openapi.json b/assets/openapi.json index 84d022900..b54452879 100644 Binary files a/assets/openapi.json and b/assets/openapi.json differ diff --git a/assets/schemas.json b/assets/schemas.json index 1590a7c3e..3d4c30c54 100644 Binary files a/assets/schemas.json and b/assets/schemas.json differ diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index 0a59543d8..0e7749f62 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -47,6 +47,10 @@ import { MessageFlags, FieldErrors, getDatabase, + ElapsedTime, + TraceRoot, + Stopwatch, + TraceNode, } from "@spacebar/util"; import { HTTPError } from "lambert-server"; import { In, Or, Equal, IsNull } from "typeorm"; @@ -66,6 +70,7 @@ import { UnfurledMediaItem, BaseMessageComponents, v1CompTypes, + MessageReferenceType, } from "@spacebar/schemas"; const allow_empty = false; // TODO: check webhook, application, system author, stickers @@ -655,22 +660,31 @@ export async function convertCloudAttachmentToAttachment(cAtt: MessageCreateClou } async function handleMessageMentionsAsync(message: Message) { + const sw = Stopwatch.startNew(), + totalSw = Stopwatch.startNew(); + const trace: TraceNode = { micros: 0, calls: [] }; + const traceRoot: TraceRoot = ["handleMessageMentionsAsync", trace]; + const channel = await Channel.findOneOrFail({ where: { id: message.channel_id }, relations: { recipients: true }, }); + trace.calls.push(`getChannel(${channel.id})`, { micros: sw.getElapsedAndReset().totalMicroseconds }); + const permission = await getPermission(message.author_id ?? message.author?.id, channel.guild_id, channel); + trace.calls.push(`getPermissions`, { micros: sw.getElapsedAndReset().totalMicroseconds }); let content = message.content; // TODO: sets // root@Rory - 20/02/2023 - This breaks channel mentions in test client. We're not sure this was used in older clients. //const mention_channel_ids = [] as string[]; - const mention_role_ids = [] as string[]; - const mention_user_ids = [] as string[]; let mention_everyone = false; + const mention_user_id_set = new Set(); + const mention_role_id_set = new Set(); if (content) { + const contentTrace: TraceNode = { micros: 0, calls: [] }; // TODO: explicit-only mentions // TODO: make mentions lazy content = content.replace(/ *`[^)]*` */g, ""); // remove codeblocks @@ -679,25 +693,33 @@ async function handleMessageMentionsAsync(message: Message) { if (!mention_channel_ids.includes(mention)) mention_channel_ids.push(mention); }*/ + contentTrace.calls.push("filterCodeblocks", { micros: sw.getElapsedAndReset().totalMicroseconds }); - for (const [, mention] of content.matchAll(USER_MENTION)) { - if (!mention_user_ids.includes(mention)) mention_user_ids.push(mention); - } - - await Promise.all( - content.matchAll(ROLE_MENTION).map(async ([, mention]) => { - const role = await Role.findOneOrFail({ - where: { id: mention, guild_id: channel.guild_id }, - }); - if (role.mentionable || message.webhook?.id || message.webhook_id || permission?.has("MANAGE_ROLES")) { - mention_role_ids.push(mention); - } - }), - ); - + for (const [, mention] of content.matchAll(USER_MENTION)) mention_user_id_set.add(mention); + for (const [, mention] of content.matchAll(ROLE_MENTION)) mention_role_id_set.add(mention); if (message.webhook?.id || message.webhook_id || permission?.has("MENTION_EVERYONE")) { mention_everyone = !!content.match(EVERYONE_MENTION) || !!content.match(HERE_MENTION); } + contentTrace.calls.push("parseMentions", { micros: sw.getElapsedAndReset().totalMicroseconds }); + + let mentionedRoles = await Role.find({ where: { id: In(mention_role_id_set.values().toArray()), guild_id: channel.guild_id } }); + contentTrace.calls.push("queryMentionRoles", { micros: sw.getElapsedAndReset().totalMicroseconds }); + + //TODO: should this throw at all? + if (mention_role_id_set.size != mentionedRoles.length) { + const missingRoles = mention_role_id_set + .values() + .filter((x) => !mentionedRoles.find((r) => r.id == x)) + .toArray(); + throw new HTTPError("Mentioned invalid roles: " + missingRoles.join(", "), 500); + } + + if (!(message.webhook?.id || message.webhook_id || permission?.has("MANAGE_ROLES"))) { + mentionedRoles = mentionedRoles.filter((x) => x.mentionable); + mention_role_id_set.clear(); + mentionedRoles.forEach((r) => mention_role_id_set.add(r.id)); + } + contentTrace.calls.push("validateMentionRoles", { micros: sw.getElapsedAndReset().totalMicroseconds }); } if (message.message_reference?.message_id) { @@ -718,8 +740,7 @@ async function handleMessageMentionsAsync(message: Message) { ); } - // FORWARD - if (message.message_reference.type === 1) { + if (message.message_reference.type === MessageReferenceType.FORWARD) { message.type = MessageType.DEFAULT; if (message.referenced_message) { @@ -733,8 +754,8 @@ async function handleMessageMentionsAsync(message: Message) { /*message.mention_channels = mention_channel_ids.map((x) => Channel.create({ id: x }), );*/ - message.mention_roles = await Role.find({ where: { id: In(mention_role_ids), guild_id: channel.guild_id } }); - message.mentions = [...message.mentions, ...(await User.find({ where: { id: In(mention_user_ids) } }))]; + message.mention_roles = await Role.find({ where: { id: In(mention_role_id_set.values().toArray()), guild_id: channel.guild_id } }); + message.mentions = [...message.mentions, ...(await User.find({ where: { id: In(mention_user_id_set.values().toArray()) } }))]; message.mention_everyone = mention_everyone; async function fillInMissingIDs(ids: string[]) { diff --git a/src/schemas/api/messages/Message.ts b/src/schemas/api/messages/Message.ts index b7eed17cc..73d0e79bd 100644 --- a/src/schemas/api/messages/Message.ts +++ b/src/schemas/api/messages/Message.ts @@ -238,5 +238,10 @@ export interface MessageReference { channel_id?: string; guild_id?: string; fail_if_not_exists?: boolean; - type?: number; + type?: MessageReferenceType; +} + +export enum MessageReferenceType { + DEFAULT = 0, + FORWARD = 1, }