all repos — stealth-developers @ 7b677753ab0e91d872164817e743c365192ce6a8

bot: button presets
vi did:web:vt3e.cat
Mon, 22 Jun 2026 02:03:08 +0100
commit

7b677753ab0e91d872164817e743c365192ce6a8

parent

e9cebecae99c7f4cd8ba9e4c42d7b1fb84782a53

A apps/bot/src/commands/tickets/README.md

@@ -0,0 +1,24 @@

+commands: + +- [ ] /ticket + - [x] new + - [x] for + - [ ] actions + - [ ] open + - [ ] close + - [ ] add + - [ ] claim + - [ ] unclaim + - [ ] delete + - [ ] view + - [ ] info + - [ ] transcript + - [ ] list + - [ ] export + - [x] manage + - [x] transcript-channel + - [x] entrypoint-channel + - [x] category + - [x] prompt + - [x] send-prompt + - [x] greeting
D apps/bot/src/commands/tickets/_index.ts

@@ -1,267 +0,0 @@

-import { Subcommand } from "@sapphire/plugin-subcommands"; -import { ApplyOptions } from "@sapphire/decorators"; -import { container } from "@sapphire/framework"; - -import * as actions from "./actions"; -import { - ButtonStyle, - ButtonBuilder, - ChannelType, - ContainerBuilder, - TextDisplayBuilder, -} from "discord.js"; -import { getGuildByDiscordId } from "@stealth-developers/db"; -import { errorMessage, getTextChannel } from "@/lib"; -import { Result } from "@sapphire/framework"; -import { ActionRowBuilder } from "discord.js"; -import { newTicketButton } from "@/components/NewTicketButton"; - -@ApplyOptions<Subcommand.Options>({ - description: "ticket-related commands", -}) -export class TicketCommand extends Subcommand { - public constructor(context: Subcommand.LoaderContext, options: Subcommand.Options) { - super(context, { - ...options, - name: "ticket", - subcommands: [ - { name: "new", chatInputRun: "createTicket" }, - { name: "for", chatInputRun: "createTicketFor", preconditions: ["ModeratorCheck"] }, - { - name: "actions", - type: "group", - entries: [ - { name: "open", chatInputRun: "openTicket" }, - { name: "close", chatInputRun: "closeTicket" }, - { name: "add", chatInputRun: "addToTicket" }, - { name: "claim", chatInputRun: "claimTicket" }, - { name: "unclaim", chatInputRun: "unclaimTicket" }, - { name: "delete", chatInputRun: "deleteTicket" }, - ], - }, - { - name: "view", - type: "group", - entries: [ - { name: "info", chatInputRun: "viewTicketInfo" }, - { name: "transcript", chatInputRun: "viewTicketTranscript" }, - { name: "list", chatInputRun: "listTickets" }, - { name: "export", chatInputRun: "exportTickets" }, - ], - }, - { - name: "manage", - type: "group", - entries: [ - { - name: "transcript-channel", - chatInputRun: "manageConfig", - preconditions: ["ModeratorCheck"], - }, - { - name: "entrypoint-channel", - chatInputRun: "manageConfig", - preconditions: ["ModeratorCheck"], - }, - { - name: "category", - chatInputRun: "manageConfig", - preconditions: ["ModeratorCheck"], - }, - { - name: "prompt", - chatInputRun: "manageConfig", - preconditions: ["ModeratorCheck"], - }, - { - name: "send-prompt", - chatInputRun: "sendPrompt", - preconditions: ["ModeratorCheck"], - }, - { - name: "greeting", - chatInputRun: "manageConfig", - preconditions: ["ModeratorCheck"], - }, - ], - }, - ], - }); - } - - public override registerApplicationCommands(registry: Subcommand.Registry) { - registry.registerChatInputCommand((builder) => - builder - .setName("ticket") - .setDescription("root ticket command") - .addSubcommand((command) => command.setName("new").setDescription("Open a new ticket")) - .addSubcommand((command) => - command - .setName("for") - .setDescription("Open a ticket for a user") - .addUserOption((option) => - option - .setName("user") - .setDescription("The user to open the ticket for") - .setRequired(true), - ), - ) - .addSubcommandGroup((subcommandGroup) => - subcommandGroup - .setName("action") - .setDescription("ticket actions") - .addSubcommand((command) => - command.setName("open").setDescription("Reopen the ticket in the current channel"), - ) - .addSubcommand((command) => - command - .setName("close") - .setDescription("Close the ticket in the current channel") - .addStringOption((option) => - option - .setName("reason") - .setDescription("Public reason for closing the ticket") - .setRequired(true), - ) - .addStringOption((option) => - option - .setName("private") - .setDescription( - "Private reason for closing the ticket, only visible to moderators", - ) - .setRequired(false), - ) - .addBooleanOption((option) => - option.setName("delete-channel").setDescription("Delete the channel immediately"), - ), - ), - ) - .addSubcommandGroup((subcommandGroup) => - subcommandGroup - .setName("manage") - .setDescription("ticket management commands") - .addSubcommand((command) => - command - .setName("transcript-channel") - .setDescription("Sets the channel for ticket transcripts") - .addChannelOption((option) => - option - .setName("channel") - .setDescription("The channel to send ticket transcripts to") - .setRequired(true) - .addChannelTypes(ChannelType.GuildText), - ), - ) - .addSubcommand((command) => - command - .setName("entrypoint-channel") - .setDescription("Sets the entry point channel for ticket creation") - .addChannelOption((option) => - option - .setName("channel") - .setDescription("The channel to set as the entry point") - .setRequired(true) - .addChannelTypes(ChannelType.GuildText), - ), - ) - .addSubcommand((command) => - command - .setName("category") - .setDescription("Sets the category for new tickets") - .addChannelOption((option) => - option - .setName("category") - .setDescription("The category to create new tickets in") - .setRequired(true) - .addChannelTypes(ChannelType.GuildCategory), - ), - ) - .addSubcommand((command) => - command - .setName("prompt") - .setDescription("Set the message sent in the entry point channel"), - ) - .addSubcommand((command) => - command - .setName("send-prompt") - .setDescription("Send the prompt to the entry point channel"), - ) - .addSubcommand((command) => - command.setName("greeting").setDescription("Set the message sent in new tickets"), - ), - ), - ); - } - - public async createTicket(interaction: Subcommand.ChatInputCommandInteraction) { - await actions.createTicket(interaction); - } - public async createTicketFor(interaction: Subcommand.ChatInputCommandInteraction) { - await actions.createTicket(interaction); - } - - public async manageConfig(interaction: Subcommand.ChatInputCommandInteraction) { - await actions.manageConfig(interaction); - } - - public async sendPrompt(interaction: Subcommand.ChatInputCommandInteraction) { - if (!interaction.guildId) - return errorMessage(interaction, "This command can only be used in a server."); - const guild = await getGuildByDiscordId(interaction.guildId); - if (!guild) return errorMessage(interaction, "Guild not configured."); - - const commandString = (sub: string) => `</ticket manage ${sub}:${interaction.commandId}>`; - - 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 button = new ButtonBuilder() - .setLabel("Open a Ticket") - .setStyle(ButtonStyle.Success) - .setCustomId(newTicketButton.id()); - - const container = new ContainerBuilder() - .addTextDisplayComponents(new TextDisplayBuilder().setContent(promptText)) - .addActionRowComponents(new ActionRowBuilder<ButtonBuilder>().addComponents(button)); - - await channel.send({ flags: ["IsComponentsV2"], components: [container] }); - return interaction.reply({ - content: "Prompt sent!", - flags: ["Ephemeral"], - }); - } -} - -container.stores.loadPiece({ - piece: TicketCommand, - name: "ticket", - store: "commands", -});
M apps/bot/src/commands/tickets/actions.tsapps/bot/src/commands/tickets/actions.ts

