all repos — stealth-developers @ ca32f225b75e833ec620793578abe42d1d3faba5

feat(bugs): impl buttons & editing
vi v@vt3e.cat
Thu, 26 Feb 2026 05:09:39 +0000
commit

ca32f225b75e833ec620793578abe42d1d3faba5

parent

06404f4204962483adf1a2e8d42c80fbe1d50390

M src/interactions/buttons/tickets/ticket.tssrc/interactions/buttons/tickets/ticket.ts

@@ -193,7 +193,6 @@ file: AttachmentBuilder,

_context: "closed" | "view" = "closed", isPublic = true, ) { - console.log(ticket.closedAt); const closedByReporter = ticket.closedBy === "reporter"; const closedByString = ticket.closedAt ? closedByReporter
M src/interactions/commands/bug/_shared.tssrc/interactions/commands/bug/_shared.ts

@@ -1,6 +1,10 @@

import config from "@/config"; -import { type Bug, attachments, db } from "@/database"; +import type { Bug } from "@/database"; +import { text, thumbnail } from "@/utils/components"; import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, type Client, ContainerBuilder, FileUploadBuilder,

@@ -65,37 +69,32 @@ ): Promise<ContainerBuilder | undefined> {

const project = PROJECT_MAP[bug.projects[0]]; if (!project) return undefined; - const text = { - body: new TextDisplayBuilder().setContent( - [`### ${bug.title}`, `${bug.description}`].join("\n"), - ), - footer: { - text: new TextDisplayBuilder().setContent( - [`-# #${bug.id}`, project.displayName, `Reported by <@${userId}>`].join( - " • ", - ), - ), - icon: project.iconURL - ? new ThumbnailBuilder().setURL(project.iconURL) - : undefined, - }, - }; + const { title, description, id } = bug; + const bodyText = text(`### ${title}\n${description}`); + const footerText = text( + [`-# #${id}`, project.displayName, `Reported by <@${userId}>`].join(" • "), + ); + const icon = thumbnail(project.iconURL); - const section = new SectionBuilder(); - if (text.footer.icon) section.setThumbnailAccessory(text.footer.icon); - section.addTextDisplayComponents(text.body); + const section = new SectionBuilder().addTextDisplayComponents(bodyText); + if (icon) section.setThumbnailAccessory(icon); - const media = await db - .select() - .from(attachments) - .where( - and(eq(attachments.ownerId, bug.id), eq(attachments.ownerType, "bug")), - ); - const urls = media.map((attachment) => attachment.url); + /* + TODO)) render media in containers + const media = await db + .select() + .from(attachments) + .where( + and(eq(attachments.ownerId, bug.id), eq(attachments.ownerType, "bug")), + ); + const urls = media.map((attachment) => attachment.url); + */ - section.addTextDisplayComponents(text.footer.text); - const container = new ContainerBuilder().addSectionComponents(section); - return container; + const buttons = await constructButtons(bug); + return new ContainerBuilder() + .addSectionComponents(section) + .addTextDisplayComponents(footerText) + .addActionRowComponents(buttons); } export function buildReportModal(bugId?: string, bug?: Bug) {

@@ -193,7 +192,7 @@ const message = await channel.messages.fetch(messageId);

const project = PROJECT_MAP[bug.projects[0]]; if (!project) return; - const container = await constructContainer(bug, message.author.id); + const container = await constructContainer(bug, bug.authorId); if (!container) return; await message.edit({

@@ -201,3 +200,31 @@ components: [container],

flags: ["IsComponentsV2"], }); } + +export async function constructButtons(bug: Bug) { + const row = new ActionRowBuilder<ButtonBuilder>(); + + const isOpen = bug.status === "open"; + const statusId = isOpen ? `bug:close:${bug.id}` : `bug:open:${bug.id}`; + const statusLabel = isOpen ? "Close" : "Reopen"; + + const statusToggle = new ButtonBuilder() + .setCustomId(statusId) + .setLabel(statusLabel) + .setStyle(isOpen ? ButtonStyle.Danger : ButtonStyle.Success); + const editButton = new ButtonBuilder() + .setCustomId(`bug:edit:${bug.id}`) + .setLabel("Edit") + .setStyle(ButtonStyle.Secondary); + const deleteButton = new ButtonBuilder() + .setCustomId(`bug:delete:${bug.id}`) + .setLabel("Delete") + .setStyle(ButtonStyle.Secondary); + const trelloButton = new ButtonBuilder() + .setCustomId(`bug:trello:${bug.id}`) + .setLabel("Trello") + .setStyle(ButtonStyle.Secondary); + + row.addComponents(statusToggle, editButton, deleteButton, trelloButton); + return row; +}
A src/interactions/commands/bug/buttons.ts

