import { option } from "@purrkit/router"; import { Result } from "@sapphire/framework"; import db, { addTicketComment, editTicket, eq, getActorByDiscordIdAndGuildDiscordId, getGuildByDiscordId, getTicket, guilds, } from "@stealth-developers/db"; import { CheckboxBuilder, LabelBuilder, MessageFlags, ModalBuilder, TextInputBuilder, TextInputStyle, } from "discord.js"; import { kitten } from "@/client"; import { errorMessage, handleError, upsertUser } from "@/lib"; import { useGetTicketData } from "@/middleware"; import * as actions from "./actions"; import { hasAccessToTicket } from "./lib"; export const configTextModal = kitten.modal("config-text", { options: { key: option.string({ required: true }), }, run: async (interaction, { key }) => { const value = interaction.fields.getTextInputValue("text-value"); if (!interaction.guildId) return errorMessage(interaction, "This interaction can only be used in a server."); const guild = await getGuildByDiscordId(interaction.guildId); if (!guild) return errorMessage(interaction, "Failed to update configuration: Guild not found."); const [res] = await db .update(guilds) .set({ [key]: value }) .where(eq(guilds.id, guild.id)) .returning(); if (!res) return errorMessage(interaction, "Failed to update configuration."); return interaction.reply({ content: "Configuration updated successfully!", flags: [MessageFlags.Ephemeral], }); }, }); export const closeTicketModal = useGetTicketData.modal("close-ticket", { options: { ticketId: option.string({ required: true }), }, run: async (interaction, _, { guild, actor, ticket }) => { await interaction.deferReply({ flags: ["Ephemeral"] }); if (!interaction.guild) { return errorMessage(interaction, "You must run this command in a guild"); } else if (!ticket) { return errorMessage(interaction, "Failed to find the ticket"); } else if (!actor) { return errorMessage(interaction, "Failed to find your actor in the database"); } else if (!guild) { return errorMessage(interaction, "Failed to find the guild"); } if (!hasAccessToTicket(actor, guild, ticket)) { return errorMessage(interaction, "You do not have access to this ticket"); } const shouldDelete = actor.moderator ? interaction.fields.getCheckbox("delete") : false; const reasons = { public: interaction.fields.getTextInputValue("reason:public"), private: actor.moderator ? interaction.fields.getTextInputValue("reason:private") : undefined, }; const result = await Result.fromAsync( actions.closeTicket(actor, ticket.id, reasons, shouldDelete), ); if (result.isErr()) return handleError(interaction, result.unwrapErr(), "Failed to close ticket."); return interaction.followUp({ content: "Ticket closed successfully" }); }, }); export const editReasonModal = kitten.modal("edit-ticket", { options: { ticketId: option.string({ required: true }), }, run: async (interaction, { ticketId }) => { await interaction.deferReply({ flags: ["Ephemeral"] }); if (!interaction.guild) return errorMessage(interaction, "You must run this command in a guild"); const ticket = await getTicket(ticketId); if (!ticket) return errorMessage(interaction, "Ticket not found"); const user = await upsertUser(interaction.user, interaction.guild); if (!user) return errorMessage(interaction, "Failed to upsert user"); const reasons = { public: interaction.fields.getTextInputValue("reason:public") || null, private: interaction.fields.getTextInputValue("reason:private") || null, }; const result = await Result.fromAsync( editTicket(ticketId, { closeReason: reasons.public ? reasons.public : ticket.closeReason, privateReason: reasons.private ? reasons.private : ticket.privateReason, }), ); if (result.isErr()) return handleError(interaction, result.unwrapErr(), "Failed to edit ticket."); return interaction.followUp({ content: "Ticket edited successfully" }); }, }); export const commentModal = kitten.modal("m-comment-modal", { options: { ticketId: option.string({ required: true }), }, async run(interaction, { ticketId }) { if (!interaction.guild) return; const actor = await getActorByDiscordIdAndGuildDiscordId( interaction.user.id, interaction.guild.id, ); if (!actor) return interaction.reply({ content: "You are not a moderator", flags: ["Ephemeral"] }); await addTicketComment({ actorId: actor.id, content: interaction.fields.getTextInputValue("comment"), ticketId, }); await interaction.reply({ content: "Comment added successfully", flags: ["Ephemeral"] }); }, }); export const TicketModalPresets = { close: (id: string, isModerator: boolean) => { const description = isModerator ? "The reason for closing the ticket; this will be shared with the user" : "The reason for closing the ticket"; const reasonLabel = new LabelBuilder() .setLabel("Reason") .setDescription(description) .setTextInputComponent( new TextInputBuilder() .setStyle(TextInputStyle.Paragraph) .setCustomId("reason:public") .setRequired(true), ); const privateReasonLabel = new LabelBuilder() .setLabel("Private Reason") .setDescription("The reason for closing the ticket.") .setTextInputComponent( new TextInputBuilder() .setStyle(TextInputStyle.Paragraph) .setCustomId("reason:private") .setRequired(isModerator), ); const deleteCheckbox = new LabelBuilder() .setLabel("Delete ticket channel") .setDescription("Delete the ticket channel after closing") .setCheckboxComponent(new CheckboxBuilder().setCustomId("delete").setDefault(false)); const labels = isModerator ? [reasonLabel, privateReasonLabel] : [reasonLabel]; const modal = new ModalBuilder() .setCustomId(closeTicketModal.id({ ticketId: id })) .setTitle("Close ticket") .addLabelComponents(...labels); if (isModerator) modal.addLabelComponents(deleteCheckbox); return modal; }, edit: (id: string, reasons: { public?: string | null; private?: string | null }) => { const reasonLabel = new LabelBuilder() .setLabel("Reason") .setDescription("The reason for closing the ticket; this will be shared with the user") .setTextInputComponent( new TextInputBuilder() .setStyle(TextInputStyle.Paragraph) .setCustomId("reason:public") .setValue(reasons.public ?? "") .setRequired(false), ); const privateReasonLabel = new LabelBuilder() .setLabel("Private Reason") .setDescription("The reason for closing the ticket.") .setTextInputComponent( new TextInputBuilder() .setStyle(TextInputStyle.Paragraph) .setCustomId("reason:private") .setValue(reasons.private ?? "") .setRequired(false), ); const modal = new ModalBuilder() .setCustomId(editReasonModal.id({ ticketId: id })) .setTitle("Edit ticket") .addLabelComponents([reasonLabel, privateReasonLabel]); return modal; }, comment: (id: string) => { const commentLabel = new LabelBuilder() .setLabel("Comment") .setDescription("The comment to add to the ticket") .setTextInputComponent( new TextInputBuilder() .setStyle(TextInputStyle.Paragraph) .setCustomId("comment") .setRequired(true), ); const modal = new ModalBuilder() .setCustomId(commentModal.id({ ticketId: id })) .setTitle("Add comment") .addLabelComponents(commentLabel); return modal; }, }; export const TicketModals = [closeTicketModal, configTextModal, commentModal, editReasonModal];