all repos — stealth-developers @ c44d4e7df8b225bf9d3c614395ffc7870cd96075

app: more generic component store; support buttons
vi did:web:vt3e.cat
Sat, 06 Jun 2026 22:00:08 +0100
commit

c44d4e7df8b225bf9d3c614395ffc7870cd96075

parent

158b915b8f28f27e37e948b8f6ae783b0cadbccf

M apps/bot/src/commands/tickets/config.tsapps/bot/src/commands/tickets/config.ts

@@ -1,16 +1,9 @@

-import { - ModalBuilder, - TextInputBuilder, - TextInputStyle, - LabelBuilder, - MessageFlags, -} from "discord.js"; +import { ModalBuilder, TextInputBuilder, TextInputStyle, LabelBuilder } from "discord.js"; import type { Subcommand } from "@sapphire/plugin-subcommands"; import db, { eq, getGuildByDiscordId, guilds } from "@stealth-developers/db"; import { errorMessage } from "@/lib"; -import { modalRegistry } from "$/structures/ModalRegistry"; -import { ModalDefinition } from "$/structures/ModalDefinition"; +import { ConfigTextModal } from "@/components/ConfigModal"; type GuildKeys = keyof typeof guilds.$inferSelect; type ConfigOptionBase = {

@@ -53,6 +46,7 @@ if (!guild) return errorMessage(interaction, "Failed to update configuration: Guild not found.");

if (!(sub in configOptions)) return errorMessage(interaction, "Invalid subcommand."); const subcommand = sub as ConfigSubcommand; + const option = configOptions[subcommand]; if (!option) return errorMessage(interaction, "Invalid configuration option.");

@@ -97,30 +91,3 @@ content: "Configuration updated successfully!",

flags: ["Ephemeral"], }); } - -export const ConfigTextModal = new ModalDefinition< - "config-text", - [key: "ticketPrompt" | "ticketGreeting"] ->("config-text"); - -modalRegistry.register(ConfigTextModal, 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], - }); -});
M apps/bot/src/commands/tickets/index.tsapps/bot/src/commands/tickets/index.ts

@@ -14,6 +14,7 @@ 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",

@@ -245,7 +246,7 @@

const button = new ButtonBuilder() .setLabel("Open a Ticket") .setStyle(ButtonStyle.Success) - .setCustomId("open-ticket"); + .setCustomId(NewTicketButton.create()); const container = new ContainerBuilder() .addTextDisplayComponents(new TextDisplayBuilder().setContent(promptText))
A apps/bot/src/components/ConfigModal.ts

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

+import { errorMessage } from "@/lib"; +import { modalRegistry } from "@/lib/registries"; +import { ComponentDefinition } from "@/lib/structures/ComponentDefinition"; +import db, { eq, getGuildByDiscordId, guilds } from "@stealth-developers/db"; +import { MessageFlags } from "discord.js"; + +export const ConfigTextModal = new ComponentDefinition< + "config-text", + [key: "ticketPrompt" | "ticketGreeting"] +>("config-text"); + +modalRegistry.register(ConfigTextModal, 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], + }); +});
A apps/bot/src/components/NewTicketButton.ts

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

