diff --git a/src/index.ts b/src/index.ts index 9bfe125..4d3f626 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,4 +14,4 @@ export { Message, type MessagePayload } from './types/message'; export { Embed } from './types/embed'; export { Emoji } from './types/emoji'; // Other -export { createCommandHandler } from './types/hander'; \ No newline at end of file +export { createCommandHandler } from './types/handler'; \ No newline at end of file diff --git a/src/services/client.ts b/src/services/client.ts index a96d04a..9e87e7a 100644 --- a/src/services/client.ts +++ b/src/services/client.ts @@ -3,7 +3,7 @@ import { Gateway } from './connectors/gateway'; import { Processor } from './processor'; import { Expectation, fromDiscord, Intent, type ClientConfig } from '../types/common'; import { exists, isNil } from '../utils/index'; -import type { Handler } from '../types/hander'; +import type { Handler } from '../types/handler'; import { type DiscordMessage, Message } from '../types/message'; import { Guilds } from './caches/guilds'; import { Messages } from './caches/messages'; diff --git a/src/services/processor.ts b/src/services/processor.ts index ba7bcce..febe325 100644 --- a/src/services/processor.ts +++ b/src/services/processor.ts @@ -1,8 +1,9 @@ import type { Client } from "./client"; import type { Context } from "../types/context"; -import { HandlerType, type EventPayload } from "../types/common"; -import { BaseHandler, type Handler } from "../types/hander"; +import { HandlerType, Trait, type EventPayload } from "../types/common"; +import { type CommandHandler, type Handler } from "../types/handler"; import type { API } from "./api"; +import type { ApplicationCommandOption } from "../types/applicationCommand"; export class Processor { private handlers = { @@ -55,11 +56,15 @@ export class Processor { if (handlers) { for (const handler of handlers) { - if (handler instanceof BaseHandler) { - await Promise.resolve(handler.execute(context, argsOrPayload)); + if (isCommandHandler(handler)) { + if (type === HandlerType.Events) { + throw new Error('Event handlers cannot be a CommandHandler'); + } + + await Promise.resolve(handler[Trait.execute](context!, argsOrPayload)); } else { // TODO: Fix ts error - await Promise.resolve(handler(context, argsOrPayload)) + await Promise.resolve(handler(context, argsOrPayload)); } } } @@ -93,3 +98,11 @@ export class Processor { has: (event: string) => this.handlers[HandlerType.Interactions].has(event) ?? this.handlers[HandlerType.Interactions].get(event)?.length! > 0 }; } + +const isCommandHandler = (handler: Handler): handler is CommandHandler => { + if (typeof handler === 'object') { + return 'args' in handler && Trait.execute in handler; + } else { + return false; + } +} \ No newline at end of file diff --git a/src/types/applicationCommand.ts b/src/types/applicationCommand.ts index cf9dc4e..701f2ff 100644 --- a/src/types/applicationCommand.ts +++ b/src/types/applicationCommand.ts @@ -2,54 +2,55 @@ import { Trait } from "./common"; // Enum for Application Command Types export enum ApplicationCommandType { - CHAT_INPUT = 1, - USER = 2, - MESSAGE = 3, - PRIMARY_ENTRY_POINT = 4, + ChatInput = 1, + User = 2, + Message = 3, + PrimaryEntryPoint = 4, } // Enum for Application Command Option Types export enum ApplicationCommandOptionType { - SUB_COMMAND = 1, - SUB_COMMAND_GROUP = 2, - STRING = 3, - INTEGER = 4, - BOOLEAN = 5, - USER = 6, - CHANNEL = 7, - ROLE = 8, - MENTIONABLE = 9, - NUMBER = 10, - ATTACHMENT = 11, + SubCommand = 1, + SubCommandGroup = 2, + String = 3, + Integer = 4, + Boolean = 5, + User = 6, + Channel = 7, + Role = 8, + Mentionable = 9, + Number = 10, + Attachment = 11, } // Enum for Channel Types (if applicable for options) export enum ChannelType { - GUILD_TEXT = 0, + GuildText = 0, DM = 1, - GUILD_VOICE = 2, - GROUP_DM = 3, - GUILD_CATEGORY = 4, - GUILD_NEWS = 5, - GUILD_STORE = 6, - GUILD_NEWS_THREAD = 10, - GUILD_PUBLIC_THREAD = 11, - GUILD_PRIVATE_THREAD = 12, - GUILD_STAGE_VOICE = 13, - GUILD_DIRECTORY = 14, - GUILD_FORUM = 15, + GuildVoice = 2, + GroupDm = 3, + GuildCategory = 4, + GuildNews = 5, + GuildStore = 6, + GuildNewsThread = 10, + GuildPublicThread = 11, + GuildPrivateThread = 12, + GuildStageVoice = 13, + GuildDirectory = 14, + GuildForum = 15, } // Enum for Application Command Handler Types export enum ApplicationCommandHandlerType { - APP_HANDLER = 1, - DISCORD_LAUNCH_ACTIVITY = 2, + AppHandler = 1, + LaunchActivity = 2, } // Define the ApplicationCommandOption type export type ApplicationCommandOption = { type: ApplicationCommandOptionType; name: string; + id: string; name_localizations?: Record | null; description: string; description_localizations?: Record | null; diff --git a/src/types/hander.ts b/src/types/hander.ts deleted file mode 100644 index 8fa91d0..0000000 --- a/src/types/hander.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { ApplicationCommandOption, ApplicationCommandOptionType } from "./applicationCommand"; -import { Trait, type EventPayload } from "./common"; -import type { Context } from "./context"; - -type PrimitiveTypeMap = { - [ApplicationCommandOptionType.STRING]: string; - [ApplicationCommandOptionType.INTEGER]: number; - [ApplicationCommandOptionType.BOOLEAN]: boolean; - [ApplicationCommandOptionType.NUMBER]: number; - [ApplicationCommandOptionType.USER]: string; // Could be a user ID or object depending on your use case - [ApplicationCommandOptionType.CHANNEL]: string; // Could be a channel ID or object - [ApplicationCommandOptionType.ROLE]: string; // Could be a role ID or object - [ApplicationCommandOptionType.MENTIONABLE]: string; // Could be a mentionable ID or object - [ApplicationCommandOptionType.ATTACHMENT]: string; // Could represent attachment IDs - [ApplicationCommandOptionType.SUB_COMMAND]: never; // Handled by `options` - [ApplicationCommandOptionType.SUB_COMMAND_GROUP]: never; // Handled by `options` -}; - -// Recursive type to map ApplicationCommandOption to arguments -type ArgsFromOptions = { - [Option in T[number] as Option['name']]: Option extends { options: readonly ApplicationCommandOption[] } - ? ArgsFromOptions // Recursive inference for subcommands - : Option['choices'] extends readonly { value: infer U }[] - ? U // Infer from choices if defined - : Option['type'] extends keyof PrimitiveTypeMap - ? PrimitiveTypeMap[Option['type']] // Otherwise, map from ApplicationCommandOptionType - : unknown; -}; - -export type BaseHandler = { - args: T; - [Trait.execute]: (context: Context, args: ArgsFromOptions) => Promise | void; -}; - -// Helper function to infer type of `args` -export function createCommandHandler( - handler: BaseHandler -): BaseHandler { - return handler as BaseHandler; -} - -const handler = createCommandHandler({ - args: [ - { - type: 3, - name: "name", - description: "The name of the user", - required: true - }, - { - type: 3, - name: "age", - description: "The age of the user", - required: true, - choices: [ - { name: "18", value: "18" }, - { name: "19", value: "19" }, - { name: "20", value: "20" } - ] - } - ] as const, - [Trait.execute]: async (context, args) => { - console.log(args); - } -}); - -export type CommandHandlerFunction = (context: Context, args: any[]) => Promise | void; -export type EventHandlerFunction = (context: Context | null, payload: EventPayload) => Promise | void; - -export type HandlerFunction = CommandHandlerFunction | EventHandlerFunction; -export type Handler = HandlerFunction | BaseHandler; diff --git a/src/types/handler.ts b/src/types/handler.ts new file mode 100644 index 0000000..a4aa100 --- /dev/null +++ b/src/types/handler.ts @@ -0,0 +1,46 @@ +import type { ApplicationCommandOption, ApplicationCommandOptionType } from "./applicationCommand"; +import { Trait, type EventPayload } from "./common"; +import type { Context } from "./context"; + +type PrimitiveTypeMap = { + [ApplicationCommandOptionType.String]: string; + [ApplicationCommandOptionType.Integer]: number; + [ApplicationCommandOptionType.Boolean]: boolean; + [ApplicationCommandOptionType.Number]: number; + [ApplicationCommandOptionType.User]: string; // Could be a user ID or object depending on your use case + [ApplicationCommandOptionType.Channel]: string; // Could be a channel ID or object + [ApplicationCommandOptionType.Role]: string; // Could be a role ID or object + [ApplicationCommandOptionType.Mentionable]: string; // Could be a mentionable ID or object + [ApplicationCommandOptionType.Attachment]: string; // Could represent attachment IDs + [ApplicationCommandOptionType.SubCommand]: never; // Handled by `options` + [ApplicationCommandOptionType.SubCommandGroup]: never; // Handled by `options` +}; + +// Recursive type to map ApplicationCommandOption to arguments +type ArgsFromOptions = { + [Option in T[number] as Option['id']]: Option extends { options: readonly ApplicationCommandOption[] } + ? ArgsFromOptions // Recursive inference for subcommands + : Option['choices'] extends readonly { value: infer U }[] + ? U // Infer from choices if defined + : Option['type'] extends keyof PrimitiveTypeMap + ? PrimitiveTypeMap[Option['type']] // Otherwise, map from ApplicationCommandOptionType + : unknown; +}; + +export type CommandHandler = { + args: T; + [Trait.execute]: (context: Context, args: ArgsFromOptions) => Promise | void; +}; + +// Helper function to infer type of `args` +export function createCommandHandler( + handler: CommandHandler +): CommandHandler { + return handler as CommandHandler; +} + +export type CommandHandlerFunction = (context: Context, args: any[]) => Promise | void; +export type EventHandlerFunction = (context: Context | null, payload: EventPayload) => Promise | void; + +export type HandlerFunction = CommandHandlerFunction | EventHandlerFunction; +export type Handler = HandlerFunction | CommandHandler; diff --git a/testing/demo.test.ts b/testing/demo.test.ts index da465ca..7c7e344 100644 --- a/testing/demo.test.ts +++ b/testing/demo.test.ts @@ -1,4 +1,5 @@ import { Client, type Context, createCommandHandler, Expectation, Intent, type MessagePayload, Trait } from '../src/index'; +import { ApplicationCommandOptionType } from '../src/types/applicationCommand'; const client = new Client({ token: process.env.BOT_TOKEN!, @@ -20,32 +21,44 @@ client.chatCommands.register('ping', async (context: Context, args: any[]) => { await context.editMessage(message, `Pong! Latency: ${time}ms`); }); -const handler = createCommandHandler({ +client.slashCommands.register('ping', createCommandHandler({ args: [ { - type: 3, - name: "name", - description: "The name of the user", + type: ApplicationCommandOptionType.String, + name: "Name", + id: "name", + description: "Your name", required: true }, { - type: 3, - name: "age", - description: "The age of the user", - required: true, + type: ApplicationCommandOptionType.Integer, + name: "Age", + id: "age", + description: "Your age", + required: true + }, + { + type: ApplicationCommandOptionType.String, + name: "Favorite Animal", + id: "favorite_animal", + description: "Your favorite animal", + required: false, choices: [ - { name: "18", value: "18" }, - { name: "19", value: "19" }, - { name: "20", value: "20" } + { name: "Dog", value: "dog" }, + { name: "Cat", value: "cat" }, + { name: "Bird", value: "bird" }, + { name: "Shark", value: "shark" }, + { name: "Elephant", value: "elephant" }, + { name: "Tiger", value: "Tiger" }, + { name: "Lion", value: "lion" }, + { name: "Bear", value: "Bear" } ] } ] as const, [Trait.execute]: async (context, args) => { - context.reply(`You are ${args.name} and you are ${args.age} years old`, true); + context.reply(`You are ${args.name} and you are ${args.age} years old and your favorite animal is ${args.favorite_animal}`, true); } -}); - -client.slashCommands.register('ping', handler); +})); client.chatCommands.register('me', async (context: Context, _args: any[]) => { // Note: Testing repeated hydration