all repos — stealth-developers @ 22bd268c32fa05c64444c6abe2514e95c0ed99cd

wip: add button row to bug messaegs
willow hai@wlo.moe
Sun, 09 Nov 2025 17:20:09 +0000
commit

22bd268c32fa05c64444c6abe2514e95c0ed99cd

parent

369d0401f43636f29472867761e809bc6a73472c

A src/interactions/commands/bug/buttons.ts

@@ -0,0 +1,283 @@

+import config from "@/config.ts"; +import { BugModel, GuildModel, MediaModel } from "@/database/schemas.ts"; +import lily from "@/utils/logging.ts"; +import { hasManagerPermissions } from "@/utils/permissions"; +import { + ActionRowBuilder, + ButtonBuilder, + type ButtonInteraction, + ButtonStyle, + ChannelType, + type Client, + type GuildMember, +} from "discord.js"; + +const logger = lily.child("bugButtons"); + +export function buildTrelloUrl(title: string, url: string): string { + const params = new URLSearchParams({ + name: title, + url: url, + idBoard: config.data?.trelloBoardId || "", + }); + return `https://trello.com/addCard?${params.toString()}`; +} + +export function buildButtonRow( + bugId: number, + messageUrl: string, + bugTitle: string, + isClosed: boolean, +): ActionRowBuilder<ButtonBuilder> { + const row = new ActionRowBuilder<ButtonBuilder>(); + + if (isClosed) { + row.addComponents( + new ButtonBuilder() + .setCustomId(`bug:open:${bugId}`) + .setLabel("open") + .setStyle(ButtonStyle.Secondary), + ); + } else { + row.addComponents( + new ButtonBuilder() + .setCustomId(`bug:close:${bugId}`) + .setLabel("close") + .setStyle(ButtonStyle.Secondary), + ); + } + + row.addComponents( + new ButtonBuilder() + .setCustomId(`bug:edit:${bugId}`) + .setLabel("edit") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId(`bug:delete:${bugId}`) + .setLabel("delete") + .setStyle(ButtonStyle.Danger), + ); + + if (config.data.trelloBoardId) { + row.addComponents( + new ButtonBuilder() + .setLabel("add to trello") + .setStyle(ButtonStyle.Link) + .setURL(buildTrelloUrl(bugTitle, messageUrl)), + ); + } + + return row; +} + +async function canManageBug( + interaction: ButtonInteraction, + bugUserId: string, +): Promise<boolean> { + if (!interaction.guild || !interaction.member) return false; + if (interaction.user.id === bugUserId) return true; + + const guildData = await GuildModel.findOne({ + guild_id: interaction.guild.id, + }); + if (!guildData) return false; + + return await hasManagerPermissions(interaction.member as GuildMember); +} + +export async function handleCloseButton( + client: Client, + interaction: ButtonInteraction, +) { + const bugId = Number.parseInt(interaction.customId.split(":")[2]); + const bug = await BugModel.findOne({ bug_id: bugId }); + + if (!bug) { + await interaction.reply({ + content: "❌ Bug report not found.", + flags: ["Ephemeral"], + }); + return; + } + + const canManage = await canManageBug(interaction, bug.user_id); + if (!canManage) { + await interaction.reply({ + content: "❌ You don't have permission to close this bug report.", + flags: ["Ephemeral"], + }); + return; + } + + bug.status = "closed"; + await bug.save(); + + if (bug.message_id && bug.thread_id) { + const thread = await client.channels.fetch(bug.thread_id); + if (thread?.isThread()) { + const message = await thread.fetchStarterMessage(); + if (message) { + const buttonRow = buildButtonRow(bugId, message.url, bug.title, true); + await message.edit({ + components: [...message.components.slice(0, -1), buttonRow], + }); + } + } + } + + await interaction.reply({ + content: "✅ Bug report closed successfully.", + flags: ["Ephemeral"], + }); + + logger.info(`Bug #${bugId} closed by ${interaction.user.id}`); +} + +export async function handleOpenButton( + client: Client, + interaction: ButtonInteraction, +) { + const bugId = Number.parseInt(interaction.customId.split(":")[2]); + const bug = await BugModel.findOne({ bug_id: bugId }); + + if (!bug) { + await interaction.reply({ + content: "❌ Bug report not found.", + flags: ["Ephemeral"], + }); + return; + } + + const canManage = await canManageBug(interaction, bug.user_id); + if (!canManage) { + await interaction.reply({ + content: "❌ You don't have permission to reopen this bug report.", + flags: ["Ephemeral"], + }); + return; + } + + bug.status = "open"; + await bug.save(); + + if (bug.message_id && bug.thread_id) { + const thread = await client.channels.fetch(bug.thread_id); + if (thread?.isThread()) { + const message = await thread.fetchStarterMessage(); + if (message) { + const buttonRow = buildButtonRow(bugId, message.url, bug.title, false); + await message.edit({ + components: [...message.components.slice(0, -1), buttonRow], + }); + } + } + } + + await interaction.reply({ + content: "✅ Bug report reopened successfully.", + flags: ["Ephemeral"], + }); + + logger.info(`Bug #${bugId} reopened by ${interaction.user.id}`); +} + +export async function handleEditButton( + _client: Client, + interaction: ButtonInteraction, +) { + const bugId = Number.parseInt(interaction.customId.split(":")[2]); + const bug = await BugModel.findOne({ bug_id: bugId }); + + if (!bug) { + await interaction.reply({ + content: "❌ Bug report not found.", + flags: ["Ephemeral"], + }); + return; + } + + const canManage = await canManageBug(interaction, bug.user_id); + if (!canManage) { + await interaction.reply({ + content: "❌ You don't have permission to edit this bug report.", + flags: ["Ephemeral"], + }); + return; + } + + // TODO)) implement edit modal + await interaction.reply({ + content: "❌ Editing bugs is not implemented yet.", + flags: ["Ephemeral"], + }); +} + +export async function handleDeleteButton( + client: Client, + interaction: ButtonInteraction, +) { + const bugId = Number.parseInt(interaction.customId.split(":")[2]); + const bug = await BugModel.findOne({ bug_id: bugId }); + + if (!bug) { + await interaction.reply({ + content: "❌ Bug report not found.", + flags: ["Ephemeral"], + }); + return; + } + + const canManage = await canManageBug(interaction, bug.user_id); + if (!canManage) { + await interaction.reply({ + content: "❌ You don't have permission to delete this bug report.", + flags: ["Ephemeral"], + }); + return; + } + + await interaction.deferReply({ flags: ["Ephemeral"] }); + + try { + // delete associated media + const media = await MediaModel.find({ bug_id: bugId }); + if (media.length > 0) { + await MediaModel.deleteMany({ bug_id: bugId }); + logger.info(`deleted ${media.length} media files for bug #${bugId}`); + } + + // delete associated message & thread + if (bug.message_id && bug.thread_id) { + const thread = await client.channels.fetch(bug.thread_id); + if (thread?.type !== ChannelType.PublicThread) return; + + const message = await thread.fetchStarterMessage(); + if (message) await message.delete(); + + try { + await thread.delete( + `bug report #${bugId} deleted by ${interaction.user.username}`, + ); + } catch (threadError) { + logger.warn(`Failed to delete thread for bug #${bugId}:`, threadError); + } + } + + // tombstone + bug.status = "closed"; + bug.title = "[DELETED]"; + bug.description = "[DELETED]"; + await bug.save(); + + await interaction.editReply({ + content: "✅ Bug report deleted successfully.", + }); + + logger.info(`Bug #${bugId} deleted by ${interaction.user.id}`); + } catch (error) { + logger.error(`Failed to delete bug #${bugId}:`, error); + await interaction.editReply({ + content: "❌ Failed to delete bug report. Please try again later.", + }); + } +}
M src/interactions/commands/bug/index.tssrc/interactions/commands/bug/index.ts

@@ -1,7 +1,18 @@

import config from "@/config.ts"; import type { ICommand } from "@/types.ts"; -import { type ApplicationCommandData, SlashCommandBuilder } from "discord.js"; +import { + type ApplicationCommandData, + type ButtonInteraction, + type Client, + SlashCommandBuilder, +} from "discord.js"; import { buttonCommand } from "./button.ts"; +import { + handleCloseButton, + handleDeleteButton, + handleEditButton, + handleOpenButton, +} from "./buttons.ts"; import { reportCommand } from "./report.ts"; const commandData = new SlashCommandBuilder()

@@ -22,6 +33,30 @@ .setRequired(true),

), ); +async function buttonExecute(client: Client, interaction: ButtonInteraction) { + const [, action] = interaction.customId.split(":"); + + switch (action) { + case "close": + await handleCloseButton(client, interaction); + break; + case "open": + await handleOpenButton(client, interaction); + break; + case "edit": + await handleEditButton(client, interaction); + break; + case "delete": + await handleDeleteButton(client, interaction); + break; + default: + await interaction.reply({ + content: "❌ Unknown button action.", + flags: ["Ephemeral"], + }); + } +} + export default { data: commandData.toJSON() as ApplicationCommandData, execute: async (client, interaction) => {

@@ -44,6 +79,5 @@ });

} }, modalExecute: reportCommand.modalExecute, - // buttonExecute: reportCommand.buttonExecute, - // selectMenuExecute: reportCommand.selectMenuExecute, + buttonExecute, } satisfies ICommand;
M src/interactions/commands/bug/report.tssrc/interactions/commands/bug/report.ts