@@ -0,0 +1,228 @@

+import config from "@/config"; +import { bugs, db } from "@/database"; +import { getGuild } from "@/utils/queries"; +import type { ButtonInteraction, Client } from "discord.js"; +import { eq } from "drizzle-orm"; +import { buildReportModal, updateBugEmbed } from "./_shared"; + +async function fetchBugById(id: string) { + const parsedId = Number(id); + if (Number.isNaN(parsedId)) return null; + const results = await db.select().from(bugs).where(eq(bugs.id, parsedId)); + return results[0] ?? null; +} + +export async function handleCloseButton( + client: Client, + interaction: ButtonInteraction, +) { + if (!interaction.guildId) return; + + const [, , id] = interaction.customId.split(":"); + const bug = await fetchBugById(id); + if (!bug) { + await interaction.reply({ + content: "❌ Bug not found.", + flags: ["Ephemeral"], + }); + return; + } + + await db + .update(bugs) + .set({ status: "closed", closedAt: new Date(), updatedAt: new Date() }) + .where(eq(bugs.id, bug.id)); + const { data, exists } = await getGuild(interaction.guildId); + + if (!exists) { + await interaction.reply({ + content: "❌ Guild configuration not found.", + flags: ["Ephemeral"], + }); + return; + } + + const channelId = data.bug_channel_id; + if (!channelId) { + await interaction.reply({ + content: "❌ Bug channel not configured.", + flags: ["Ephemeral"], + }); + return; + } + + if (bug.messageId) { + await updateBugEmbed( + client, + { ...bug, status: "closed" }, + bug.messageId, + channelId, + ); + } + + await interaction.reply({ + content: `✅ Bug #${bug.bugNumber.toString().padStart(4, "0")} closed.`, + flags: ["Ephemeral"], + }); +} + +export async function handleOpenButton( + client: Client, + interaction: ButtonInteraction, +) { + if (!interaction.guildId) return; + + const [, , id] = interaction.customId.split(":"); + const bug = await fetchBugById(id); + + if (!bug) { + await interaction.reply({ + content: "❌ Bug not found.", + flags: ["Ephemeral"], + }); + return; + } + + await db + .update(bugs) + .set({ status: "open", closedAt: null, updatedAt: new Date() }) + .where(eq(bugs.id, bug.id)); + const { data, exists } = await getGuild(interaction.guildId); + + if (!exists) { + await interaction.reply({ + content: "❌ Guild configuration not found.", + flags: ["Ephemeral"], + }); + return; + } + + const channelId = data.bug_channel_id; + if (!channelId) { + await interaction.reply({ + content: "❌ Bug channel not configured.", + flags: ["Ephemeral"], + }); + return; + } + + if (bug.messageId) { + await updateBugEmbed( + client, + { ...bug, status: "open" }, + bug.messageId, + channelId, + ); + } + + await interaction.reply({ + content: `✅ Bug #${bug.bugNumber.toString().padStart(4, "0")} reopened.`, + flags: ["Ephemeral"], + }); +} + +export async function handleEditButton( + _client: Client, + interaction: ButtonInteraction, +) { + const [, , id] = interaction.customId.split(":"); + const bug = await fetchBugById(id); + + if (!bug) { + await interaction.reply({ + content: "❌ Bug not found.", + flags: ["Ephemeral"], + }); + return; + } + + const modal = buildReportModal(String(bug.id), bug); + await interaction.showModal(modal); +} + +export async function handleDeleteButton( + _client: Client, + interaction: ButtonInteraction, +) { + if (!interaction.guildId) return; + + const [, , id] = interaction.customId.split(":"); + const bug = await fetchBugById(id); + if (!bug) { + await interaction.reply({ + content: "❌ Bug not found.", + flags: ["Ephemeral"], + }); + return; + } + + await db.delete(bugs).where(eq(bugs.id, bug.id)); + const { data, exists } = await getGuild(interaction.guildId); + if (!exists) { + await interaction.reply({ + content: "❌ Guild configuration not found, bug removed from database.", + flags: ["Ephemeral"], + }); + return; + } + const channelId = data.bug_channel_id; + if (!channelId) { + await interaction.reply({ + content: + "✅ Bug removed from database. (No bug channel configured to remove message/thread)", + flags: ["Ephemeral"], + }); + return; + } + + try { + const channel = await interaction.guild?.channels.fetch(channelId); + if (channel?.isTextBased() && bug.messageId) { + const { message } = interaction; + const { thread } = message; + + if (message) { + if (thread) + await thread + .delete(`bug ${bug.bugNumber} deleted by <@${interaction.user.id}>`) + .catch(() => {}); + + await message.delete().catch(() => {}); + } + } + } catch (err) {} + + await interaction.reply({ + content: `✅ Bug #${bug.bugNumber.toString().padStart(4, "0")} deleted.`, + flags: ["Ephemeral"], + }); +} + +export async function handleTrelloButton( + _client: Client, + interaction: ButtonInteraction, +) { + const [, , id] = interaction.customId.split(":"); + const bug = await fetchBugById(id); + if (!bug) { + await interaction.reply({ + content: "❌ Bug not found.", + flags: ["Ephemeral"], + }); + return; + } + + const params = new URLSearchParams({ + name: bug.title, + url: interaction.message.url, + idBoard: config.trelloBoardId || "", + }); + + const url = new URL("https://trello.com/addCard"); + url.search = params.toString(); + + await interaction.reply({ + content: url.toString(), + flags: ["Ephemeral"], + }); +}
M src/interactions/commands/bug/index.tssrc/interactions/commands/bug/index.ts

