import { db, ticketAttachments, ticketMessages, tickets } from "@/database"; import { getPublicUrl, s3 } from "@/utils/s3"; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, type Client, Events, type Message, } from "discord.js"; import { and, eq } from "drizzle-orm"; import { nanoid } from "nanoid"; import config from "@/config"; import { loggers } from "@/utils/logging"; const logger = loggers.events.child({ name: "messageCreate" }); type UploadStatus = "queued" | "uploading" | "done" | "failed"; export default { event: Events.MessageCreate, async execute(_client: Client, message: Message) { if (message.author.bot) return; if (message.content.includes("grok is this true")) { if (message.channel.isSendable()) await message.channel.send("yeh"); } const ticket = db .select() .from(tickets) .where( and( eq(tickets.channelId, message.channelId), eq(tickets.guildId, message.guildId ?? ""), ), ) .get(); if (!ticket) return; try { const savedMessage = db .insert(ticketMessages) .values({ ticketId: ticket.id, authorId: message.author.id, authorType: "user", content: message.content, createdAt: message.createdAt, }) .returning() .get(); if (message.attachments.size > 0) { const attachmentsArray = Array.from(message.attachments.values()); const total = attachmentsArray.length; const statusList: { attachment: (typeof attachmentsArray)[number]; status: UploadStatus; url?: string; }[] = attachmentsArray.map((att) => ({ attachment: att, status: "queued", })); const formatSize = (size: number) => { if (size < 1024) return `${size} B`; if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`; if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(2)} MB`; return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`; }; const renderBoard = () => { const list = statusList .map((item) => { const name = item.attachment.name ?? "unknown"; const emoji = (() => { switch (item.status) { case "done": return ":white_check_mark:"; case "uploading": return ":hourglass_flowing_sand:"; case "queued": return ":black_large_square:"; case "failed": return ":x:"; } })(); if (item.status === "done" && item.url) return `${emoji} [${name}](<${item.url}>) (${formatSize(item.attachment.size)})`; return `${emoji} ${name} (${formatSize(item.attachment.size)})`; }) .join("\n"); return list; }; let statusMessage: Message | null = null; if (message.channel.isSendable()) { statusMessage = await message.reply({ content: `**Attachment upload status**\n${renderBoard()}`, }); } for (let i = 0; i < total; i++) { const { attachment } = statusList[i]; if (!statusMessage) continue; try { statusList[i].status = "uploading"; await statusMessage.edit( `**Attachment upload status**\n${renderBoard()}`, ); const key = `${ticket.anonymousId}/${savedMessage.id}/${nanoid( 8, )}_${attachment.name}`; const file = s3.file(key); const resp = await fetch(attachment.url); const blob = await resp.blob(); await file.write(blob); const publicUrl = getPublicUrl(key); await db.insert(ticketAttachments).values({ id: attachment.id, ticketId: ticket.id, messageId: savedMessage.id, fileName: attachment.name, contentType: attachment.contentType ?? "application/octet-stream", size: attachment.size, bucket: config.data.s3.bucket, key: key, url: attachment.url, }); statusList[i].status = "done"; statusList[i].url = publicUrl; await statusMessage.edit( `**Attachment upload status**\n${renderBoard()}`, ); } catch (error) { logger.error( error, `Failed to upload attachment ${attachment.name}`, ); statusList[i].status = "failed"; await statusMessage.edit( `**Attachment upload status**\n${renderBoard()}`, ); } } } } catch (error) { logger.error(error, "Failed to log ticket message"); } }, };