Split out attachment handling from message handler

This commit is contained in:
Rory&
2026-04-17 19:55:36 +02:00
parent 81d44788c6
commit a3d8a427aa
5 changed files with 94 additions and 84 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -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<Message> {
}
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<Message> {
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<Message> {
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<Message> {
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<Message> {
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<Message> {
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> {
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<T>(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<Attachment> => {
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;
}

View File

@@ -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[];

View File

@@ -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));