import { type Ticket, db, guild, tickets } from "@/database"; import type { ICommand } from "@/types"; import { formatMessage } from "@/utils/formatting"; import { countTickets, getGuild, getManagerRoleIds, getTicket, getTicketMessage, } from "@/utils/queries"; import { ActionRowBuilder, ButtonBuilder, type ButtonInteraction, ButtonStyle, ChannelType, type ChatInputCommandInteraction, type Client, ContainerBuilder, EmbedBuilder, type Interaction, LabelBuilder, type ModalSubmitInteraction, PermissionFlagsBits, SectionBuilder, SlashCommandBuilder, SlashCommandSubcommandBuilder, TextDisplayBuilder, TextInputBuilder, TextInputStyle, } from "discord.js"; import { ModalBuilder } from "discord.js"; import { eq } from "drizzle-orm"; const commandData = new SlashCommandBuilder() .setName("ticket") .setDescription("Manage and create new tickets") .addSubcommand( new SlashCommandSubcommandBuilder() .setName("create") .setDescription("Create a new ticket"), ) .addSubcommand( new SlashCommandSubcommandBuilder() .setName("message") .setDescription("Set the initial message sent in new tickets"), ) .addSubcommand( new SlashCommandSubcommandBuilder() .setName("close") .setDescription("Close the ticket in the current channel"), ) .addSubcommand( new SlashCommandSubcommandBuilder() .setName("reopen") .setDescription("Reopen the ticket in the current channel"), ) .addSubcommand( new SlashCommandSubcommandBuilder() .setName("delete") .setDescription("Delete the ticket in the current channel"), ); async function getModal(guildId: string) { const { data: message } = await getTicketMessage(guildId); const modal = new ModalBuilder() .setCustomId("ticket: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; } // ==================================== // interaction handlers // ==================================== async function execute( client: Client, interaction: ChatInputCommandInteraction, ) { const action = interaction.options.getSubcommand(); if (!interaction.channelId || !interaction.guildId) { await interaction.reply({ content: "This command can only be used in a server.", }); return; } if (action === "message") { const modal = await getModal(interaction.guildId); await interaction.showModal(modal); return; } await interaction.deferReply({ flags: ["Ephemeral"] }); if (action === "create") await handleCreate(client, interaction); const ticket = await getTicket(interaction.guildId, interaction.channelId); } export async function buttonExecute( client: Client, interaction: ButtonInteraction, ) { await interaction.deferReply({ flags: ["Ephemeral"] }); const [action] = interaction.customId.split(":").slice(1); if (!interaction.channelId || !interaction.guildId) { await interaction.editReply({ content: "This command can only be used in a server.", }); return; } if (action === "create") await handleCreate(client, interaction); const ticket = await getTicket(interaction.guildId, interaction.channelId); } async function modalExecute( _client: Client, interaction: ModalSubmitInteraction, ) { await interaction.deferReply({ flags: ["Ephemeral"] }); if (!interaction.guildId) { await interaction.editReply({ content: "This command can only be used in a server.", }); return; } const action = interaction.customId.split(":")[1]; 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!", }); } } // ==================================== // action handlers // ==================================== async function handleCreate( client: Client, interaction: ChatInputCommandInteraction | ButtonInteraction, ) { if (!interaction.guild) throw new Error("Guild is not available"); if (!client.user) throw new Error("Client user is not available"); const { data: guild, exists } = await getGuild(interaction.guild.id); if (!exists) { await interaction.editReply({ content: "This server hasn't been setup yet.", }); return; } const anonymousId = crypto.randomUUID().split("-")[0]; const managerRoleIds = await getManagerRoleIds(interaction.guild.id); const ticketCount = await countTickets(guild.guildId); const paddedTicketCount = (ticketCount + 1).toString().padStart(3, "0"); const channel = await interaction.guild.channels.create({ name: `ticket-${paddedTicketCount}`, type: ChannelType.GuildText, parent: guild.ticket_category_id, permissionOverwrites: [ { id: interaction.guild.id, deny: [PermissionFlagsBits.ViewChannel], }, { id: interaction.user.id, allow: [ PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.AttachFiles, ], }, { id: client.user.id, allow: [ PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ManageChannels, ], }, ...managerRoleIds.map((roleId) => ({ id: roleId, allow: [ PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.AttachFiles, ], })), ], }); const _ticket = ( await db .insert(tickets) .values({ guildId: interaction.guild.id, channelId: channel.id, authorId: interaction.user.id, anonymousId: anonymousId, type: "general", status: "open", }) .returning() )[0]; const formatted = formatMessage(guild.ticket_message ?? "", interaction); const embed = new EmbedBuilder() .setColor("#ff5a00") .setTitle("Ticket created!") .setDescription(formatMessage(guild.ticket_message ?? "", interaction)) .setFooter({ text: `ref: ${anonymousId}` }); const controls = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("ticket:close") .setLabel("Close ticket") .setStyle(ButtonStyle.Danger) .setEmoji("🔏"), ); const container = new ContainerBuilder() .setAccentColor(0xff5a00) .addTextDisplayComponents( new TextDisplayBuilder().setContent("# Ticket Created!"), new TextDisplayBuilder().setContent(formatted), ) .addActionRowComponents(controls); await channel.send({ components: [container], flags: ["IsComponentsV2"], }); await interaction.editReply({ content: `:white_check_mark: Ticket created ${channel.toString()}`, }); } async function handleDelete( _client: Client, interaction: ButtonInteraction, ticket: Ticket, ) {} async function handleClose( _client: Client, interaction: ButtonInteraction, ticket: Ticket, ) {} async function handleReopen( _client: Client, interaction: ButtonInteraction, ticket: Ticket, ) {} export default { data: commandData, execute, buttonExecute, modalExecute, };