@@ -1,13 +1,22 @@

import { randomUUID } from "node:crypto"; -import { TextChannel, ChannelType, type Guild } from "discord.js"; -import { container, Result } from "@sapphire/framework"; +import { + TextChannel, + ChannelType, + type Guild, + ContainerBuilder, + TextDisplayBuilder, + ActionRowBuilder, +} from "discord.js"; +import { Result } from "@sapphire/framework"; import db, { desc, eq, getGuildByDiscordId, tickets } from "@stealth-developers/db"; export * from "./config"; -import { upsertUser, getCategory, PublicError } from "@/lib"; +import { upsertUser, getCategory, PublicError, logger } from "@/lib"; import type { AnyUser } from "@/types"; +import { ButtonBuilder } from "discord.js"; +import TicketButtonPresets from "@/components/buttons/tickets"; type CreateTicketArgs = | undefined

@@ -82,10 +91,26 @@

if (!channel) throw new PublicError("failed to create Discord channel."); createdChannel = channel; - await tx.update(tickets).set({ channelId: channel.id }).where(eq(tickets.id, ticket.id)); - await channel.send({ content: `Hi! Thanks for submitting a report.` }); + const greeting = dbGuild.ticketGreeting; + if (greeting) { + const container = new ContainerBuilder() + .addTextDisplayComponents( + new TextDisplayBuilder({ + content: greeting.replaceAll("{user}", `<@${subjectUser.discordId}>`), + }), + ) + .addActionRowComponents( + new ActionRowBuilder<ButtonBuilder>().addComponents( + TicketButtonPresets.claim(publicId), + TicketButtonPresets.close(publicId), + ), + ); - return channel.id; + channel.send({ components: [container], flags: ["IsComponentsV2"] }); + } + + await tx.update(tickets).set({ channelId: channel.id }).where(eq(tickets.id, ticket.id)); + return channel; }); });