@@ -28,6 +28,8 @@ TextInputBuilder,

TextInputStyle, ThumbnailBuilder, } from "discord.js"; + +import { buildButtonRow } from "./buttons.ts"; import { PROJECT_MAP, logger as lily } from "./shared.ts"; const logger = lily.child("report");

@@ -63,15 +65,6 @@ totalSize: number;

} // utils -function buildTrelloUrl(title: string, url: string): string { - const params = new URLSearchParams({ - name: title, - url: url, - idBoard: config.data?.trelloBoardId || "", - }); - return `https://trello.com/addCard?${params.toString()}`; -} - function formatFileSize(bytes: number): string { return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; }

@@ -153,7 +146,7 @@ : { label: project.displayName, value: key },

) as ProjectChoice<T>[]; } -// Modal builders +// modal builders function buildReportModal(): ModalBuilder { const terminology = config.data.terminology; const capitalizedTerminology =

@@ -210,7 +203,7 @@ .setTitle("Report a Bug")

.setLabelComponents(gameLabel, titleLabel, descriptionLabel, mediaLabel); } -// Validation +// validation async function validateGuildSetup(interaction: ModalSubmitInteraction) { if (!interaction.guild) { throw new Error("This command can only be used in a server.");

@@ -413,12 +406,17 @@ "Use this space to discuss the bug, provide additional details, or ask questions.",

}); await thread.members.add(interaction.user.id); - // Update bug with message and thread IDs + // update bug with message and thread IDs, add buttons bug.message_id = message.id; bug.thread_id = thread.id; await bug.save(); - // Final success message + const buttonRow = buildButtonRow(bugId, message.url, title, false); + await message.edit({ + components: [container, buttonRow], + }); + + // final success message await interaction.editReply({ content: `✅ Bug report #${bugId} has been submitted successfully! Check <#${channel.id}> for your report.`, });
M src/utils/permissions.tssrc/utils/permissions.ts

@@ -1,8 +1,8 @@

-import type { APIGuildMember, GuildMember } from "discord.js"; +import type { GuildMember } from "discord.js"; import { GuildModel } from "../database/schemas.ts"; export async function hasManagerPermissions( - member: GuildMember | APIGuildMember, + member: GuildMember, ): Promise<boolean> { if ( member.permissions.has("ManageGuild") ||