import type { ChatInputCommandInteraction, UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction, BaseInteraction, ButtonInteraction, AnySelectMenuInteraction, ModalSubmitInteraction, LocalizationMap, } from "discord.js"; import type { CommandOption, Option, InferOptions } from "./options"; import { KittenComponent, KittenComponentInteraction, type KittenComponentConfig, } from "./components"; export const INTEGRATION_TYPES = { GUILD_INSTALL: 0, USER_INSTALL: 1, } as const; export const INTERACTION_CONTEXTS = { GUILD: 0, BOT_DM: 1, PRIVATE_CHANNEL: 2, } as const; export const INTEGRATION_TYPE_PRESETS = { ALL: Object.values(INTEGRATION_TYPES), GUILD_ONLY: [INTEGRATION_TYPES.GUILD_INSTALL], USER_ONLY: [INTEGRATION_TYPES.USER_INSTALL], }; export const INTEGRATION_CONTEXT_PRESETS = { EVERYWHERE: Object.values(INTERACTION_CONTEXTS), GUILDS_ONLY: [INTERACTION_CONTEXTS.GUILD], DMS_ONLY: [INTERACTION_CONTEXTS.BOT_DM, INTERACTION_CONTEXTS.PRIVATE_CHANNEL], }; export type CommandResultType = Promise | Res; export type IntegrationType = (typeof INTEGRATION_TYPES)[keyof typeof INTEGRATION_TYPES]; export type InteractionContextType = (typeof INTERACTION_CONTEXTS)[keyof typeof INTERACTION_CONTEXTS]; /** * pre-execution interceptors that let you chain execution checks and pass their * results down as strongly-typed context. * * @throws {HaltExecution} if you wish to intentionally stop propagation. */ export type Middleware = ( interaction: I, ctx: InCtx, ) => Promise | OutCtx | void; /** * an exception handler that can catch, handle, or rethrow errors. * these are context-aware and will receive whatever context was successfully accumulated * before the exception occurred. */ export type ErrorHandler = ( err: unknown, interaction: I, ctx: Ctx, ) => Promise | unknown; /** * strictly-typed post-execution flow handlers. * * afterwares run sequentially inside a safe `finally` block and execute regardless * of whether the interaction completed cleanly, threw an exception, or was halted. */ export type Afterware = ( interaction: I, ctx: Ctx, error: unknown | null, result: Res | undefined, ) => Promise | void; export interface BaseCommandConfig { integrationTypes?: IntegrationType[]; contexts?: InteractionContextType[]; nameLocalizations?: LocalizationMap; defaultMemberPermissions?: string | number | bigint | any; nsfw?: boolean; onError?: ErrorHandler; } export interface LocalizedDescription { description: string; descriptionLocalizations?: LocalizationMap; } export interface ChatCommandConfig< O extends Record>, Ctx = any, Res = unknown, > extends BaseCommandConfig, LocalizedDescription { options?: O; run: ( interaction: ChatInputCommandInteraction, args: InferOptions, ctx: Ctx, ) => CommandResultType; } export interface ContextMenuConfig< I extends BaseInteraction, Ctx = any, Res = unknown, > extends BaseCommandConfig { run: (interaction: I, ctx: Ctx) => CommandResultType; } export class Subcommand { constructor( public name: string, public config: ChatCommandConfig, ) {} } export class SubcommandGroup { public subcommands: Record = {}; constructor( public name: string, public description: string, public config?: { nameLocalizations?: LocalizationMap; descriptionLocalizations?: LocalizationMap; }, ) {} /** * append a subcommand to this specific group. */ subcommand>, R extends Res>( name: string, config: ChatCommandConfig, ): this { this.subcommands[name] = new Subcommand(name, config); return this; } } export class SubcommandGroupBuilder { public subcommands: Record = {}; public groups: Record> = {}; constructor( public name: string, public description: string, public middlewares: Middleware[] = [], public errorHandlers: ErrorHandler[] = [], public afterwares: Afterware[] = [], public config?: BaseCommandConfig & LocalizedDescription, ) {} /** * chain a loose subcommand onto this builder. */ subcommand>, R extends Res>( name: string, config: ChatCommandConfig, ): this { this.subcommands[name] = new Subcommand(name, config); return this; } /** * define a sub-group to isolate subcommands further. */ group( name: string, config: LocalizedDescription & { nameLocalizations?: LocalizationMap }, setup: (group: SubcommandGroup) => void, ): this { const group = new SubcommandGroup(name, config.description, config); setup(group); this.groups[name] = group; return this; } } export class Command { constructor( public name: string, public config: ChatCommandConfig, public middlewares: Middleware[] = [], public errorHandlers: ErrorHandler[] = [], public afterwares: Afterware[] = [], ) {} } export class ContextMenuCommand< Ctx = any, I extends UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction = any, Res = unknown, > { constructor( public name: string, public type: "user" | "message", public run: (interaction: I, ctx: Ctx) => CommandResultType, public middlewares: Middleware[] = [], public errorHandlers: ErrorHandler[] = [], public afterwares: Afterware[] = [], public config?: BaseCommandConfig, ) {} } /** * a chainable builder for creating commands, context menus, and components with shared * context, middleware, error handlers, and afterwares. */ export class CommandBuilder { constructor( private middlewares: Middleware[] = [], private errorHandlers: ErrorHandler[] = [], private afterwares: Afterware[] = [], ) {} /** * attach a middleware to this builder. any context returned will be merged and * strictly typed for all subsequent middlewares and handlers. */ use>( mw: (interaction: BaseInteraction, ctx: Ctx) => Promise | NewCtx | void, ): CommandBuilder { return new CommandBuilder( [...this.middlewares, mw], this.errorHandlers, this.afterwares, ); } /** * attach a builder-level error handler. if an exception is thrown inside a command * or component produced by this builder, this handler will catch it. */ onError(handler: ErrorHandler): CommandBuilder { return new CommandBuilder( this.middlewares, [...this.errorHandlers, handler], this.afterwares, ); } /** * attach an afterware to this builder. these strictly enforce the return type of your commands * to prevent type safety degradation. * * @example * ```ts * .after(async (interaction, ctx, err, result) => { * // result is strictly typed as: MyResult | undefined * }) * ``` */ after(handler: Afterware): CommandBuilder { return new CommandBuilder(this.middlewares, this.errorHandlers, [ ...this.afterwares, handler as any, ]); } /** * construct a top-level chat input command. */ command>, R extends Res>( name: string, config: ChatCommandConfig, ): Command; /** * construct a subcommand group base. */ command( name: string, config: BaseCommandConfig & LocalizedDescription, ): SubcommandGroupBuilder; command(name: string, config: any): any { if (config.run) { return new Command( name, config, this.middlewares, this.errorHandlers, this.afterwares, ); } return new SubcommandGroupBuilder( name, config.description, this.middlewares, this.errorHandlers, this.afterwares, config, ); } private createContextMenu< I extends UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction, R extends Res, >(name: string, type: "user" | "message", runOrConfig: any) { const isFn = typeof runOrConfig === "function"; return new ContextMenuCommand( name, type, isFn ? runOrConfig : runOrConfig.run, this.middlewares, this.errorHandlers, this.afterwares, isFn ? undefined : runOrConfig, ); } /** * construct a user context menu command. */ userContextMenu( name: string, runOrConfig: | ((interaction: UserContextMenuCommandInteraction, ctx: Ctx) => CommandResultType) | ContextMenuConfig, ): ContextMenuCommand { return this.createContextMenu(name, "user", runOrConfig); } /** * construct a message context menu command. */ messageContextMenu( name: string, runOrConfig: | ((interaction: MessageContextMenuCommandInteraction, ctx: Ctx) => CommandResultType) | ContextMenuConfig, ): ContextMenuCommand { return this.createContextMenu(name, "message", runOrConfig); } private createComponent< I extends KittenComponentInteraction, O extends Record>, R extends Res, >(name: string, config: any): KittenComponent { return new KittenComponent( name, config, this.middlewares, this.errorHandlers, this.afterwares, ); } /** * construct a reusable button component handler. */ button>, R extends Res>( name: string, config: KittenComponentConfig, ): KittenComponent { return this.createComponent(name, config); } /** * construct a reusable select menu component handler. */ selectMenu>, R extends Res>( name: string, config: KittenComponentConfig, ): KittenComponent { return this.createComponent(name, config); } /** * construct a reusable modal component handler. */ modal>, R extends Res>( name: string, config: KittenComponentConfig, ): KittenComponent { return this.createComponent(name, config); } }