@@ -98,11 +123,11 @@ error instanceof PublicError

? `Failed to create ticket: ${error.message}` : "There was an error creating the ticket."; - if (error instanceof Error) container.logger.error(error.message, error.stack); + if (error instanceof Error) logger.error(error.stack, error.message); throw new PublicError(message); } - const channelId = txResult.unwrap(); - return { channelId, publicId }; + const channel = txResult.unwrap(); + return { channelId: channel.id, publicId }; }
M apps/bot/src/commands/tickets/command.tsapps/bot/src/commands/tickets/command.ts

@@ -1,18 +1,12 @@

-import { kitten } from "@/client"; import { option } from "@purrkit/router"; import { Result } from "@sapphire/result"; +import { ButtonBuilder, ContainerBuilder, TextDisplayBuilder, ActionRowBuilder } from "discord.js"; +import { getGuildByDiscordId } from "@stealth-developers/db"; -import * as actions from "./actions"; +import { kitten } from "@/client"; import { errorMessage, getTextChannel, PublicError } from "@/lib"; -import { getGuildByDiscordId } from "@stealth-developers/db"; -import { newTicketButton } from "@/components"; -import { - ButtonBuilder, - ButtonStyle, - ContainerBuilder, - TextDisplayBuilder, - ActionRowBuilder, -} from "discord.js"; +import { TicketButtonPresets } from "@/components/buttons"; +import * as actions from "./actions"; const ticketCommand = kitten.command("ticket", { description: "Ticket-related commands",

@@ -126,6 +120,14 @@ await actions.manageConfig(interaction);

}, }); + group.subcommand("greeting", { + description: "Set the message sent in new tickets", + async run(interaction) { + await actions.manageConfig(interaction); + }, + }); + + // actions group.subcommand("send-prompt", { description: "Send the prompt to the entry point channel", async run(interaction) {

