diff --git a/assets/openapi.json b/assets/openapi.json index 913920f..25833b9 100644 --- a/assets/openapi.json +++ b/assets/openapi.json @@ -12327,7 +12327,7 @@ "mentions": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/PartialUser" } }, "mention_roles": { @@ -12391,6 +12391,92 @@ "message" ] }, + "PartialUser": { + "type": "object", + "properties": { + "id": { + "description": "A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z\n```\nIf we have a snowflake '266241948824764416' we can represent it as binary:\n\n64 22 17 12 0\n 000000111011000111100001101001000101000000 00001 00000 000000000000\n number of ms since Discord epoch worker pid increment\n```", + "type": "string" + }, + "username": { + "type": "string" + }, + "discriminator": { + "type": "string" + }, + "global_name": { + "type": "string", + "nullable": true + }, + "avatar": { + "type": "string", + "nullable": true + }, + "avatar_decoration_data": { + "anyOf": [ + { + "$ref": "#/components/schemas/AvatarDecorationData" + }, + { + "type": "null" + } + ] + }, + "collectibles": { + "anyOf": [ + { + "$ref": "#/components/schemas/Collectibles" + }, + { + "type": "null" + } + ] + }, + "display_name_styles": { + "anyOf": [ + { + "$ref": "#/components/schemas/DisplayNameStyle" + }, + { + "type": "null" + } + ] + }, + "primary_guild": { + "anyOf": [ + { + "$ref": "#/components/schemas/PrimaryGuild" + }, + { + "type": "null" + } + ] + }, + "bot": { + "type": "boolean" + }, + "system": { + "type": "boolean" + }, + "banner": { + "type": "string", + "nullable": true + }, + "accent_color": { + "type": "integer", + "nullable": true + }, + "public_flags": { + "type": "integer" + } + }, + "required": [ + "avatar", + "discriminator", + "id", + "username" + ] + }, "MessageComponent": { "type": "object", "properties": { @@ -12882,92 +12968,6 @@ "type" ] }, - "PartialUser": { - "type": "object", - "properties": { - "id": { - "description": "A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z\n```\nIf we have a snowflake '266241948824764416' we can represent it as binary:\n\n64 22 17 12 0\n 000000111011000111100001101001000101000000 00001 00000 000000000000\n number of ms since Discord epoch worker pid increment\n```", - "type": "string" - }, - "username": { - "type": "string" - }, - "discriminator": { - "type": "string" - }, - "global_name": { - "type": "string", - "nullable": true - }, - "avatar": { - "type": "string", - "nullable": true - }, - "avatar_decoration_data": { - "anyOf": [ - { - "$ref": "#/components/schemas/AvatarDecorationData" - }, - { - "type": "null" - } - ] - }, - "collectibles": { - "anyOf": [ - { - "$ref": "#/components/schemas/Collectibles" - }, - { - "type": "null" - } - ] - }, - "display_name_styles": { - "anyOf": [ - { - "$ref": "#/components/schemas/DisplayNameStyle" - }, - { - "type": "null" - } - ] - }, - "primary_guild": { - "anyOf": [ - { - "$ref": "#/components/schemas/PrimaryGuild" - }, - { - "type": "null" - } - ] - }, - "bot": { - "type": "boolean" - }, - "system": { - "type": "boolean" - }, - "banner": { - "type": "string", - "nullable": true - }, - "accent_color": { - "type": "integer", - "nullable": true - }, - "public_flags": { - "type": "integer" - } - }, - "required": [ - "avatar", - "discriminator", - "id", - "username" - ] - }, "HubGuild": { "type": "object", "properties": { diff --git a/assets/schemas.json b/assets/schemas.json index 3d9692a..a7acf2b 100644 --- a/assets/schemas.json +++ b/assets/schemas.json @@ -13081,7 +13081,7 @@ "mentions": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/PartialUser" } }, "mention_roles": { @@ -13147,6 +13147,102 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, + "PartialUser": { + "type": "object", + "properties": { + "id": { + "description": "A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z\n```\nIf we have a snowflake '266241948824764416' we can represent it as binary:\n\n64 22 17 12 0\n 000000111011000111100001101001000101000000 00001 00000 000000000000\n number of ms since Discord epoch worker pid increment\n```", + "type": "string" + }, + "username": { + "type": "string" + }, + "discriminator": { + "type": "string" + }, + "global_name": { + "type": [ + "null", + "string" + ] + }, + "avatar": { + "type": [ + "null", + "string" + ] + }, + "avatar_decoration_data": { + "anyOf": [ + { + "$ref": "#/definitions/AvatarDecorationData" + }, + { + "type": "null" + } + ] + }, + "collectibles": { + "anyOf": [ + { + "$ref": "#/definitions/Collectibles" + }, + { + "type": "null" + } + ] + }, + "display_name_styles": { + "anyOf": [ + { + "$ref": "#/definitions/DisplayNameStyle" + }, + { + "type": "null" + } + ] + }, + "primary_guild": { + "anyOf": [ + { + "$ref": "#/definitions/PrimaryGuild" + }, + { + "type": "null" + } + ] + }, + "bot": { + "type": "boolean" + }, + "system": { + "type": "boolean" + }, + "banner": { + "type": [ + "null", + "string" + ] + }, + "accent_color": { + "type": [ + "null", + "integer" + ] + }, + "public_flags": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "avatar", + "discriminator", + "id", + "username" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, "MessageComponent": { "type": "object", "properties": { @@ -13661,102 +13757,6 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, - "PartialUser": { - "type": "object", - "properties": { - "id": { - "description": "A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z\n```\nIf we have a snowflake '266241948824764416' we can represent it as binary:\n\n64 22 17 12 0\n 000000111011000111100001101001000101000000 00001 00000 000000000000\n number of ms since Discord epoch worker pid increment\n```", - "type": "string" - }, - "username": { - "type": "string" - }, - "discriminator": { - "type": "string" - }, - "global_name": { - "type": [ - "null", - "string" - ] - }, - "avatar": { - "type": [ - "null", - "string" - ] - }, - "avatar_decoration_data": { - "anyOf": [ - { - "$ref": "#/definitions/AvatarDecorationData" - }, - { - "type": "null" - } - ] - }, - "collectibles": { - "anyOf": [ - { - "$ref": "#/definitions/Collectibles" - }, - { - "type": "null" - } - ] - }, - "display_name_styles": { - "anyOf": [ - { - "$ref": "#/definitions/DisplayNameStyle" - }, - { - "type": "null" - } - ] - }, - "primary_guild": { - "anyOf": [ - { - "$ref": "#/definitions/PrimaryGuild" - }, - { - "type": "null" - } - ] - }, - "bot": { - "type": "boolean" - }, - "system": { - "type": "boolean" - }, - "banner": { - "type": [ - "null", - "string" - ] - }, - "accent_color": { - "type": [ - "null", - "integer" - ] - }, - "public_flags": { - "type": "integer" - } - }, - "additionalProperties": false, - "required": [ - "avatar", - "discriminator", - "id", - "username" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - }, "HubGuild": { "type": "object", "properties": { diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index 964f48f..ea42283 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -50,6 +50,7 @@ MessageFlags, FieldErrors, Snowflake, + getDatabase, } from "@spacebar/util"; import { HTTPError } from "lambert-server"; import { In, Or, Equal, IsNull } from "typeorm"; @@ -321,16 +322,6 @@ } const stickers = opts.sticker_ids ? await Sticker.find({ where: { id: In(opts.sticker_ids) } }) : undefined; - // cloud attachments with indexes - const cloudAttachments = opts.attachments?.reduce( - (acc, att, index) => { - if ("uploaded_filename" in att) { - acc.push({ attachment: att, index }); - } - return acc; - }, - [] as { attachment: MessageCreateCloudAttachment; index: number }[], - ); const message = Message.create({ ...opts, @@ -339,7 +330,7 @@ sticker_items: stickers, guild_id: channel.guild_id, channel_id: opts.channel_id, - attachments: opts.attachments || [], + attachments: [], embeds: opts.embeds || [], reactions: opts.reactions || [], type: opts.type ?? 0, @@ -347,10 +338,7 @@ components: opts.components ?? undefined, // Fix Discord-Go? }); message.channel = channel; - message.attachments?.forEach((att) => { - att.message_id = message.id; - att.save(); - }); + await processMessageOptionAttachments(opts, message); if (opts.author_id) { message.author = await User.findOneOrFail({ @@ -372,52 +360,7 @@ await channel.save(); } - if (cloudAttachments && cloudAttachments.length > 0) { - console.log("[Message] Processing attachments for message", message.id, ":", message.attachments); - handle?.(message.id, message.author as User, message.channel); - const uploadedAttachments = await Promise.all( - cloudAttachments.map(async (att) => { - const cAtt = att.attachment; - const attEnt = await CloudAttachment.findOneOrFail({ - where: { - uploadFilename: cAtt.uploaded_filename, - }, - }); - - const cloneResponse = await fetch(`${conf.cdn.endpointPrivate}/attachments/${attEnt.uploadFilename}/clone_to_message/${message.id}`, { - method: "POST", - headers: { - signature: conf.security.requestSignature || "", - }, - }); - - if (!cloneResponse.ok) { - console.error(`[Message] Failed to clone attachment ${attEnt.userFilename} to message ${message.id}`); - throw new HTTPError("Failed to process attachment: " + (await cloneResponse.text()), 500); - } - - const cloneRespBody = (await cloneResponse.json()) as { success: boolean; new_path: string }; - - const realAtt = Attachment.create({ - filename: attEnt.userFilename, - size: attEnt.size, - height: attEnt.height, - width: attEnt.width, - content_type: attEnt.contentType || attEnt.userOriginalContentType, - channel_id: channel.id, - message_id: message.id, - }); - await realAtt.save(); - return { attachment: realAtt, index: att.index }; - }), - ); - console.log("[Message] Processed attachments for message", message.id, ":", message.attachments); - - for (const att of uploadedAttachments) { - message.attachments![att.index] = att.attachment; - } - } - // else console.log("[Message] No cloud attachments to process for message", message.id, ":", message.attachments); + // TODO: Removed cloud attachment handling being inline - handle components! if (message.content && message.content.length > conf.limits.message.maxCharacters) { throw new HTTPError("Content length over max character limit"); @@ -562,6 +505,7 @@ if (content) { // TODO: explicit-only mentions + // TODO: make mentions lazy message.content = content.trim(); content = content.replace(/ *`[^)]*` */g, ""); // remove codeblocks // root@Rory - 20/02/2023 - This breaks channel mentions in test client. We're not sure this was used in older clients. @@ -596,6 +540,10 @@ id: message.message_reference.message_id, channel_id: message.channel_id, }, + relations: { + mentions: true, + mention_roles: true, + }, }); if (referencedMessage && referencedMessage.author_id !== message.author_id) { message.mentions.push( @@ -609,27 +557,8 @@ message.type = MessageType.DEFAULT; if (message.referenced_message) { - const mention_roles: string[] = []; - const mentions: string[] = []; - // TODO: mention_roles and mentions arrays - not needed it seems, but discord still returns that - - message.message_snapshots = [ - { - message: { - attachments: message.referenced_message.attachments, - components: message.referenced_message.components, - content: message.referenced_message.content!, - edited_timestamp: message.referenced_message.edited_timestamp, - embeds: message.referenced_message.embeds, - flags: message.referenced_message.flags, - mention_roles, - mentions, - timestamp: message.referenced_message.timestamp, - type: message.referenced_message.type, - }, - }, - ]; + message.message_snapshots = [message.referenced_message.toSnapshot()]; } } } @@ -792,9 +721,12 @@ const message = await handleMessage({ ...opts, timestamp: new Date() }); const ephemeral = (message.flags & Number(MessageFlags.FLAGS.EPHEMERAL)) !== 0; + await getDatabase()?.transaction(async (entityManager) => { + await entityManager.save(message); + await entityManager.save(message.channel); + if (message.attachments && message.attachments.length > 0) await entityManager.save(message.attachments); + }); await Promise.all([ - message.insert(), - message.channel.save(), emitEvent({ event: "MESSAGE_CREATE", ...(ephemeral ? { user_id: message.interaction_metadata?.user_id } : { channel_id: message.channel_id }), @@ -808,6 +740,7 @@ return message; } +type MessageOptionAttachment = MessageCreateAttachment | MessageCreateCloudAttachment | Attachment; interface MessageOptions extends MessageCreateSchema { id?: string; type?: MessageType; @@ -824,3 +757,63 @@ username?: string; avatar_url?: string; } + +// Makes for concise code, inspired by Nix' lib.trace +function logPassthru(obj: T, ...data: unknown[]) { + console.log(...data); + return obj; +} +export async function processMessageOptionAttachments(source: MessageOptions, destination: Message) { + if (!source.attachments || source.attachments.length == 0) return; + const logp = `[Message/${destination.id}/Attachments]`; + console.log("[Message] Processing attachments for message", source.id, "->", source.attachments); + const tasks = source.attachments?.map(async (src): Promise => { + if (src instanceof Attachment) return logPassthru(src, logp, `Got Attachment instance`); + if (isCloudAttachment(src)) + return logPassthru(await convertCloudAttachmentToAttachment(src, destination.channel_id!, destination.id), logp, "Got MessageCreateCloudAttachment contents"); + throw new Error(logp + " Unhandled attachment: " + JSON.stringify(src)); + }); + + destination.attachments = []; + for (const task of tasks) { + destination.attachments.push(await task); + } +} + +export function isCloudAttachment(attachment: MessageOptionAttachment) { + return "uploaded_filename" in attachment; +} + +export async function convertCloudAttachmentToAttachment(cAtt: MessageCreateCloudAttachment, destinationChannelId: string, destinationMessageId: string) { + const attEnt = await CloudAttachment.findOneOrFail({ + where: { + uploadFilename: cAtt.uploaded_filename, + }, + }); + + const cloneResponse = await fetch(`${Config.get().cdn.endpointPrivate}/attachments/${attEnt.uploadFilename}/clone_to_message/${destinationMessageId}`, { + method: "POST", + headers: { + signature: Config.get().security.requestSignature || "", + }, + }); + + if (!cloneResponse.ok) { + console.error(`[Message] Failed to clone attachment ${attEnt.userFilename} to message ${destinationMessageId}`); + throw new HTTPError("Failed to process attachment: " + (await cloneResponse.text()), 500); + } + + const cloneRespBody = (await cloneResponse.json()) as { success: boolean; new_path: string }; + + const realAtt = Attachment.create({ + filename: attEnt.userFilename, + size: attEnt.size, + height: attEnt.height, + width: attEnt.width, + content_type: attEnt.contentType || attEnt.userOriginalContentType, + channel_id: destinationChannelId, + message_id: destinationMessageId, + }); + console.log("[Message] Converted cloud attachment to", realAtt); + return realAtt; +} diff --git a/src/schemas/api/messages/Message.ts b/src/schemas/api/messages/Message.ts index 515514e..ee2b9a8 100644 --- a/src/schemas/api/messages/Message.ts +++ b/src/schemas/api/messages/Message.ts @@ -149,7 +149,7 @@ content: string; timestamp: Date; edited_timestamp?: Date | null; - mentions: Snowflake[]; + mentions: PartialUser[]; mention_roles: Snowflake[]; attachments?: Attachment[]; embeds: Embed[]; diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts index 7c5d6b6..217f1da 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts @@ -328,6 +328,23 @@ }; } + toSnapshot(): MessageSnapshot { + return { + message: { + attachments: this.attachments, + components: this.components, + content: this.content!, + edited_timestamp: this.edited_timestamp, + embeds: this.embeds, + flags: this.flags, + mention_roles: this.mention_roles?.map((x) => x.id), + mentions: this.mentions.map((x) => x.toPublicUser() as unknown as PartialUser), // TODO: write a proper method for this + timestamp: this.timestamp, + type: this.type, + }, + }; + } + withSignedAttachments(data: NewUrlUserSignatureData) { function signMedia(media: UnfurledMediaItem) { Object.assign(media, Attachment.prototype.signUrls.call(media, data));