import { ApplicationCommandOptionTypes, DiscordApplicationCommandOption, type CreateApplicationCommand, type DiscordInteractionDataOption } from '@discordeno/types' import type { InteractionData } from '..' import type { SLASH_COMMANDS } from './slash_commands' // Map option types to their TypeScript types type OptionTypeMap = { [ApplicationCommandOptionTypes.String]: string [ApplicationCommandOptionTypes.Integer]: number [ApplicationCommandOptionTypes.Boolean]: boolean [ApplicationCommandOptionTypes.User]: string // user ID [ApplicationCommandOptionTypes.Channel]: string // channel ID [ApplicationCommandOptionTypes.Role]: string // role ID [ApplicationCommandOptionTypes.Number]: number [ApplicationCommandOptionTypes.Mentionable]: string // ID [ApplicationCommandOptionTypes.Attachment]: string // attachment ID } // Helper type to get option by name type GetOption = Options extends readonly DiscordApplicationCommandOption[] ? (Options[number] extends infer O ? (O extends { name: Name } ? O : never) : never) : never // Extract the argument types from command options export type ExtractArgs = { [K in Options[number]['name']]: GetOption extends { type: infer T; required?: infer R } ? T extends keyof OptionTypeMap ? R extends true ? OptionTypeMap[T] : OptionTypeMap[T] | undefined : never : never } // Handler function type that accepts typed arguments type HandlerFunction> = (args: Args) => Promise | void // Get subcommand or subcommand group by name type GetSubcommandOrGroup = Options extends readonly DiscordApplicationCommandOption[] ? Options[number] extends infer O ? O extends { name: Name; type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup } ? O : never : never : never // Check if all options are subcommands or subcommand groups type HasOnlySubcommands = Options[number] extends { type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup } ? true : false // Extract subcommand or subcommand group names from options type SubcommandNames = Options[number] extends { name: infer N; type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup } ? N extends string ? N : never : never // Type to extract subcommand handlers (recursive for groups) export type SubcommandHandlers = { [K in SubcommandNames]: GetSubcommandOrGroup extends { type: infer T; options?: infer SubOpts } ? T extends ApplicationCommandOptionTypes.SubCommandGroup ? SubOpts extends readonly DiscordApplicationCommandOption[] ? SubcommandHandlers : never : T extends ApplicationCommandOptionTypes.SubCommand ? SubOpts extends readonly DiscordApplicationCommandOption[] ? HandlerFunction> : HandlerFunction> : never : never } // Get command by name from array type GetCommand = Commands[number] extends infer C ? (C extends { name: Name } ? C : never) : never // Main type to extract command handlers from slash commands export type ExtractCommands = { [Name in T[number]['name']]: GetCommand extends { options?: infer Options } ? Options extends readonly DiscordApplicationCommandOption[] ? HasOnlySubcommands extends true ? SubcommandHandlers : HandlerFunction> : HandlerFunction> : HandlerFunction> } // Type representing the possible output of ExtractCommands export type CommandHandler = HandlerFunction | CommandHandlerMap export type CommandHandlerMap = { [key: string]: CommandHandler } // The actual command handler type based on SLASH_COMMANDS export type CommandHandlers = ExtractCommands // Helper function to parse option values from interaction data. there is sort of a hack here // so the actual type we need to parse comes from the parsing of DiscordApplicationCommandOption // but this is parsing from the DiscordInteractionDataOption type, which is just the generic data response. // technically, at runtime, with the SLASH_COMMANDS object, it's possible to validate this struct, and produce a better type than any // but that is... a lot of work, so we are just going to do this for now and leave validation for another day. function parseOptions(options?: T): any { if (!options) return {} const args: Record = {} for (const option of options) { if (option.type === ApplicationCommandOptionTypes.SubCommand || option.type === ApplicationCommandOptionTypes.SubCommandGroup) { continue } args[option.name] = option.value } return args } // Unified recursive handler for commands, subcommands, and subcommand groups async function handleCommands( handler: CommandHandler, options: DiscordInteractionDataOption[] | undefined, notFoundHandler: HandlerFunction<{path?: string}> ): Promise { // If handler is a function, execute it with parsed options if (typeof handler === 'function') { const args = parseOptions(options) await handler(args) return } // Handler is a map of subcommands/groups const subcommand = options?.find( (opt) => opt.type === ApplicationCommandOptionTypes.SubCommand || opt.type === ApplicationCommandOptionTypes.SubCommandGroup ) if (!subcommand) { await notFoundHandler({}) return } const subHandler = handler[subcommand.name] if (!subHandler) { await notFoundHandler({}) return } // Recursively handle the subcommand/group await handleCommands(subHandler, subcommand.options, notFoundHandler) } // Helper function to create command handlers with type safety export function createCommandHandler({ handler, notFoundHandler, }: { commands: T, handler: ExtractCommands notFoundHandler: HandlerFunction<{path?: string}> }) { return async (data: InteractionData): Promise => { if (!data || !data.name) { await notFoundHandler({}) return } const commandName = data.name as keyof typeof handler const commandHandler = handler[commandName] if (!commandHandler) { await notFoundHandler({}) return } // Use unified handler for all command types await handleCommands(commandHandler, data.options, notFoundHandler) } }