import { option } from "@purrkit/router"; import { Result } from "@sapphire/result"; import { ButtonBuilder, ContainerBuilder, TextDisplayBuilder, ActionRowBuilder } from "discord.js"; import { addTicketComment, editTicket, getTicketByChannelId, getTicketByGuild, getTicketByUser, } from "@stealth-developers/db"; import { errorMessage, getTextChannel, logger, PublicError } from "@/lib"; import { TicketButtonPresets } from "./buttons"; import * as actions from "./actions"; import { manageConfig } from "./config"; import { useGetData } from "@/middleware"; import { hasAccessToTicket } from "@/lib/tickets"; import { TicketModalPresets } from "./modals"; import { ChannelType } from "discord-api-types/v9"; 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, guild, ticket, ticketChannel }) { if (!interaction.guild || !guild || !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, "This command can only be used in a ticket."); } await editTicket(ticket.id, { status: "open", closedAt: null, closedBy: null, claimedBy: actor.id, claimedAt: new Date(), lastClaimantActivityAt: new Date(), }); if (ticketChannel) { actions.messageChannel(ticketChannel, { content: `<@${interaction.user.id}> reopened this ticket.`, flags: ["SuppressNotifications"], }); if (ticketChannel.type === ChannelType.GuildText) { ticketChannel.edit({ name: actions.channelName(ticket.localId, "claimed", interaction.user.displayName), }); } } await interaction.reply({ content: `Ticket has been reopened.`, flags: ["Ephemeral"], }); }, }); group.subcommand("claim", { description: "Claim the ticket", async run(interaction, _, { actor, ticket, ticketChannel }) { if (!interaction.guild) { return errorMessage(interaction, "This command can only be used in a server."); } if (!ticket) { return errorMessage(interaction, "This command can only be used in a ticket."); } else if (!actor?.moderator) { return errorMessage(interaction, "You do not have permission to use this command."); } else if (ticket.status !== "open") { return errorMessage(interaction, "This ticket is not open."); } const lastClaimerActive = ticket.lastClaimantActivityAt ? new Date(ticket.lastClaimantActivityAt) : null; // if the last claimer was active in the last 15 minutes, don't allow claiming if (lastClaimerActive && lastClaimerActive.getTime() > Date.now() - 1000 * 60 * 15) { const timeAgo = Math.floor(lastClaimerActive.getTime() / 1000); return errorMessage( interaction, `This ticket was claimed by <@${ticket.claimedByActor?.discordId}> .`, ); } await editTicket(ticket.id, { claimedAt: new Date(), claimedBy: actor.id, lastClaimantActivityAt: new Date(), }); if (ticketChannel) { actions.messageChannel(ticketChannel, { content: `<@${interaction.user.id}> has claimed this ticket, they will be with you shortly.`, flags: ["SuppressNotifications"], }); if (ticketChannel.type === ChannelType.GuildText) { ticketChannel.edit({ name: actions.channelName(ticket.localId, "claimed", interaction.user.displayName), }); } } await interaction.reply({ content: "Ticket claimed!", flags: ["Ephemeral"], }); }, }); group.subcommand("unclaim", { description: "Unclaim a ticket", async run(interaction, _, { actor, ticket, ticketChannel }) { if (!interaction.guild) { return errorMessage(interaction, "This command can only be used in a server."); } if (!ticket) { return errorMessage(interaction, "This command can only be used in a ticket."); } else if (!actor?.moderator) { return errorMessage(interaction, "You do not have permission to use this command."); } else if (ticket.status !== "open") { return errorMessage(interaction, "This ticket is not open."); } if (ticket.claimedBy !== actor.id) { return errorMessage(interaction, "You have not claimed this ticket."); } await editTicket(ticket.id, { claimedAt: null, claimedBy: null, lastClaimantActivityAt: null, }); if (ticketChannel) { actions.messageChannel(ticketChannel, { content: `<@${interaction.user.id}> unclaimed this ticket, someone else will be with you shortly.`, flags: ["SuppressNotifications"], }); if (ticketChannel.type === ChannelType.GuildText) { ticketChannel.edit({ name: actions.channelName(ticket.localId, "open"), }); } } 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, guild, ticket }) { if (!interaction.guild || !guild || !actor) return errorMessage(interaction, "This command can only be used in a server."); if (!actor.moderator) return errorMessage(interaction, "You do not have permission to use this command."); if (!ticket) return errorMessage(interaction, "Ticket not found."); if (comment) { await addTicketComment({ actorId: actor.id, content: comment, ticketId: ticket.id, }); await interaction.reply({ content: `Comment added to ticket ${ticket.id}`, 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;