171 lines
6.8 KiB
TypeScript
171 lines
6.8 KiB
TypeScript
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, Name> = 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<Options extends readonly DiscordApplicationCommandOption[]> = {
|
|
[K in Options[number]['name']]: GetOption<Options, K> 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 = Record<string, never>> = (args: Args) => Promise<void> | void
|
|
|
|
|
|
|
|
// Get subcommand or subcommand group by name
|
|
type GetSubcommandOrGroup<Options, Name> = 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 extends readonly DiscordApplicationCommandOption[]> = Options[number] extends { type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup } ? true : false
|
|
|
|
// 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 }
|
|
? N extends string
|
|
? N
|
|
: never
|
|
: never
|
|
|
|
// 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
|
|
}
|
|
|
|
// Get command by name from array
|
|
type GetCommand<Commands extends readonly any[], Name> = Commands[number] extends infer C ? (C extends { name: Name } ? C : never) : never
|
|
|
|
// Main type to extract command handlers from slash commands
|
|
export type ExtractCommands<T extends readonly CreateApplicationCommand[]> = {
|
|
[Name in T[number]['name']]: GetCommand<T, Name> extends { options?: infer Options }
|
|
? Options extends readonly DiscordApplicationCommandOption[]
|
|
? HasOnlySubcommands<Options> extends true
|
|
? SubcommandHandlers<Options>
|
|
: HandlerFunction<ExtractArgs<Options>>
|
|
: 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
|
|
}
|
|
|
|
// The actual command handler type based on SLASH_COMMANDS
|
|
export type CommandHandlers = ExtractCommands<typeof SLASH_COMMANDS>
|
|
|
|
// 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 {
|
|
if (!options) return {}
|
|
|
|
const args: Record<string, any> = {}
|
|
|
|
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<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)
|
|
}
|
|
|
|
// Helper function to create command handlers with type safety
|
|
export function createCommandHandler<T extends readonly CreateApplicationCommand[]>({
|
|
handler,
|
|
notFoundHandler,
|
|
}: {
|
|
commands: T,
|
|
handler: ExtractCommands<T>
|
|
notFoundHandler: HandlerFunction<{path?: string}>
|
|
}) {
|
|
return async (data: InteractionData): Promise<void> => {
|
|
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)
|
|
}
|
|
}
|