diff --git a/rewrite/commandManager.ts b/rewrite/commandManager.ts new file mode 100644 index 0000000..049b7fd --- /dev/null +++ b/rewrite/commandManager.ts @@ -0,0 +1,93 @@ +import type { + ArgumentTypeFromOptions, + Unicord, + UnicordArgumentDefinition, + UnicordCommandOptions, + UnicordEventContext, + UnicordHandler, +} from '.'; +import { UnicordCommandContext } from './context'; +import type { UnicordEventProcessor } from './eventProcessor'; +import { UnicordArgumentType, UnicordEventType } from './types/common'; + +type UnicordCommandDefinition = { + options: Options; + handler: UnicordHandler; +}; + +export const OptionConstructorMap = { + [UnicordArgumentType.String]: String, + [UnicordArgumentType.Integer]: Number, + [UnicordArgumentType.Boolean]: Boolean, + [UnicordArgumentType.Number]: Number, + [UnicordArgumentType.User]: String, // Could be a user ID or object depending on your use case + [UnicordArgumentType.Channel]: String, // Could be a channel ID or object + [UnicordArgumentType.Role]: String, // Could be a role ID or object + [UnicordArgumentType.Mentionable]: String, // Could be a mentionable ID or object + [UnicordArgumentType.Attachment]: String, // Could represent attachment IDs + [UnicordArgumentType.SubCommand]: String, // Handled by `options` + [UnicordArgumentType.SubCommandGroup]: String, // Handled by `options` +}; + +export class UnicordCommandManager { + private handlers = { + [UnicordEventType.ChatCommands]: new Map[]>(), + [UnicordEventType.ApplicationCommands]: new Map[]>(), + }; + + constructor(private readonly self: Unicord, private readonly eventProcessor: UnicordEventProcessor) { + this.handlers = { + [UnicordEventType.ChatCommands]: new Map(), + [UnicordEventType.ApplicationCommands]: new Map(), + }; + } + + private validateAndResolveArgs = async ( + args: any[], + definitions: Options['args'], + ): Promise> => { + // Zip the args and definition together + const zipped: [UnicordArgumentDefinition, any][] = args.map((arg, i) => [definitions[i], arg]); + // Validate the args + const validated: Record = {}; + // TODO: Attempt some type coresion/resolution here (mostly for text commands) + for (const [def, arg] of zipped) { + if (false) { + } else { + validated[def.name] = arg; + } + } + // TODO: Fix types so we don't need to cast here + return Promise.resolve(validated as ArgumentTypeFromOptions); + }; + + register( + type: UnicordEventType.ChatCommands | UnicordEventType.ApplicationCommands, + event: string, + handler: UnicordHandler, + options: Options = { args: [] } as unknown as Options, + ) { + if (this.handlers[type].has(event) === false) { + this.handlers[type].set(event, []); + } + + this.handlers[type].get(event)!.push({ options, handler }); + + // Generate a closure to wrap the handler + const wrappedHandler = async (context: UnicordCommandContext | UnicordEventContext, args: any[]) => { + console.log('Raw Args:', args); + console.log('options:', options.args); + const resolvedArgs = await this.validateAndResolveArgs(args, options.args); + console.log('Resolved Args:', resolvedArgs); + // TODO: Fix context type here + // @ts-expect-error will fix later + return handler(context, resolvedArgs); + }; + + this.eventProcessor.register(`${type}:${event}`, wrappedHandler); + return this.self; + } + + registerChatCommand = this.register.bind(this, UnicordEventType.ChatCommands); + registerApplicationCommand = this.register.bind(this, UnicordEventType.ApplicationCommands); +} diff --git a/rewrite/eventProcessor.ts b/rewrite/eventProcessor.ts index ec0c1f1..4d3bcd1 100644 --- a/rewrite/eventProcessor.ts +++ b/rewrite/eventProcessor.ts @@ -1,67 +1,15 @@ import { EventEmitter } from 'node:events'; import type { Unicord } from '.'; -import type { - UnicordCommandHandler, - UnicordCommandOptions, - UnicordEventContext, - ArgumentTypeFromOptions, - UnicordArgumentDefinition, - UnicordHandler, -} from './types/common'; -import { UnicordArgumentType } from './types/common'; import type { UnicordCommandContext } from './context'; - -export const OptionConstructorMap = { - [UnicordArgumentType.String]: String, - [UnicordArgumentType.Integer]: Number, - [UnicordArgumentType.Boolean]: Boolean, - [UnicordArgumentType.Number]: Number, - [UnicordArgumentType.User]: String, // Could be a user ID or object depending on your use case - [UnicordArgumentType.Channel]: String, // Could be a channel ID or object - [UnicordArgumentType.Role]: String, // Could be a role ID or object - [UnicordArgumentType.Mentionable]: String, // Could be a mentionable ID or object - [UnicordArgumentType.Attachment]: String, // Could represent attachment IDs - [UnicordArgumentType.SubCommand]: String, // Handled by `options` - [UnicordArgumentType.SubCommandGroup]: String, // Handled by `options` -}; +import type { UnicordCommandOptions, UnicordEventContext, UnicordHandler } from './types/common'; export class UnicordEventProcessor extends EventEmitter { constructor(private readonly self: Unicord) { super(); } - private validateAndResolveArgs = async ( - args: any[], - definitions: Options['args'], - ): Promise> => { - // Zip the args and definition together - const zipped: [UnicordArgumentDefinition, any][] = args.map((arg, i) => [definitions[i], arg]); - // Validate the args - const validated: Record = {}; - // TODO: Attempt some type coresion/resolution here (mostly for text commands) - for (const [def, arg] of zipped) { - if (false) { - } else { - validated[def.name] = arg; - } - } - // TODO: Fix types so we don't need to cast here - return Promise.resolve(validated as ArgumentTypeFromOptions); - }; - - register( - event: string, - handler: UnicordHandler, - options: Options, - ) { - // Generate a closure to wrap the handler - const wrappedHandler = async (context: UnicordCommandContext | UnicordEventContext, args: any[]) => { - const resolvedArgs = await this.validateAndResolveArgs(args, options.args); - // TODO: Fix context type here - // @ts-expect-error will fix later - return handler(context, resolvedArgs); - }; - return super.on(event, wrappedHandler); + register(event: string, handler: UnicordHandler) { + return super.on(event, handler); } emit(event: string, context: UnicordCommandContext | UnicordEventContext, args: any[]): boolean { diff --git a/rewrite/index.ts b/rewrite/index.ts index 1a9ae5f..8004762 100644 --- a/rewrite/index.ts +++ b/rewrite/index.ts @@ -1,11 +1,11 @@ import { UnicordChannelsCache, UnicordGuildsCache, UnicordUsersCache } from './caches'; +import { UnicordCommandManager } from './commandManager'; import type { UnicordCommandContext } from './context'; import { UnicordDiscordAPI } from './discordAPI'; import { UnicordEventProcessor } from './eventProcessor'; import { UnicordGatewayManager } from './gatewayManager'; import { hydrate } from './hydrator'; import type { - UnicordCommandHandler, UnicordCommandOptions, UnicordConfig, UnicordConfigWithDefaults, @@ -27,6 +27,12 @@ export class Unicord { readonly discordAPI: UnicordDiscordAPI; private readonly eventProcessor: UnicordEventProcessor; private readonly gatewayManager: UnicordGatewayManager; + // TODO: split command management out of the event processor + // This way we can properly store and manage registered commands + // This will enable features like command listing, removal, autogenerated help, etc. + // We can also go registration with the Discord API here + // This will also eliminate the weird helper methods on the Unicord class itself + private readonly commandManager: UnicordCommandManager; readonly caches: { readonly users: UnicordUsersCache; readonly channels: UnicordChannelsCache; @@ -38,6 +44,7 @@ export class Unicord { this.discordAPI = new UnicordDiscordAPI(this); this.eventProcessor = new UnicordEventProcessor(this); this.gatewayManager = new UnicordGatewayManager(this); + this.commandManager = new UnicordCommandManager(this, this.eventProcessor); this.caches = { users: new UnicordUsersCache(this), channels: new UnicordChannelsCache(this), @@ -59,41 +66,31 @@ export class Unicord { return hydrate.bind(null, this); } - private registerEvent( + private __onEvent( type: UnicordEventType, event: string, handler: UnicordHandler, - options: Options = { args: [] } as unknown as Options, ) { - return this.eventProcessor.register(`${type}:${event}`, handler, options); + return this.eventProcessor.register(`${type}:${event}`, handler); } emit(type: UnicordEventType, event: string, context: UnicordCommandContext | UnicordEventContext, args: any[]) { this.eventProcessor.emit(`${type}:${event}`, context, args); } - registerChatCommand( - event: string, - handler: UnicordCommandHandler, - options: Options = { args: [] } as unknown as Options, - ): this { - this.registerEvent(UnicordEventType.ChatCommands, event, handler, options); - return this; + // TODO: type signature from registerChatCommand needs to transfer to here + get registerChatCommand() { + return this.commandManager.registerChatCommand; } - registerApplicationCommand( - event: string, - handler: UnicordCommandHandler, - options: Options = { args: [] } as unknown as Options, - ): this { - // TODO: Register with Discord API - this.registerEvent(UnicordEventType.ApplicationCommands, event, handler, options); - return this; + // TODO: type signature from registerApplicationCommand needs to transfer to here + get registerApplicationCommand() { + return this.commandManager.registerApplicationCommand; } // TODO: should have a different signature for events that are not commands onEvent(event: string, handler: UnicordEventHandler): this { - this.registerEvent(UnicordEventType.SystemEvent, event, handler); + this.__onEvent(UnicordEventType.SystemEvent, event, handler); return this; }