+import { getGuildByDiscordId } from "@stealth-developers/db"; + +import { errorMessage } from "@/lib"; +import { buttonRegistry } from "@/lib/registries"; +import { ComponentDefinition } from "@/lib/structures/ComponentDefinition"; + +export const NewTicketButton = new ComponentDefinition<"open-ticket", []>("open-ticket"); + +buttonRegistry.register(NewTicketButton, async (interaction) => { + 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."); + + await interaction.reply("ok"); +});
A apps/bot/src/components/index.ts

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

+export * from "./ConfigModal"; +export * from "./NewTicketButton";
A apps/bot/src/handlers/buttons.ts

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

+import { container, InteractionHandler, InteractionHandlerTypes } from "@sapphire/framework"; +import type { ButtonInteraction } from "discord.js"; +import { buttonRegistry } from "../lib/registries"; + +export class ButtonHandler extends InteractionHandler { + public constructor(ctx: InteractionHandler.LoaderContext, options: InteractionHandler.Options) { + super(ctx, { + ...options, + interactionHandlerType: InteractionHandlerTypes.Button, + }); + } + + public override parse(interaction: ButtonInteraction) { + const [id] = interaction.customId.split(":"); + if (!id) return this.none(); + + const registered = buttonRegistry.get(id); + if (!registered) return this.none(); + + const args = registered.definition.parse(interaction.customId); + if (!args) return this.none(); + + return this.some({ registered, args }); + } + + public async run( + interaction: ButtonInteraction, + { registered, args }: { registered: any; args: string[] }, + ) { + console.log("Running button interaction with ID:", registered.definition.id); + try { + await registered.run(interaction, args); + } catch (error) { + this.container.logger.error("Failed to execute button interaction:", error); + } + } +} + +container.stores.loadPiece({ + name: "ButtonHandler", + piece: ButtonHandler, + store: "interaction-handlers", +});
M apps/bot/src/handlers/index.tsapps/bot/src/handlers/index.ts

@@ -1,1 +1,2 @@

export * from "./modals"; +export * from "./buttons";
M apps/bot/src/handlers/modals.tsapps/bot/src/handlers/modals.ts

@@ -1,6 +1,6 @@

import { InteractionHandler, InteractionHandlerTypes, container } from "@sapphire/framework"; import type { ModalSubmitInteraction } from "discord.js"; -import { modalRegistry } from "$/structures/ModalRegistry"; +import { modalRegistry } from "$/registries"; export class ModalHandler extends InteractionHandler { public constructor(ctx: InteractionHandler.LoaderContext, options: InteractionHandler.Options) {
A apps/bot/src/lib/registries.ts

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

+import type { + ButtonInteraction, + ModalSubmitInteraction, + AnySelectMenuInteraction, +} from "discord.js"; +import { ComponentRegistry } from "./structures/ComponentRegistry"; + +export const buttonRegistry = new ComponentRegistry<ButtonInteraction>(); +export const modalRegistry = new ComponentRegistry<ModalSubmitInteraction>(); +export const selectMenuRegistry = new ComponentRegistry<AnySelectMenuInteraction>();
A apps/bot/src/lib/structures/ComponentRegistry.ts

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

+import type { Interaction } from "discord.js"; +import type { ComponentDefinition } from "./ComponentDefinition"; + +export interface RegisteredComponent<Int extends Interaction, Args extends string[]> { + definition: ComponentDefinition<any, Args>; + run(interaction: Int, args: Args): unknown | Promise<unknown>; +} + +export class ComponentRegistry<Int extends Interaction> { + private registry = new Map<string, RegisteredComponent<Int, any>>(); + + public register<Args extends string[]>( + definition: ComponentDefinition<any, Args>, + run: (interaction: Int, args: Args) => unknown | Promise<unknown>, + ) { + this.registry.set(definition.id, { definition, run }); + } + + public get(id: string) { + return this.registry.get(id); + } +}
M apps/bot/src/lib/structures/ModalDefinition.tsapps/bot/src/lib/structures/ComponentDefinition.ts

@@ -1,9 +1,10 @@

-export class ModalDefinition<Id extends string, Args extends string[]> { +export class ComponentDefinition<Id extends string, Args extends string[]> { public readonly id: Id; public constructor(id: Id) { this.id = id; } + public create(...args: Args): string { return args.length > 0 ? `${this.id}:${args.join(":")}` : this.id; }
D apps/bot/src/lib/structures/ModalRegistry.ts

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

-import type { ModalSubmitInteraction } from "discord.js"; -import type { ModalDefinition } from "./ModalDefinition"; - -export interface RegisteredModal<Args extends string[]> { - definition: ModalDefinition<any, Args>; - run(interaction: ModalSubmitInteraction, args: Args): unknown | Promise<unknown>; -} - -class ModalRegistry { - private modals = new Map<string, RegisteredModal<any>>(); - - public register<Args extends string[]>( - definition: ModalDefinition<any, Args>, - run: (interaction: ModalSubmitInteraction, args: Args) => unknown | Promise<unknown>, - ) { - this.modals.set(definition.id, { definition, run }); - } - - public get(id: string) { - return this.modals.get(id); - } -} - -export const modalRegistry = new ModalRegistry();