diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index 68ec403..0a50809 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,12 +104,86 @@ } } } - -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 { @@ -197,8 +271,6 @@ throw FieldErrors(errors); } - await Promise.all(medias.map(processMedia)); - const channel = await Channel.findOneOrFail({ where: { id: opts.channel_id }, relations: { recipients: true }, @@ -246,6 +318,18 @@ components: opts.components ?? undefined, // Fix Discord-Go? message_reference: opts.message_reference ?? undefined, }); + const batchId = `CLOUD_${message.author_id}_${randomString(128)}`; + + if (opts.author_id) { + message.author = await User.findOneOrFail({ + where: { id: opts.author_id }, + }); + 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(); @@ -308,14 +392,6 @@ 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 },