wynn/ts/src/discord/botevent/command_parser.ts

171 lines
6.8 KiB
TypeScript
Raw Normal View History

2025-06-14 23:54:24 +00:00
import { ApplicationCommandOptionTypes, DiscordApplicationCommandOption, type CreateApplicationCommand, type DiscordInteractionDataOption } from '@discordeno/types'
2025-06-14 23:04:46 +00:00
import type { InteractionData } from '..'
import type { SLASH_COMMANDS } from './slash_commands'
2025-06-14 05:36:37 +00:00
// Map option types to their TypeScript types
type OptionTypeMap = {
2025-06-14 23:04:46 +00:00
[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
}
2025-06-14 05:36:37 +00:00
2025-06-14 21:17:42 +00:00
// Helper type to get option by name
2025-06-14 23:54:24 +00:00
type GetOption<Options, Name> = Options extends readonly DiscordApplicationCommandOption[] ? (Options[number] extends infer O ? (O extends { name: Name } ? O : never) : never) : never
2025-06-14 21:17:42 +00:00
2025-06-14 05:36:37 +00:00
// Extract the argument types from command options
2025-06-14 23:54:24 +00:00
export type ExtractArgs<Options extends readonly DiscordApplicationCommandOption[]> = {
2025-06-14 21:17:42 +00:00
[K in Options[number]['name']]: GetOption<Options, K> extends { type: infer T; required?: infer R }
2025-06-14 05:36:37 +00:00
? T extends keyof OptionTypeMap
2025-06-14 21:17:42 +00:00
? R extends true
? OptionTypeMap[T]
: OptionTypeMap[T] | undefined
2025-06-14 05:36:37 +00:00
: never
2025-06-14 23:04:46 +00:00
: never
}
2025-06-14 05:36:37 +00:00
// Handler function type that accepts typed arguments
2025-06-14 23:54:24 +00:00
type HandlerFunction<Args = Record<string, never>> = (args: Args) => Promise<void> | void
2025-06-14 05:36:37 +00:00
2025-06-14 23:54:24 +00:00
// Get subcommand or subcommand group by name
type GetSubcommandOrGroup<Options, Name> = Options extends readonly DiscordApplicationCommandOption[]
2025-06-14 21:17:42 +00:00
? Options[number] extends infer O
2025-06-14 23:54:24 +00:00
? O extends { name: Name; type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup }
2025-06-14 21:17:42 +00:00
? O
: never
: never
2025-06-14 23:04:46 +00:00
: never
2025-06-14 21:17:42 +00:00
2025-06-14 23:54:24 +00:00
// Check if all options are subcommands or subcommand groups
type HasOnlySubcommands<Options extends readonly DiscordApplicationCommandOption[]> = Options[number] extends { type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup } ? true : false
2025-06-14 21:17:42 +00:00
2025-06-14 23:54:24 +00:00
// Extract subcommand or subcommand group names from options
type SubcommandNames<Options extends readonly DiscordApplicationCommandOption[]> = Options[number] extends { name: infer N; type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup }
2025-06-14 23:04:46 +00:00
? N extends string
? N
: never
: never
2025-06-14 21:17:42 +00:00
2025-06-14 23:54:24 +00:00
// Type to extract subcommand handlers (recursive for groups)
export type SubcommandHandlers<Options extends readonly DiscordApplicationCommandOption[]> = {
[K in SubcommandNames<Options>]: GetSubcommandOrGroup<Options, K> extends { type: infer T; options?: infer SubOpts }
? T extends ApplicationCommandOptionTypes.SubCommandGroup
? SubOpts extends readonly DiscordApplicationCommandOption[]
? SubcommandHandlers<SubOpts>
: never
: T extends ApplicationCommandOptionTypes.SubCommand
? SubOpts extends readonly DiscordApplicationCommandOption[]
? HandlerFunction<ExtractArgs<SubOpts>>
: HandlerFunction<Record<string, never>>
: never
: never
2025-06-14 23:04:46 +00:00
}
2025-06-14 21:17:42 +00:00
// Get command by name from array
2025-06-14 23:04:46 +00:00
type GetCommand<Commands extends readonly any[], Name> = Commands[number] extends infer C ? (C extends { name: Name } ? C : never) : never
2025-06-14 05:36:37 +00:00
2025-06-14 21:17:42 +00:00
// Main type to extract command handlers from slash commands
2025-06-14 23:54:24 +00:00
export type ExtractCommands<T extends readonly CreateApplicationCommand[]> = {
2025-06-14 21:17:42 +00:00
[Name in T[number]['name']]: GetCommand<T, Name> extends { options?: infer Options }
2025-06-14 23:54:24 +00:00
? Options extends readonly DiscordApplicationCommandOption[]
2025-06-14 21:17:42 +00:00
? HasOnlySubcommands<Options> extends true
? SubcommandHandlers<Options>
: HandlerFunction<ExtractArgs<Options>>
2025-06-14 23:54:24 +00:00
: HandlerFunction<Record<string, never>>
: HandlerFunction<Record<string, never>>
}
// Type representing the possible output of ExtractCommands
export type CommandHandler = HandlerFunction<any> | CommandHandlerMap
export type CommandHandlerMap = {
[key: string]: CommandHandler
2025-06-14 23:04:46 +00:00
}
2025-06-14 05:36:37 +00:00
// The actual command handler type based on SLASH_COMMANDS
2025-06-14 23:04:46 +00:00
export type CommandHandlers = ExtractCommands<typeof SLASH_COMMANDS>
2025-06-14 05:36:37 +00:00
2025-06-14 23:54:24 +00:00
// 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<T extends DiscordInteractionDataOption[]>(options?: T): any {
2025-06-14 23:04:46 +00:00
if (!options) return {}
2025-06-14 05:36:37 +00:00
2025-06-14 23:04:46 +00:00
const args: Record<string, any> = {}
2025-06-14 05:36:37 +00:00
for (const option of options) {
2025-06-14 23:04:46 +00:00
if (option.type === ApplicationCommandOptionTypes.SubCommand || option.type === ApplicationCommandOptionTypes.SubCommandGroup) {
continue
2025-06-14 05:36:37 +00:00
}
2025-06-14 23:04:46 +00:00
args[option.name] = option.value
2025-06-14 05:36:37 +00:00
}
2025-06-14 23:04:46 +00:00
return args
2025-06-14 05:36:37 +00:00
}
2025-06-14 23:54:24 +00:00
// Unified recursive handler for commands, subcommands, and subcommand groups
async function handleCommands(
handler: CommandHandler,
options: DiscordInteractionDataOption[] | undefined,
notFoundHandler: HandlerFunction<{path?: string}>
): Promise<void> {
// 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)
}
2025-06-14 05:36:37 +00:00
// Helper function to create command handlers with type safety
2025-06-14 23:04:46 +00:00
export function createCommandHandler<T extends readonly CreateApplicationCommand[]>({
handler,
notFoundHandler,
}: {
2025-06-14 23:54:24 +00:00
commands: T,
2025-06-14 05:36:37 +00:00
handler: ExtractCommands<T>
2025-06-14 23:54:24 +00:00
notFoundHandler: HandlerFunction<{path?: string}>
2025-06-14 05:36:37 +00:00
}) {
return async (data: InteractionData): Promise<void> => {
if (!data || !data.name) {
2025-06-14 23:04:46 +00:00
await notFoundHandler({})
return
2025-06-14 05:36:37 +00:00
}
2025-06-14 23:04:46 +00:00
const commandName = data.name as keyof typeof handler
const commandHandler = handler[commandName]
2025-06-14 05:36:37 +00:00
if (!commandHandler) {
2025-06-14 23:04:46 +00:00
await notFoundHandler({})
return
2025-06-14 05:36:37 +00:00
}
2025-06-14 23:54:24 +00:00
// Use unified handler for all command types
await handleCommands(commandHandler, data.options, notFoundHandler)
2025-06-14 05:36:37 +00:00
}
}