@@ -6,7 +6,14 @@ type ModalSubmitInteraction,

SlashCommandBuilder, } from "discord.js"; import { buildReportModal } from "./_shared"; -import { handleNewBugModal } from "./modals"; +import { + handleCloseButton, + handleDeleteButton, + handleEditButton, + handleOpenButton, + handleTrelloButton, +} from "./buttons"; +import { handleEditModal, handleNewBugModal } from "./modals"; const commandData = new SlashCommandBuilder() .setName("bug")

@@ -54,16 +61,19 @@ const [, action] = interaction.customId.split(":");

switch (action) { case "close": - // await handleCloseButton(client, interaction); + await handleCloseButton(client, interaction); break; case "open": - // await handleOpenButton(client, interaction); + await handleOpenButton(client, interaction); break; case "edit": - // await handleEditButton(client, interaction); + await handleEditButton(client, interaction); break; case "delete": - // await handleDeleteButton(client, interaction); + await handleDeleteButton(client, interaction); + break; + case "trello": + await handleTrelloButton(client, interaction); break; default: await interaction.reply({

@@ -78,10 +88,9 @@ client: Client,

interaction: ModalSubmitInteraction, ) { const [, action] = interaction.customId.split(":"); - console.log(interaction.customId); switch (action) { case "edit": - // await handleEditModal(client, interaction); + await handleEditModal(client, interaction); break; case "report": await handleNewBugModal(client, interaction);
M src/interactions/commands/bug/modals.tssrc/interactions/commands/bug/modals.ts

@@ -7,6 +7,7 @@ INPUT_IDS,

MODAL_IDS, VALIDATION, constructContainer, + updateBugEmbed, } from "./_shared"; export async function handleNewBugModal(

@@ -68,7 +69,6 @@ const channel = await interaction.guild?.channels.fetch(channelId);

if (!channel || !channel.isSendable()) return; // send message - // TODO)) buttons const message = await channel.send({ components: [container], flags: ["IsComponentsV2"],

@@ -99,3 +99,93 @@ interaction.editReply(

`Bug #${paddedNumber} created successfully! Find it here: ${message.url}`, ); } + +export async function handleEditModal( + client: Client, + interaction: ModalSubmitInteraction, +) { + if (!interaction.guildId) return; + + const parts = interaction.customId.split(":"); + const id = parts[2]; + 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(interaction.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"], + }); +}
M src/utils/components.tssrc/utils/components.ts

@@ -1,3 +1,5 @@

+import { TextDisplayBuilder, ThumbnailBuilder } from "discord.js"; + export function makeComponentId( cmd: unknown, ...parts: Array<string | number>

@@ -17,3 +19,10 @@ export function parseComponentId(customId: string) {

const [commandName, ...parts] = customId.split(":"); return { commandName, parts }; } + +export function text(content: string) { + return new TextDisplayBuilder().setContent(content); +} +export function thumbnail(url?: string) { + return url ? new ThumbnailBuilder().setURL(url) : undefined; +}