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

190 lines
6.1 KiB
TypeScript
Raw Normal View History

2025-06-14 05:36:37 +00:00
import { ApplicationCommandOptionTypes, CreateApplicationCommand, DiscordInteractionDataOption} from "@discordeno/types";
import { SLASH_COMMANDS } from "./slash_commands";
import { InteractionData } from "..";
// 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
};
// Extract the argument types from command options
type ExtractArgs<Options extends readonly any[]> = {
[K in Options[number] as K extends { name: infer N; type: infer T }
? T extends keyof OptionTypeMap
? N extends string
? N
: never
: never
: never]: Options[number] extends { name: K; 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: Args) => Promise<void> | void;
// Helper types to extract command structure with proper argument types
type ExtractSubcommands<T> = T extends { options: infer O }
? O extends readonly any[]
? {
[K in O[number] as K extends { name: infer N; type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup }
? N extends string
? N
: never
: never]: O[number] extends { name: K; options?: infer SubO }
? SubO extends readonly any[]
? HandlerFunction<ExtractArgs<SubO>>
: HandlerFunction<{}>
: HandlerFunction<{}>
}
: never
: never;
type ExtractCommands<T extends readonly CreateApplicationCommand[]> = {
[K in T[number] as K['name']]: ExtractSubcommands<K> extends never
? T[number] extends { name: K; options?: infer O }
? O extends readonly any[]
? HandlerFunction<ExtractArgs<O>>
: HandlerFunction<{}>
: HandlerFunction<{}>
: ExtractSubcommands<K>
};
// 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
function parseOptions(options?: DiscordInteractionDataOption[]): Record<string, 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;
}
// Helper function to create command handlers with type safety
export function createCommandHandler<T extends readonly CreateApplicationCommand[]>(
{handler, notFoundHandler}:{
handler: ExtractCommands<T>
notFoundHandler: HandlerFunction<{}>
}) {
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;
}
// Check if it's a direct command or has subcommands
if (typeof commandHandler === 'function') {
// Parse arguments from top-level options
const args = parseOptions(data.options);
await commandHandler(args);
} else {
// Handle subcommands
const subcommand = data.options?.find(
opt => opt.type === ApplicationCommandOptionTypes.SubCommand ||
opt.type === ApplicationCommandOptionTypes.SubCommandGroup
);
if (!subcommand) {
await notFoundHandler({});
return;
}
const subHandler = commandHandler[subcommand.name as keyof typeof commandHandler];
if (!subHandler || typeof subHandler !== 'function') {
await notFoundHandler({});
return;
}
// Parse arguments from subcommand options
const args = parseOptions(subcommand.options);
await subHandler(args);
}
}
}
// Function to validate that all commands are implemented
export function validateCommandImplementation(handlers: CommandHandlers): void {
for (const command of SLASH_COMMANDS) {
const commandName = command.name;
if (!(commandName in handlers)) {
throw new Error(`Command "${commandName}" is not implemented`);
}
// Check subcommands if they exist
if (command.options) {
const subcommands = command.options.filter(
opt => opt.type === ApplicationCommandOptionTypes.SubCommand
);
if (subcommands.length > 0) {
const handlerObj = handlers[commandName as keyof CommandHandlers];
if (typeof handlerObj === 'function') {
throw new Error(`Command "${commandName}" has subcommands but is implemented as a function`);
}
for (const subcommand of subcommands) {
if (!(subcommand.name in handlerObj)) {
throw new Error(`Subcommand "${commandName}.${subcommand.name}" is not implemented`);
}
}
}
}
}
}
// Helper function to create command handlers with type safety
export function createCommandHandlers<T extends CommandHandlers>(handlers: T): T {
return handlers;
}
// Example usage showing the type-safe implementation
export const exampleHandlers = createCommandHandlers({
guild: {
leaderboard: async (args: {}) => {
console.log("Guild leaderboard command");
},
info: async (args: {}) => {
console.log("Guild info command");
},
online: async (args: {}) => {
console.log("Guild online command");
},
},
admin: {
set_wynn_guild: async (args: {}) => {
console.log("Admin set_wynn_guild command");
},
},
});