all repos — stealth-developers @ 5f45aba7d2730b77b4ef22cb42151f1eee375e70

feat(tickets): /manage-ticket command
vi v@vt3e.cat
Thu, 26 Feb 2026 21:51:49 +0000
commit

5f45aba7d2730b77b4ef22cb42151f1eee375e70

parent

ca32f225b75e833ec620793578abe42d1d3faba5

3 files changed, 225 insertions(+), 193 deletions(-)

jump to
A src/interactions/buttons/tickets/manage.ts

@@ -0,0 +1,224 @@

+import { db, guild as guildTable } from "@/database"; +import { getTicketMessage } from "@/utils/queries"; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + type ChatInputCommandInteraction, + type Client, + ContainerBuilder, + type GuildMember, + LabelBuilder, + ModalBuilder, + type ModalSubmitInteraction, + PermissionFlagsBits, + SlashCommandBuilder, + TextDisplayBuilder, + TextInputBuilder, + TextInputStyle, +} from "discord.js"; + +import { makeComponentId } from "@/utils/components"; +import { formatMessage } from "@/utils/formatting"; +import { loggers } from "@/utils/logging"; +import { hasManagerPermissions } from "@/utils/permissions"; +import { eq } from "drizzle-orm"; + +const logger = loggers.interactions.child({ name: "messageCommand" }); + +const commandData = new SlashCommandBuilder() + .setName("manage-tickets") + .setDescription("Manage aspects of the ticket system") + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) + + .addSubcommand((subcommand) => + subcommand + .setName("prompt") + .setDescription("Send the prompt to the designated channel") + .addChannelOption((c) => + c + .setName("channel") + .setDescription("The channel to send the prompt in") + .setRequired(true), + ), + ) + .addSubcommand((subcommand) => + subcommand + .setName("message") + .setDescription( + "Edit the saved ticket message (shown when a ticket is created)", + ), + ); + +const MODAL_ID = makeComponentId(commandData, "message"); +const INPUT_ID = "message_content"; + +async function execute( + _client: Client, + interaction: ChatInputCommandInteraction, +) { + if (!interaction.guild || !interaction.member) { + await interaction.reply({ + content: "❌ This command can only be used in a server.", + flags: ["Ephemeral"], + }); + return; + } + + const hasPerms = await hasManagerPermissions( + interaction.member as GuildMember, + ); + if (!hasPerms) { + await interaction.reply({ + content: "❌ You don't have permission to use this command.", + flags: ["Ephemeral"], + }); + return; + } + + const sub = interaction.options.getSubcommand(); + + if (sub === "prompt") { + const targetChannel = interaction.options.getChannel("channel", true); + + const textInput = new LabelBuilder() + .setLabel("Message Content") + .setDescription( + "This message will be followed by a button to create a new ticket.", + ) + .setTextInputComponent( + new TextInputBuilder() + .setCustomId(INPUT_ID) + .setStyle(TextInputStyle.Paragraph) + .setRequired(true), + ); + + await interaction.showModal( + new ModalBuilder() + .setCustomId(`${MODAL_ID}:${targetChannel.id}`) + .setTitle("Ticket Prompt") + .setLabelComponents(textInput), + ); + return; + } + + if (sub === "message") { + const { data: currentMessage } = await getTicketMessage( + interaction.guild.id, + ); + + const textInput = new LabelBuilder() + .setLabel("Message Content") + .setDescription( + "This message will be shown to users when they create a ticket.", + ) + .setTextInputComponent( + new TextInputBuilder() + .setCustomId(INPUT_ID) + .setStyle(TextInputStyle.Paragraph) + .setRequired(true) + .setValue(currentMessage ?? ""), + ); + + await interaction.showModal( + new ModalBuilder() + .setCustomId(MODAL_ID) + .setTitle("Edit Ticket Message") + .setLabelComponents(textInput), + ); + return; + } +} + +async function modalExecute( + _client: Client, + interaction: ModalSubmitInteraction, +) { + await interaction.deferReply({ flags: ["Ephemeral"] }); + if (!interaction.channelId || !interaction.guild || !interaction.guildId) { + await interaction.editReply({ + content: "This command can only be used in a server.", + }); + return; + } + + const parts = interaction.customId.split(":"); + const channelId = parts[2]; + + const messageContent = interaction.fields.getTextInputValue(INPUT_ID); + + if (channelId) { + const channel = interaction.guild.channels.cache.get(channelId); + if (!channel) { + await interaction.editReply({ + content: "❌ The channel specified is invalid.", + }); + return; + } + + if (!channel.isSendable()) { + await interaction.editReply({ + content: "❌ I'm not able to send messages in the specified channel.", + }); + return; + } + + const formatted = formatMessage(messageContent, interaction); + + const controls = new ActionRowBuilder<ButtonBuilder>().addComponents( + new ButtonBuilder() + .setCustomId("ticket:create") + .setLabel("Open a Ticket") + .setEmoji("📩") + .setStyle(ButtonStyle.Success), + ); + + const container = new ContainerBuilder() + .addTextDisplayComponents(new TextDisplayBuilder().setContent(formatted)) + .addActionRowComponents(controls); + + const sent = await channel + .send({ components: [container], flags: ["IsComponentsV2"] }) + .catch((err) => { + logger.error({ err, channelId }, "Failed to send ticket message"); + return null; + }); + + if (!sent) { + await interaction.editReply({ + content: "❌ Failed to send the message in the specified channel.", + }); + return; + } + + await interaction.editReply({ + content: `✅ Message sent successfully! ${sent.url}`, + allowedMentions: { + users: [], + }, + }); + return; + } + + try { + await db + .update(guildTable) + .set({ ticket_message: messageContent }) + .where(eq(guildTable.guildId, interaction.guildId)); + + await interaction.editReply({ + content: "✅ Ticket message updated successfully!", + }); + } catch (err) { + logger.error(err, "Failed to update ticket message in DB"); + await interaction.editReply({ + content: "❌ Failed to update ticket message.", + }); + } +} + +export default { + data: commandData, + execute, + modalExecute, +};
D src/interactions/buttons/tickets/message.ts

