diff --git a/assets/openapi.json b/assets/openapi.json index 913920f56..25833b979 100644 Binary files a/assets/openapi.json and b/assets/openapi.json differ diff --git a/assets/schemas.json b/assets/schemas.json index 3d9692a6a..a7acf2b04 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 964f48f06..ea42283f1 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -50,6 +50,7 @@ import { MessageFlags, FieldErrors, Snowflake, + getDatabase, } from "@spacebar/util"; import { HTTPError } from "lambert-server"; import { In, Or, Equal, IsNull } from "typeorm"; @@ -321,16 +322,6 @@ export async function handleMessage(opts: MessageOptions): Promise { } 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 @@ export async function handleMessage(opts: MessageOptions): Promise { 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 @@ export async function handleMessage(opts: MessageOptions): Promise { 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 @@ export async function handleMessage(opts: MessageOptions): Promise { 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 @@ export async function handleMessage(opts: MessageOptions): Promise { 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 @@ export async function handleMessage(opts: MessageOptions): Promise { 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 @@ export async function handleMessage(opts: MessageOptions): Promise { 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 @@ export async function sendMessage(opts: MessageOptions) { 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 @@ export async function sendMessage(opts: MessageOptions) { return message; } +type MessageOptionAttachment = MessageCreateAttachment | MessageCreateCloudAttachment | Attachment; interface MessageOptions extends MessageCreateSchema { id?: string; type?: MessageType; @@ -824,3 +757,63 @@ interface MessageOptions extends MessageCreateSchema { 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 515514ee6..ee2b9a8bc 100644 --- a/src/schemas/api/messages/Message.ts +++ b/src/schemas/api/messages/Message.ts @@ -149,7 +149,7 @@ export interface MessageSnapshot { 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 7c5d6b6d6..217f1da00 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts @@ -328,6 +328,23 @@ export class Message extends BaseClass { }; } + 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));