diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index 69ebef0b9..c9b7a5406 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -416,7 +416,7 @@ router.post( for (const currFile of files) { try { const file = await uploadFile(`/attachments/${channel.id}/${messageId}`, currFile); - attachments.push(Attachment.create(file)); + attachments.push(file); } catch (error) { return res.status(400).json({ message: error?.toString() }); } @@ -436,7 +436,7 @@ router.post( timestamp: new Date(), }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore dont care2 + // @ts-ignore dont care message.edited_timestamp = null; if (channel.isDm()) { diff --git a/src/api/routes/channels/#channel_id/threads.ts b/src/api/routes/channels/#channel_id/threads.ts index b992d7004..59425e60b 100644 --- a/src/api/routes/channels/#channel_id/threads.ts +++ b/src/api/routes/channels/#channel_id/threads.ts @@ -128,7 +128,7 @@ router.post( for (const currFile of files) { try { const file = await uploadFile(`/attachments/${channel.id}/${thread.id}`, currFile); - attachments.push(Attachment.create(file)); + attachments.push(file); } catch (error) { return res.status(400).json({ message: error?.toString() }); } diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index dd158bbae..2940b3064 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -47,23 +47,21 @@ import { } from "@spacebar/util"; import { ActionRowComponent, + AttachmentFlags, BaseMessageComponents, ButtonStyle, ChannelType, - Embed, EmbedType, MessageComponentType, - MessageCreateAttachment, MessageCreateCloudAttachment, - MessageCreateSchema, MessageReferenceType, MessageType, - Reaction, ReadStateType, UnfurledMediaItem, v1CompTypes, } from "@spacebar/schemas"; import { addPendingPoll } from "../utility/polls"; +import { MessageOptionAttachment, MessageOptions } from "@spacebar/util/dtos/MessageOptions"; const allow_empty = false; // TODO: check webhook, application, system author, stickers @@ -597,24 +595,6 @@ export async function sendMessage(opts: MessageOptions) { return message; } -type MessageOptionAttachment = MessageCreateAttachment | MessageCreateCloudAttachment | Attachment; -export interface MessageOptions extends MessageCreateSchema { - id?: string; - type?: MessageType; - pinned?: boolean; - author_id?: string; - webhook_id?: string; - application_id?: string; - embeds?: Embed[] | null; - reactions?: Reaction[]; - channel_id?: string; - attachments?: (MessageCreateAttachment | MessageCreateCloudAttachment | Attachment)[]; // why are we masking this? - edited_timestamp?: Date; - timestamp?: Date; - username?: string; - avatar_url?: string; -} - // Makes for concise code, inspired by Nix' lib.trace function logPassthru(obj: T, ...data: unknown[]) { console.log(...data); @@ -626,8 +606,28 @@ export async function processMessageOptionAttachments(source: MessageOptions, de 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"); + if (isCloudAttachment(src)) { + const result = logPassthru(await convertCloudAttachmentToAttachment(src, destination.channel_id!, destination.id), logp, "Got MessageCreateCloudAttachment contents"); + + result.flags = 0 as AttachmentFlags; + result.flags &= (src.is_clip ? 1 : 0) * (AttachmentFlags.IS_CLIP as number); + result.flags &= (src.is_remix ? 1 : 0) * (AttachmentFlags.IS_REMIX as number); + result.flags &= (src.is_thumbnail ? 1 : 0) * (AttachmentFlags.IS_THUMBNAIL as number); + result.flags &= (src.is_spoiler ? 1 : 0) * (AttachmentFlags.IS_SPOILER as number); + return logPassthru(result, logp, "Got MessageCreateCloudAttachment contents"); + } + if (isInternalCdnAttachment(src)) { + const result = Attachment.create({ + ...src, + }); + + // result.flags = 0 as AttachmentFlags; + // result.flags &= (src.is_clip ? 1 : 0) * (AttachmentFlags.IS_CLIP as number); + // result.flags &= (src.is_remix ? 1 : 0) * (AttachmentFlags.IS_REMIX as number); + // result.flags &= (src.is_thumbnail ? 1 : 0) * (AttachmentFlags.IS_THUMBNAIL as number); + // result.flags &= (src.is_spoiler ? 1 : 0) * (AttachmentFlags.IS_SPOILER as number); + return result; + } throw new Error(logp + " Unhandled attachment: " + JSON.stringify(src)); }); @@ -641,14 +641,18 @@ 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({ +export function isInternalCdnAttachment(attachment: MessageOptionAttachment) { + return "url" in attachment; +} + +export async function convertCloudAttachmentToAttachment(cloudAttachmentReference: MessageCreateCloudAttachment, destinationChannelId: string, destinationMessageId: string) { + const cloudAttachment = await CloudAttachment.findOneOrFail({ where: { - uploadFilename: cAtt.uploaded_filename, + uploadFilename: cloudAttachmentReference.uploaded_filename, }, }); - const cloneResponse = await fetch(`${Config.get().cdn.endpointPrivate}/attachments/${attEnt.uploadFilename}/clone_to_message/${destinationMessageId}`, { + const cloneResponse = await fetch(`${Config.get().cdn.endpointPrivate}/attachments/${cloudAttachment.uploadFilename}/clone_to_message/${destinationMessageId}`, { method: "POST", headers: { signature: Config.get().security.requestSignature || "", @@ -656,7 +660,7 @@ export async function convertCloudAttachmentToAttachment(cAtt: MessageCreateClou }); if (!cloneResponse.ok) { - console.error(`[Message] Failed to clone attachment ${attEnt.userFilename} to message ${destinationMessageId}`); + console.error(`[Message] Failed to clone attachment ${cloudAttachment.userFilename} to message ${destinationMessageId}`); throw new HTTPError("Failed to process attachment: " + (await cloneResponse.text()), 500); } @@ -666,18 +670,19 @@ export async function convertCloudAttachmentToAttachment(cAtt: MessageCreateClou channel_id: destinationChannelId, message_id: destinationMessageId, - filename: attEnt.userFilename, - size: attEnt.size, - height: attEnt.height, - width: attEnt.width, - content_type: attEnt.contentType || attEnt.userOriginalContentType, + filename: cloudAttachment.userFilename, + size: cloudAttachment.size, + height: cloudAttachment.height, + width: cloudAttachment.width, + content_type: cloudAttachment.contentType || cloudAttachment.userOriginalContentType, - title: cAtt.title, - duration_secs: cAtt.duration_secs, - clip_created_at: cAtt.clip_created_at, - description: cAtt.description, - waveform: cAtt.waveform, + title: cloudAttachmentReference.title, + duration_secs: cloudAttachmentReference.duration_secs, + clip_created_at: cloudAttachmentReference.clip_created_at, + description: cloudAttachmentReference.description, + waveform: cloudAttachmentReference.waveform, }); + console.log("[Message] Converted cloud attachment to", realAtt); return realAtt; } diff --git a/src/api/util/utility/polls.ts b/src/api/util/utility/polls.ts index bea5cd50b..549726c13 100644 --- a/src/api/util/utility/polls.ts +++ b/src/api/util/utility/polls.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ +import { sendMessage } from "@spacebar/api"; import { EmbedType, MessageReferenceType, MessageType, PollAnswerCount } from "@spacebar/schemas"; import { pendingPolls } from "@spacebar/util"; -import { MessageOptions, sendMessage } from "../handlers/Message"; import { Message } from "@spacebar/database"; +import { MessageOptions } from "@spacebar/util/dtos/MessageOptions"; export async function generatePollResultsMessage(options: MessageOptions): Promise { // TODO: shouldnt this get saved? diff --git a/src/cdn/routes/attachments.ts b/src/cdn/routes/attachments.ts index 81571c334..04f750002 100644 --- a/src/cdn/routes/attachments.ts +++ b/src/cdn/routes/attachments.ts @@ -23,6 +23,7 @@ import { HTTPError } from "lambert-server/HTTPError"; import { CloudAttachment } from "@spacebar/database"; import { Config, hasValidSignature, NewUrlUserSignatureData, Snowflake, UrlSignResult } from "@spacebar/util"; import { storage, multer, cache } from "../util"; +import { InternalCdnAttachment } from "@spacebar/util/dtos/MessageOptions"; const router = Router({ mergeParams: true }); @@ -45,16 +46,20 @@ router.post("/:channel_id/:message_id", multer.single("file"), async (req: Reque let width; let height; if (mimetype.includes("image")) { - const dimensions = imageSize(buffer); - if (dimensions) { - width = dimensions.width; - height = dimensions.height; + try { + const dimensions = imageSize(buffer); + if (dimensions) { + width = dimensions.width; + height = dimensions.height; + } + } catch (e) { + console.warn("Failed to get image size for attachment of type", mimetype, "because of", e); } } const finalUrl = `${endpoint}/${path}`; - const file = { + const file: InternalCdnAttachment = { id: Snowflake.generate(), channel_id, message_id, diff --git a/src/util/dtos/MessageOptions.ts b/src/util/dtos/MessageOptions.ts new file mode 100644 index 000000000..61e99cdd8 --- /dev/null +++ b/src/util/dtos/MessageOptions.ts @@ -0,0 +1,51 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { Embed, MessageCreateAttachment, MessageCreateCloudAttachment, MessageCreateSchema, MessageType, Reaction } from "@spacebar/schemas"; + +export type MessageOptionAttachment = MessageCreateAttachment | MessageCreateCloudAttachment | InternalCdnAttachment; + +export interface MessageOptions extends MessageCreateSchema { + id?: string; + type?: MessageType; + pinned?: boolean; + author_id?: string; + webhook_id?: string; + application_id?: string; + embeds?: Embed[] | null; + reactions?: Reaction[]; + channel_id?: string; + attachments?: MessageOptionAttachment[]; + edited_timestamp?: Date; + timestamp?: Date; + username?: string; + avatar_url?: string; +} + +export interface InternalCdnAttachment { + id: string; + channel_id: string; + message_id: string; + content_type: string; + filename: string; + size: number; + url: string; + path: string; + width?: number; + height?: number; +} diff --git a/src/util/util/cdn.ts b/src/util/util/cdn.ts index a2dc8c68d..0823fb36c 100644 --- a/src/util/util/cdn.ts +++ b/src/util/util/cdn.ts @@ -18,14 +18,14 @@ import FormData from "form-data"; import { HTTPError } from "lambert-server/HTTPError"; -import { Attachment } from "../../database/entities"; +import { InternalCdnAttachment } from "@spacebar/util/dtos/MessageOptions"; import { Config } from "./Config"; export async function uploadFile( path: string, // These are the only props we use, don't need to enforce the full type. file?: Pick, -): Promise { +): Promise { if (!file?.buffer) throw new HTTPError("Missing file in body"); const form = new FormData(); @@ -42,7 +42,7 @@ export async function uploadFile( method: "POST", body: form.getBuffer(), }); - const result = (await response.json()) as Attachment; + const result = (await response.json()) as InternalCdnAttachment; if (response.status !== 200) throw result; return result;