@@ -1,151 +0,0 @@

-import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - type ChatInputCommandInteraction, - type Client, - ContainerBuilder, - type GuildMember, - LabelBuilder, - ModalBuilder, - type ModalSubmitInteraction, - SlashCommandBuilder, - TextDisplayBuilder, - TextInputBuilder, - TextInputStyle, -} from "discord.js"; - -import { makeComponentId } from "@/utils/components"; -import { formatMessage } from "@/utils/formatting"; -import { loggers } from "@/utils/logging"; -import { hasManagerPermissions } from "@/utils/permissions"; -import { getGuild } from "@/utils/queries"; - -const logger = loggers.interactions.child({ name: "messageCommand" }); - -const commandData = new SlashCommandBuilder() - .setName("ticket-message") - .setDescription("Send the ticket message in the designated channel") - .addChannelOption((c) => - c - .setName("channel") - .setDescription("The channel to send the ticket message in") - .setRequired(true), - ); - -const MODAL_ID = makeComponentId(commandData, "message"); -const INPUT_ID = "message_content"; - -async function execute( - _client: Client, - interaction: ChatInputCommandInteraction, -) { - if (!interaction.guild || !interaction.member) { - await interaction.reply({ - content: "❌ This command can only be used in a server.", - flags: ["Ephemeral"], - }); - return; - } - - const hasPerms = await hasManagerPermissions( - interaction.member as GuildMember, - ); - if (!hasPerms) { - await interaction.reply({ - content: "❌ You don't have permission to use this command.", - flags: ["Ephemeral"], - }); - return; - } - - const targetChannel = interaction.options.getChannel("channel", true); - - // construct & show the modal - const textInput = new LabelBuilder() - .setLabel("Message Content") - .setTextInputComponent( - new TextInputBuilder() - .setCustomId(INPUT_ID) - .setStyle(TextInputStyle.Paragraph) - .setRequired(true), - ); - - await interaction.showModal( - new ModalBuilder() - .setCustomId(`${MODAL_ID}:${targetChannel.id}`) - .setTitle("Ticket Message") - .setLabelComponents(textInput), - ); -} - -async function modalExecute( - _client: Client, - interaction: ModalSubmitInteraction, -) { - if (!interaction.guild || !interaction.member) return; - - const channelId = interaction.customId.split(":")[2]; - - const { data: guildData } = await getGuild(interaction.guild.id); - const channel = interaction.guild.channels.cache.get(channelId); - if (!channel) { - await interaction.reply({ - content: "❌ The channel specified is invalid.", - flags: ["Ephemeral"], - }); - return; - } - - if (!channel.isSendable()) { - await interaction.reply({ - content: "❌ I'm not able to send messages in the specified channel.", - flags: ["Ephemeral"], - }); - return; - } - - const message = interaction.fields.getTextInputValue(INPUT_ID); - const formatted = formatMessage(message, interaction); - - const controls = new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder() - .setCustomId("ticket:create") - .setLabel("Open a Ticket") - .setEmoji("📩") - .setStyle(ButtonStyle.Success), - ); - - const container = new ContainerBuilder() - .addTextDisplayComponents(new TextDisplayBuilder().setContent(formatted)) - .addActionRowComponents(controls); - - const sent = await channel - .send({ components: [container], flags: ["IsComponentsV2"] }) - .catch((err) => { - logger.error({ err, channelId }, "Failed to send ticket message"); - return null; - }); - - if (!sent) { - await interaction.reply({ - content: "❌ Failed to send the message in the specified channel.", - flags: ["Ephemeral"], - }); - return; - } - - await interaction.reply({ - content: `✅ Message sent successfully! ${sent.url}`, - flags: ["Ephemeral"], - allowedMentions: { - users: [], - }, - }); -} - -export default { - data: commandData, - execute, - modalExecute, -};
M src/interactions/buttons/tickets/ticket.tssrc/interactions/buttons/tickets/ticket.ts

@@ -40,7 +40,6 @@ getGuild,

getManagerRoleIds, getTicket, getTicketByNumericId, - getTicketMessage, } from "@/utils/queries"; import { getPublicUrl } from "@/utils/s3"; import { generateTranscript } from "@/utils/transcripts";

