import { bugs, db } from "@/database"; import { getGuild } from "@/database/queries"; import { parseComponentId } from "@/utils/discord/components"; import { AttachmentBuilder } from "discord.js"; import type { Client, ModalSubmitInteraction } from "discord.js"; import { eq, sql } from "drizzle-orm"; import { INPUT_IDS, VALIDATION, constructContainer, updateBugEmbed } from "./_shared"; export async function handleNewBugModal(_client: Client, interaction: ModalSubmitInteraction) { const guildId = interaction.guildId; if (!guildId) return; const title = interaction.fields.getTextInputValue(INPUT_IDS.TITLE); const description = interaction.fields.getTextInputValue(INPUT_IDS.DESCRIPTION); const affected = interaction.fields.getStringSelectValues(INPUT_IDS.AFFECTED); const media = interaction.fields.getUploadedFiles(INPUT_IDS.MEDIA); await interaction.deferReply({ flags: ["Ephemeral"] }); const bug = await db.transaction(async (tx) => { const [{ count }] = await tx .select({ count: sql`COUNT(*)`.mapWith(Number) }) .from(bugs) .where(eq(bugs.guildId, guildId)); const bugNumber = count + 1; return await tx .insert(bugs) .values({ guildId: guildId, bugNumber: bugNumber, authorId: interaction.user.id, status: "open", title: title, description: description, projects: [...affected], sent: false, messageId: null, threadId: null, createdAt: new Date(), updatedAt: new Date(), closedAt: null, }) .returning(); }); let attachment: AttachmentBuilder | undefined; if (media && media.size > 0) { const [_, _attachment] = Array.from(media)[0]; try { const fileUrl = _attachment.url; const fileName = _attachment.name; if (fileUrl) { const res = await fetch(fileUrl); if (res.ok) { const arrayBuffer = await res.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); attachment = new AttachmentBuilder(buffer, { name: fileName }); } } } catch (err) { console.error("error processing uploaded file:", err); } } const container = await constructContainer(bug[0], interaction.user.id, attachment); if (!container) return; const { data, exists } = await getGuild(guildId); if (!exists) return; const channelId = data.bug_channel_id; if (!channelId) return; const channel = await interaction.guild?.channels.fetch(channelId); if (!channel || !channel.isSendable()) return; const message = await channel.send({ components: [container], flags: ["IsComponentsV2"], ...(attachment ? { files: [attachment] } : {}), }); // thread function truncateTitle(title: string, maxLength: number): string { return title.length > maxLength ? `${title.substring(0, maxLength)}...` : title; } const paddedNumber = bug[0].bugNumber.toString().padStart(4, "0"); const threadName = `#${paddedNumber} ${truncateTitle(title, VALIDATION.THREAD_NAME_MAX_LENGTH)}`; const thread = await message.startThread({ name: threadName, autoArchiveDuration: 1440, }); // save & update user await db .update(bugs) .set({ messageId: message.id, threadId: thread.id, sent: true }) .where(eq(bugs.id, bug[0].id)); thread.send("Use this space to discuss the bug, provide additional details, or ask questions."); interaction.editReply(`Bug #${paddedNumber} created successfully! Find it here: ${message.url}`); } export async function handleEditModal(client: Client, interaction: ModalSubmitInteraction) { const guildId = interaction.guildId; if (!guildId) return; const { parts } = parseComponentId(interaction.customId); const id = parts[1]; // bug:edit:ID -> parts is ["edit", "ID"] if (!id) { await interaction.reply({ content: "❌ Could not determine bug id.", flags: ["Ephemeral"], }); return; } const title = interaction.fields.getTextInputValue(INPUT_IDS.TITLE); const description = interaction.fields.getTextInputValue(INPUT_IDS.DESCRIPTION); const affected = interaction.fields.getStringSelectValues(INPUT_IDS.AFFECTED); if ( title.length > VALIDATION.TITLE_MAX_LENGTH || description.length > VALIDATION.DESCRIPTION_MAX_LENGTH ) { await interaction.reply({ content: "❌ Input too long.", flags: ["Ephemeral"], }); return; } const parsedId = Number(id); if (Number.isNaN(parsedId)) { await interaction.reply({ content: "❌ Invalid bug id.", flags: ["Ephemeral"], }); return; } await db .update(bugs) .set({ title, description, projects: [...affected], updatedAt: new Date(), }) .where(eq(bugs.id, parsedId)); const [updated] = await db.select().from(bugs).where(eq(bugs.id, parsedId)); if (!updated) { await interaction.reply({ content: "❌ Failed to fetch updated bug.", flags: ["Ephemeral"], }); return; } const { data, exists } = await getGuild(guildId); if (!exists) { await interaction.reply({ content: "✅ Bug updated, but guild configuration missing for updating the public message.", flags: ["Ephemeral"], }); return; } const channelId = data.bug_channel_id; if (!channelId) { await interaction.reply({ content: "✅ Bug updated, but no bug channel configured to update message.", flags: ["Ephemeral"], }); return; } if (updated.messageId) { await updateBugEmbed(client, updated, updated.messageId, channelId); } await interaction.reply({ content: `✅ Bug #${updated.bugNumber.toString().padStart(4, "0")} updated.`, flags: ["Ephemeral"], }); }