mirror of
https://github.com/spacebarchat/server.git
synced 2026-04-25 11:52:07 +00:00
Split out attachment handling from message handler
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user