From 540e5d0d7a9991fd2828c4efefbf0ae4f6524f9a Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Fri, 13 Feb 2026 21:53:51 -0600 Subject: maybe processing media? --- src/api/util/handlers/Message.ts | 102 ++++++++++++++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 12 deletions(-) diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index de98e455..f73b62fb 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { EmbedHandlers } from "@spacebar/api"; +import { EmbedHandlers, randomString } from "@spacebar/api"; import { Application, Attachment, @@ -104,11 +104,86 @@ function checkActionRow(row: ActionRowComponent, knownComponentIds: string[], er } } } -async function processMedia(media: UnfurledMediaItem) { +async function processMedia(media: UnfurledMediaItem, messageId: string, batchId: string, user: User, channel: Channel, id: string) { if (Object.keys(media).length > 1) throw new HTTPError("no, you can't send those"); if (!URL.canParse(media.url)) throw new HTTPError("media URL must be a URI"); const url = new URL(media.url); if (!["http", "https", "attachment"].includes(url.protocol)) throw new HTTPError("invalid media protocol"); + let attEnt: CloudAttachment; + let delWhenDone = false; + if (url.protocol !== "attachment") { + const res = await fetch(url); + if (!res.ok) throw new HTTPError("URL did not return OK"); + const blob = await res.blob(); + const name = url.pathname.split("/").findLast((_) => _) || id; + const uploadFilename = `${channel.id}/${batchId}/${id ?? "0"}/${name}`; + attEnt = CloudAttachment.create({ + user: user, + channel: channel, + uploadFilename: uploadFilename, + userAttachmentId: "0", + userFilename: name, + userFileSize: blob.size, + userIsClip: false, + }); + await attEnt.save(); + const cdnUrl = Config.get().cdn.endpointPublic; + const fetchUrl = `${cdnUrl}/attachments/${attEnt.uploadFilename}`; + await ( + await fetch(fetchUrl, { + method: "PUT", + body: blob, + }) + ).text(); + // re-fetch due to changed DB entry + attEnt = await CloudAttachment.findOneOrFail({ + where: { + id: attEnt.id, + }, + }); + delWhenDone = true; + } else { + attEnt = await CloudAttachment.findOneOrFail({ + where: { + uploadFilename: url.hostname, + }, + }); + } + + const cloneResponse = await fetch(`${Config.get().cdn.endpointPrivate}/attachments/${attEnt.uploadFilename}/clone_to_message/${messageId}`, { + method: "POST", + headers: { + signature: Config.get().security.requestSignature || "", + }, + }); + + if (!cloneResponse.ok) { + console.error(`[Message] Failed to clone attachment ${attEnt.userFilename} to message ${messageId}`); + throw new HTTPError("Failed to process attachment: " + (await cloneResponse.text()), 500); + } + + const cloneRespBody = (await cloneResponse.json()) as { success: boolean; new_path: string }; + //TODO maybe this needs to be a new DB object? I don't see a reason to do this rn though, though this id *should* technically be different from the id of the attachment + media.id = attEnt.id; + media.proxy_url = `${Config.get().cdn.endpointPublic}/${cloneRespBody.new_path}`; + if (url.protocol !== "attachment") media.url = media.proxy_url; + media.height = attEnt.height; + media.width = attEnt.width; + media.content_type = attEnt.contentType; + //TODO flags? + media.attachment_id = attEnt.id; + //TODO preview stuff + + if (delWhenDone) { + fetch(`${Config.get().cdn.endpointPrivate}/attachments/${attEnt.uploadFilename}`, { + headers: { + signature: Config.get().security.requestSignature, + }, + method: "DELETE", + }).then(() => { + attEnt.remove(); + }); + } } export async function handleMessage(opts: MessageOptions): Promise { const errors: Record = {}; @@ -195,8 +270,6 @@ export async function handleMessage(opts: MessageOptions): Promise { throw FieldErrors(errors); } - await Promise.all(medias.map(processMedia)); - const channel = await Channel.findOneOrFail({ where: { id: opts.channel_id }, relations: { recipients: true }, @@ -243,6 +316,19 @@ export async function handleMessage(opts: MessageOptions): Promise { mentions: [], components: opts.components ?? undefined, // Fix Discord-Go? }); + const batchId = `CLOUD_${message.author_id}_${randomString(128)}`; + + if (opts.author_id) { + message.author = await User.findOneOrFail({ + where: { id: opts.author_id }, + }); + message.author.clean_data(); + const rights = await getRights(opts.author_id); + rights.hasThrow("SEND_MESSAGES"); + } + + await Promise.all(medias.map((m, index) => processMedia(m, message.id, batchId, message.author as User, channel, index + ""))); + const ephermal = (message.flags & (1 << 6)) !== 0; if (!ephermal && channel.type === ChannelType.GUILD_PUBLIC_THREAD) { const rep = Channel.getRepository(); @@ -305,14 +391,6 @@ export async function handleMessage(opts: MessageOptions): Promise { throw new HTTPError("Content length over max character limit"); } - if (opts.author_id) { - message.author = await User.findOneOrFail({ - where: { id: opts.author_id }, - }); - message.author.clean_data(); - const rights = await getRights(opts.author_id); - rights.hasThrow("SEND_MESSAGES"); - } if (opts.application_id) { message.application = await Application.findOneOrFail({ where: { id: opts.application_id }, -- cgit 1.5.1