@@ -166,27 +168,17 @@ "I don't have permission to send messages in the prompt channel.",

); } - const button = new ButtonBuilder() - .setLabel("Open a Ticket") - .setStyle(ButtonStyle.Success) - .setCustomId(newTicketButton.id()); - const container = new ContainerBuilder() .addTextDisplayComponents(new TextDisplayBuilder().setContent(promptText)) - .addActionRowComponents(new ActionRowBuilder<ButtonBuilder>().addComponents(button)); + .addActionRowComponents( + new ActionRowBuilder<ButtonBuilder>().addComponents(TicketButtonPresets.create()), + ); await channel.send({ flags: ["IsComponentsV2"], components: [container] }); return interaction.reply({ content: "Prompt sent!", flags: ["Ephemeral"], }); - }, - }); - - group.subcommand("greeting", { - description: "Set the message sent in new tickets", - async run(interaction) { - await actions.manageConfig(interaction); }, }); },
A apps/bot/src/components/buttons/index.ts

@@ -0,0 +1,1 @@

+export { TicketButtons, TicketButtonPresets } from "./tickets";
M apps/bot/src/components/buttons/newTicket.tsapps/bot/src/components/buttons/newTicket.ts

@@ -1,32 +1,1 @@

-import { Result } from "@sapphire/result"; - -import * as actions from "@/commands/tickets/actions"; -import { errorMessage, PublicError } from "@/lib"; -import { kitten } from "@/client"; - -export const newTicketButton = kitten.button("open-ticket", { - run: async (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"], - }); - }, -}); +export { newTicketButton } from "./tickets/index";
A apps/bot/src/components/buttons/tickets/index.ts

@@ -0,0 +1,94 @@

+import { ButtonBuilder, ButtonStyle } from "discord.js"; +import { Result } from "@sapphire/framework"; +import { option } from "@purrkit/router"; + +import { kitten } from "@/client"; +import { errorMessage, PublicError } from "@/lib"; +import * as actions from "@/commands/tickets/actions"; + +export const closeTicketButton = kitten.button("close-ticket", { + options: { + ticketId: option.string({ required: true }), + }, + async run() {}, +}); + +export const deleteTicketButton = kitten.button("delete-ticket", { + options: { + ticketId: option.string({ required: true }), + }, + async run() {}, +}); + +export const claimTicketButton = kitten.button("claim-ticket", { + options: { + ticketId: option.string({ required: true }), + }, + async run() {}, +}); + +export const unclaimTicketButton = kitten.button("unclaim-ticket", { + options: { + ticketId: option.string({ required: true }), + }, + async run() {}, +}); + +export const newTicketButton = kitten.button("open-ticket", { + run: async (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"], + }); + }, +}); + +export const TicketButtons = [ + newTicketButton, + closeTicketButton, + deleteTicketButton, + claimTicketButton, +]; + +export const TicketButtonPresets = { + close: (id: string) => + new ButtonBuilder() + .setCustomId(closeTicketButton.id({ ticketId: id })) + .setLabel("Close Ticket") + .setStyle(ButtonStyle.Danger), + delete: (id: string) => + new ButtonBuilder() + .setCustomId(deleteTicketButton.id({ ticketId: id })) + .setLabel("Delete Ticket") + .setStyle(ButtonStyle.Danger), + claim: (id: string) => + new ButtonBuilder() + .setCustomId(claimTicketButton.id({ ticketId: id })) + .setLabel("Claim Ticket") + .setStyle(ButtonStyle.Primary), + create: () => + new ButtonBuilder() + .setLabel("Open a Ticket") + .setStyle(ButtonStyle.Success) + .setCustomId(newTicketButton.id()), +}; + +export default TicketButtonPresets;
M apps/bot/src/index.tsapps/bot/src/index.ts

@@ -2,8 +2,9 @@ import config from "@stealth-developers/config";

import { logger } from "$/logger"; import { client, kitten } from "./client"; -import { configTextModal, newTicketButton } from "./components"; +import { configTextModal } from "./components"; import commands from "./commands"; +import { TicketButtons } from "./components/buttons"; const main = async () => { try {

@@ -18,7 +19,7 @@

client.once("clientReady", async (client) => { kitten.register({ commands: commands, - components: [configTextModal, newTicketButton], + components: [configTextModal, ...TicketButtons], }); await kitten.sync();