import { option } from "@purrkit/router"; import { Result } from "@sapphire/result"; import { ButtonBuilder, ContainerBuilder, TextDisplayBuilder, ActionRowBuilder } from "discord.js"; import { getTicketByChannelId, getTicketByGuild, getTicketByUser } from "@stealth-developers/db"; import { errorMessage, getTextChannel, handleError, logger, PublicError } from "@/lib"; import { hasAccessToTicket } from "@/lib/tickets"; import { useGetData } from "@/middleware"; import { TicketButtonPresets } from "./buttons"; import * as actions from "./actions"; import { manageConfig } from "./config"; import { TicketModalPresets } from "./modals"; const useGetTicketData = useGetData.use(async (interaction, { guild }) => { if (interaction.isChatInputCommand()) { const ticketId = interaction.options.getNumber("ticket", false); if (ticketId && guild) { const ticket = await getTicketByGuild(guild.id, ticketId); if (!ticket) return {}; return { ticket }; } } if (!interaction.channel) return {}; const ticket = await getTicketByChannelId(interaction.channel.id); if (!ticket) return {}; if (!ticket.channelId) return { ticket }; const ticketChannel = await getTextChannel(ticket.channelId, true); return { ticket, ticketChannel }; }); const ticketCommand = useGetTicketData.command("ticket", { description: "Ticket-related commands", }); ticketCommand.subcommand("new", { description: "Open a new ticket.", async run(interaction) { if (!interaction.guild) return errorMessage(interaction, "You must run this command in a guild"); const func = actions.createTicket(interaction.member ?? interaction.user, interaction.guild); const ticketResult = await Result.fromAsync(async () => func); if (ticketResult.isErr()) { const error = ticketResult.unwrapErr(); if (error instanceof PublicError) { return errorMessage(interaction, error.message); } else { console.error(error); return errorMessage(interaction, "An unexpected error occurred while creating the ticket."); } } const { channelId } = ticketResult.unwrap(); return interaction.reply({ content: `Opened ticket <#${channelId}>.`, flags: ["Ephemeral"], }); }, }); ticketCommand.subcommand("for", { description: "Open a new ticket on behalf of someone else", options: { user: option.user("The user to open the ticket for", { required: true }), reason: option.string("The reason for opening the ticket"), }, async run(interaction, args) { if (!interaction.guild) return errorMessage(interaction, "You must run this command in a guild"); const func = actions.createTicket(interaction.member ?? interaction.user, interaction.guild, { for: args.user, reason: args.reason, }); const ticketResult = await Result.fromAsync(async () => func); if (ticketResult.isErr()) { const error = ticketResult.unwrapErr(); if (error instanceof PublicError) { return errorMessage(interaction, error.message); } else { logger.error(error); return errorMessage(interaction, "An unexpected error occurred while creating the ticket."); } } const { channelId } = ticketResult.unwrap(); return interaction.reply({ content: `Opened ticket <#${channelId}>.`, flags: ["Ephemeral"], }); }, }); ticketCommand.subcommand("view", { description: "View a ticket", options: { ticket: option.number("The ticket's ID", { required: true }), }, async run(interaction, args, { guild, actor, user }) { if (guild && actor) { const ticket = await getTicketByGuild(guild.id, args.ticket); if (!ticket) return errorMessage(interaction, `Ticket with ID ${args.ticket} not found.`); if (!hasAccessToTicket(actor, guild, ticket)) return; const container = actions.getTicketContainer(ticket, !actor.moderator); await interaction.reply({ components: [container], flags: ["Ephemeral", "IsComponentsV2"] }); return; } if (user) { const ticket = await getTicketByUser(user.id, args.ticket); if (!ticket) return errorMessage(interaction, `Ticket with ID ${args.ticket} not found.`); const container = actions.getTicketContainer(ticket, false); await interaction.reply({ components: [container], flags: ["Ephemeral", "IsComponentsV2"] }); return; } await errorMessage(interaction, "We couldn't find a ticket with that ID."); }, }); ticketCommand.group( "actions", { description: "Perform ticket actions", }, (group) => { group.subcommand("close", { description: "Close the ticket", async run(interaction) { if (!interaction.guild) return errorMessage(interaction, "This command can only be used in a server."); const modal = await actions.triggerCloseTicket( interaction.user, interaction.guild, interaction.channelId, ); await interaction.showModal(modal); }, }); group.subcommand("reopen", { description: "Reopen a ticket", async run(interaction, _, { actor, ticket, ticketChannel, guild }) { if (!ticket || !ticketChannel || !actor || !guild) return errorMessage(interaction, "Failed to find ticket"); const result = await Result.fromAsync( actions.reopenTicket({ ticket, ticketChannel, actor, guild }), ); if (result.isErr()) return handleError(interaction, result.unwrapErr()); await interaction.reply({ content: `Ticket has been reopened.`, flags: ["Ephemeral"], }); }, }); group.subcommand("claim", { description: "Claim the ticket", async run(interaction, _, { actor, ticket, ticketChannel, guild }) { if (!ticket || !ticketChannel || !actor || !guild) return errorMessage(interaction, "Failed to find ticket"); const result = await Result.fromAsync( actions.claimTicket({ ticket, ticketChannel, actor, guild }), ); if (result.isErr()) return handleError(interaction, result.unwrapErr()); await interaction.reply({ content: "Ticket claimed!", flags: ["Ephemeral"], }); }, }); group.subcommand("unclaim", { description: "Unclaim a ticket", async run(interaction, _, { actor, ticket, ticketChannel, guild }) { if (!ticket || !ticketChannel || !actor || !guild) return errorMessage(interaction, "Failed to find ticket"); const result = await Result.fromAsync( actions.unclaimTicket({ ticket, ticketChannel, actor, guild }), ); if (result.isErr()) return handleError(interaction, result.unwrapErr()); await interaction.reply({ content: `You have unclaimed ticket #${ticket.localId}.`, flags: ["Ephemeral"], }); }, }); group.subcommand("comment", { description: "Add a comment to the ticket", options: { ticket: option.number("The ID of the ticket", { required: false }), comment: option.string("The comment to add", { required: false }), }, async run(interaction, { comment }, { actor, ticket, ticketChannel, guild }) { if (!ticket || !ticketChannel || !actor || !guild) return errorMessage(interaction, "Failed to find ticket"); if (!actor.moderator) return errorMessage(interaction, "You must be a moderator to use this command"); if (comment) { const result = await Result.fromAsync( actions.addComment({ actor, ticket, guild }, comment), ); if (result.isErr()) return handleError(interaction, result.unwrapErr()); await interaction.reply({ content: `Comment added to ticket.`, flags: ["Ephemeral"], }); return; } await interaction.showModal(TicketModalPresets.comment(ticket.id)); }, }); group.subcommand("edit", { description: "Edit the reasons for closing a ticket", options: { ticket: option.number("The ticket to edit", { required: false }), }, async run(interaction, _, { actor, ticket }) { if (!actor) { return errorMessage(interaction, "This command can only be used in a server."); } else if (!actor.moderator) { return errorMessage(interaction, "You do not have permission to use this command."); } else if (!ticket) { return errorMessage(interaction, "Ticket not found."); } await interaction.showModal( TicketModalPresets.edit(ticket.id, { public: ticket.closeReason, private: ticket.privateReason, }), ); }, }); // TODO)) clean }, ); ticketCommand.group( "manage", { description: "Manage ticket settings", }, (group) => { // channels & categories group.subcommand("transcript-channel", { description: "Set the channel where transcripts will be sent", options: { channel: option.channel("The channel to send transcripts to", { required: true }), }, async run(interaction, _, { actor }) { if (!actor?.moderator) return errorMessage(interaction, "You must be a moderator to run this command"); await manageConfig(interaction); }, }); group.subcommand("entrypoint-channel", { description: "Set the channel where the ticket prompt will be sent", options: { channel: option.channel("The channel to send the ticket prompt to", { required: true }), }, async run(interaction, _, { actor }) { if (!actor?.moderator) return errorMessage(interaction, "You must be a moderator to run this command"); await manageConfig(interaction); }, }); group.subcommand("category", { description: "Sets the category for new tickets", options: { category: option.channel("The category to create new tickets in", { required: true }), }, async run(interaction, _, { actor }) { if (!actor?.moderator) return errorMessage(interaction, "You must be a moderator to run this command"); await manageConfig(interaction); }, }); // text group.subcommand("prompt", { description: "Set the message sent in the entry point channel", async run(interaction, _, { actor }) { if (!actor?.moderator) return errorMessage(interaction, "You must be a moderator to run this command"); await manageConfig(interaction); }, }); group.subcommand("greeting", { description: "Set the message sent in new tickets", async run(interaction, _, { actor }) { if (!actor?.moderator) return errorMessage(interaction, "You must be a moderator to run this command"); await manageConfig(interaction); }, }); // actions group.subcommand("send-prompt", { description: "Send the prompt to the entry point channel", async run(interaction, _, { guild, actor }) { if (!interaction.guild || !guild) return errorMessage(interaction, "You must run this command in a guild"); if (!actor?.moderator) return errorMessage(interaction, "You must be a moderator to run this command"); const commandString = (sub: string) => ``; const channelId = guild.ticketPromptChannel; const promptText = guild.ticketPrompt; if (!channelId) return errorMessage( interaction, `Prompt chnanel not configured. Use ${commandString("entrypoint-channel")} to set it up.`, ); if (!promptText) return errorMessage( interaction, `Ticket prompt not configured. Use ${commandString("prompt")} to set it up.`, ); const channelRes = await Result.fromAsync(() => getTextChannel(channelId)); if (channelRes.isErr()) { const err = channelRes.unwrapErr(); const errorMessageText = err instanceof Error ? err.message : "Unknown error"; return errorMessage(interaction, `Failed to fetch prompt channel: ${errorMessageText}`); } const channel = channelRes.unwrap(); if (!channel.isSendable()) { return errorMessage( interaction, "I don't have permission to send messages in the prompt channel.", ); } const container = new ContainerBuilder() .addTextDisplayComponents(new TextDisplayBuilder().setContent(promptText)) .addActionRowComponents( new ActionRowBuilder().addComponents(TicketButtonPresets.create()), ); await channel.send({ flags: ["IsComponentsV2"], components: [container] }); return interaction.reply({ content: "Prompt sent!", flags: ["Ephemeral"], }); }, }); }, ); export default ticketCommand;