import { type ButtonInteraction, type ChatInputCommandInteraction, type Client, type ModalSubmitInteraction, SlashCommandBuilder, SlashCommandSubcommandBuilder, } from "discord.js"; const closeLock = new Map(); import { getGuild, getTicket } from "@/database/queries"; import { parseComponentId } from "@/utils/discord/components"; import { getTicketReasonModal } from "./_shared"; import { handleAddUser, handleClose, handleCreate, handleDelete, handleReopen, handleThank, handleTranscript, } from "./actions"; import { handleExport, handleList, handleListPagination, handleViewTicket, } from "./gdpr"; import { handleReasonModal } from "./modals"; const commandData = new SlashCommandBuilder() .setName("ticket") .setDescription("Manage and create new tickets") // create .addSubcommand( new SlashCommandSubcommandBuilder() .setName("create") .setDescription("Create a new ticket"), ) // create-for .addSubcommand( new SlashCommandSubcommandBuilder() .setName("create-for") .setDescription("Create a new ticket for a specific user") .addUserOption((option) => option .setName("user") .setDescription("The user to create the ticket for") .setRequired(true), ) .addStringOption((option) => option .setName("reason") .setDescription("Reason for creating the ticket, shown to the user") .setRequired(false), ), ) // add .addSubcommand( new SlashCommandSubcommandBuilder() .setName("add") .setDescription("Add another user to the ticket") .addUserOption((option) => option .setName("user") .setDescription("The user to add") .setRequired(true), ), ) // close .addSubcommand( new SlashCommandSubcommandBuilder() .setName("close") .setDescription("Close the ticket in the current channel") .addStringOption((option) => option .setName("reason") .setDescription("Reason for closing the ticket, shown to the user") .setRequired(false), ) .addStringOption((option) => option .setName("private_reason") .setDescription( "Private reason for closing the ticket, shown only to staff", ) .setRequired(false), ), ) // reopen .addSubcommand( new SlashCommandSubcommandBuilder() .setName("reopen") .setDescription("Reopen the ticket in the current channel"), ) // delete .addSubcommand( new SlashCommandSubcommandBuilder() .setName("delete") .setDescription("Delete the ticket in the current channel"), ) // transcript .addSubcommand( new SlashCommandSubcommandBuilder() .setName("transcript") .setDescription( "Get the transcript of the ticket in the current channel", ), ) // list .addSubcommand( new SlashCommandSubcommandBuilder() .setName("list") .setDescription("Lists all of your current and past tickets"), ) // view .addSubcommand( new SlashCommandSubcommandBuilder() .setName("view") .setDescription("View details about a specific ticket") .addIntegerOption((option) => option .setName("ticket_id") .setDescription("The ID of the ticket you want to view") .setRequired(true), ), ) // export .addSubcommand( new SlashCommandSubcommandBuilder() .setName("export") .setDescription( "Export all of your ticket data in a machine readable format", ), ); async function execute( client: Client, interaction: ChatInputCommandInteraction, ) { const action = interaction.options.getSubcommand(); const actions = { create: handleCreate, "create-for": handleCreate, list: handleList, export: handleExport, view: handleViewTicket, }; if (action in actions) { await actions[action as keyof typeof actions](client, interaction); return; } if (!interaction.channelId || !interaction.guildId) { await interaction.reply({ content: "This command can only be used in a server.", flags: ["Ephemeral"], }); return; } const { data: guild } = await getGuild(interaction.guildId); const { data: ticket } = await getTicket( interaction.guildId, interaction.channelId, ); if (!ticket) { await interaction.reply({ content: "❌ This channel is not a valid ticket.", }); return; } if (action === "close") { const thirtySeconds = 30 * 1000; const lock = closeLock.get(ticket.id); if (lock && Date.now() < lock.expiresAt) { if (lock.by !== interaction.user.id) { interaction.reply({ content: `Someone else has a has a lock on this ticket, it expires .`, flags: ["Ephemeral"], }); return; } } const publicReason = interaction.options.getString("reason") ?? undefined; const privateReason = interaction.options.getString("private_reason") ?? undefined; closeLock.set(ticket.id, { expiresAt: Date.now() + thirtySeconds, by: interaction.user.id, }); if (interaction.channel?.isSendable()) { const message = interaction.channel.send({ content: `<@${interaction.user.id}> is closing this ticket...`, allowedMentions: { parse: [], }, }); setTimeout(async () => { (await message).delete().catch(() => {}); }, thirtySeconds); } if (publicReason || privateReason) { await interaction.deferReply({ flags: ["Ephemeral"] }); await handleClose(client, interaction, ticket, guild, { publicReason, privateReason, }); return; } return await interaction.showModal( getTicketReasonModal(interaction.user.id !== ticket.authorId), ); } await interaction.deferReply({ flags: ["Ephemeral"] }); if (action === "reopen") await handleReopen(client, interaction, ticket); else if (action === "delete") await handleDelete(client, interaction, ticket); else if (action === "transcript") await handleTranscript(client, interaction, ticket); else if (action === "add") await handleAddUser(client, interaction, ticket); else await interaction.editReply("❌ Invalid or unhandled action."); } async function buttonExecute(client: Client, interaction: ButtonInteraction) { const [action] = interaction.customId.split(":").slice(1); if (action === "thank") { await handleThank(client, interaction); return; } if (!interaction.channelId || !interaction.guildId) { await interaction.reply({ content: "This command can only be used in a server.", }); return; } if (action === "list") { await handleListPagination(client, interaction); return; } if (action === "create") { await handleCreate(client, interaction); return; } const { data: ticket } = await getTicket( interaction.guildId, interaction.channelId, ); if (!ticket) { await interaction.reply({ content: "❌ This channel is not a valid ticket.", }); return; } if (action === "close") return await interaction.showModal( getTicketReasonModal(interaction.user.id !== ticket.authorId), ); await interaction.deferReply({ flags: ["Ephemeral"] }); if (action === "reopen") await handleReopen(client, interaction, ticket); else if (action === "delete") await handleDelete(client, interaction, ticket); else if (action === "transcript") await handleTranscript(client, interaction, ticket); else await interaction.editReply("❌ Invalid or unhandled action."); } async function modalExecute( client: Client, interaction: ModalSubmitInteraction, ) { const components = parseComponentId(interaction.customId); const action = components.parts[0]; if (action === "reason") await handleReasonModal(client, interaction); } export default { data: commandData, execute, buttonExecute, modalExecute, };