190 lines
6.1 KiB
TypeScript
190 lines
6.1 KiB
TypeScript
|
|
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");
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|