@@ -122,30 +121,6 @@ message: makeComponentId(commandData, "message"),

reason: makeComponentId(commandData, "reason"), }; -async function getTicketMessageModal(guildId: string) { - const { data: message } = await getTicketMessage(guildId); - - const modal = new ModalBuilder() - .setCustomId(MODAL_IDS.message) - .setTitle("Edit Report Message"); - - const label = new LabelBuilder() - .setLabel("Report Message Content") - .setDescription("This will be the message shown to first time reporters.") - .setTextInputComponent( - new TextInputBuilder() - .setCustomId("ticket:input") - .setPlaceholder("meow meow :3") - .setStyle(TextInputStyle.Paragraph) - .setMaxLength(2000) - .setRequired(true) - .setValue(message ?? ""), - ); - - modal.addLabelComponents(label); - return modal; -} - function getTicketReasonModal(staff = false) { const modal = new ModalBuilder() .setCustomId(MODAL_IDS.reason)

@@ -297,11 +272,6 @@ });

return; } - if (action === "message") { - const modal = await getTicketMessageModal(interaction.guildId); - await interaction.showModal(modal); - return; - } if (action === "create") { await handleCreate(client, interaction); return;

@@ -417,18 +387,7 @@

const components = parseComponentId(interaction.customId); const action = components.parts[0]; - if (action === "message") { - const message = interaction.fields.getTextInputValue("ticket:input"); - - await db - .update(guild) - .set({ ticket_message: message }) - .where(eq(guild.guildId, interaction.guildId)); - - await interaction.editReply({ - content: "Ticket message updated successfully!", - }); - } else if (action === "reason") { + if (action === "reason") { const publicReason = interaction.fields.getTextInputValue( "ticket:reason_input", );