refactored command management into a seperate service for future features

This commit is contained in:
2025-10-27 02:38:45 -05:00
parent 6c59334c27
commit e711473b7f
3 changed files with 113 additions and 75 deletions
+93
View File
@@ -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 extends UnicordCommandOptions> = {
options: Options;
handler: UnicordHandler<Options>;
};
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<string, UnicordCommandDefinition<any>[]>(),
[UnicordEventType.ApplicationCommands]: new Map<string, UnicordCommandDefinition<any>[]>(),
};
constructor(private readonly self: Unicord, private readonly eventProcessor: UnicordEventProcessor) {
this.handlers = {
[UnicordEventType.ChatCommands]: new Map(),
[UnicordEventType.ApplicationCommands]: new Map(),
};
}
private validateAndResolveArgs = async <const Options extends UnicordCommandOptions>(
args: any[],
definitions: Options['args'],
): Promise<ArgumentTypeFromOptions<Options['args']>> => {
// Zip the args and definition together
const zipped: [UnicordArgumentDefinition, any][] = args.map((arg, i) => [definitions[i], arg]);
// Validate the args
const validated: Record<string, any> = {};
// 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<Options['args']>);
};
register<const Options extends UnicordCommandOptions>(
type: UnicordEventType.ChatCommands | UnicordEventType.ApplicationCommands,
event: string,
handler: UnicordHandler<Options>,
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<Options>(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);
}
+3 -55
View File
@@ -1,67 +1,15 @@
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
import type { Unicord } from '.'; 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'; import type { UnicordCommandContext } from './context';
import type { UnicordCommandOptions, UnicordEventContext, UnicordHandler } from './types/common';
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 UnicordEventProcessor extends EventEmitter { export class UnicordEventProcessor extends EventEmitter {
constructor(private readonly self: Unicord) { constructor(private readonly self: Unicord) {
super(); super();
} }
private validateAndResolveArgs = async <const Options extends UnicordCommandOptions>( register<const Options extends UnicordCommandOptions>(event: string, handler: UnicordHandler<Options>) {
args: any[], return super.on(event, handler);
definitions: Options['args'],
): Promise<ArgumentTypeFromOptions<Options['args']>> => {
// Zip the args and definition together
const zipped: [UnicordArgumentDefinition, any][] = args.map((arg, i) => [definitions[i], arg]);
// Validate the args
const validated: Record<string, any> = {};
// 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<Options['args']>);
};
register<const Options extends UnicordCommandOptions>(
event: string,
handler: UnicordHandler<Options>,
options: Options,
) {
// Generate a closure to wrap the handler
const wrappedHandler = async (context: UnicordCommandContext | UnicordEventContext, args: any[]) => {
const resolvedArgs = await this.validateAndResolveArgs<Options>(args, options.args);
// TODO: Fix context type here
// @ts-expect-error will fix later
return handler(context, resolvedArgs);
};
return super.on(event, wrappedHandler);
} }
emit(event: string, context: UnicordCommandContext | UnicordEventContext, args: any[]): boolean { emit(event: string, context: UnicordCommandContext | UnicordEventContext, args: any[]): boolean {
+17 -20
View File
@@ -1,11 +1,11 @@
import { UnicordChannelsCache, UnicordGuildsCache, UnicordUsersCache } from './caches'; import { UnicordChannelsCache, UnicordGuildsCache, UnicordUsersCache } from './caches';
import { UnicordCommandManager } from './commandManager';
import type { UnicordCommandContext } from './context'; import type { UnicordCommandContext } from './context';
import { UnicordDiscordAPI } from './discordAPI'; import { UnicordDiscordAPI } from './discordAPI';
import { UnicordEventProcessor } from './eventProcessor'; import { UnicordEventProcessor } from './eventProcessor';
import { UnicordGatewayManager } from './gatewayManager'; import { UnicordGatewayManager } from './gatewayManager';
import { hydrate } from './hydrator'; import { hydrate } from './hydrator';
import type { import type {
UnicordCommandHandler,
UnicordCommandOptions, UnicordCommandOptions,
UnicordConfig, UnicordConfig,
UnicordConfigWithDefaults, UnicordConfigWithDefaults,
@@ -27,6 +27,12 @@ export class Unicord {
readonly discordAPI: UnicordDiscordAPI; readonly discordAPI: UnicordDiscordAPI;
private readonly eventProcessor: UnicordEventProcessor; private readonly eventProcessor: UnicordEventProcessor;
private readonly gatewayManager: UnicordGatewayManager; 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 caches: {
readonly users: UnicordUsersCache; readonly users: UnicordUsersCache;
readonly channels: UnicordChannelsCache; readonly channels: UnicordChannelsCache;
@@ -38,6 +44,7 @@ export class Unicord {
this.discordAPI = new UnicordDiscordAPI(this); this.discordAPI = new UnicordDiscordAPI(this);
this.eventProcessor = new UnicordEventProcessor(this); this.eventProcessor = new UnicordEventProcessor(this);
this.gatewayManager = new UnicordGatewayManager(this); this.gatewayManager = new UnicordGatewayManager(this);
this.commandManager = new UnicordCommandManager(this, this.eventProcessor);
this.caches = { this.caches = {
users: new UnicordUsersCache(this), users: new UnicordUsersCache(this),
channels: new UnicordChannelsCache(this), channels: new UnicordChannelsCache(this),
@@ -59,41 +66,31 @@ export class Unicord {
return hydrate.bind(null, this); return hydrate.bind(null, this);
} }
private registerEvent<Options extends UnicordCommandOptions>( private __onEvent<Options extends UnicordCommandOptions>(
type: UnicordEventType, type: UnicordEventType,
event: string, event: string,
handler: UnicordHandler<Options>, handler: UnicordHandler<Options>,
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[]) { emit(type: UnicordEventType, event: string, context: UnicordCommandContext | UnicordEventContext, args: any[]) {
this.eventProcessor.emit(`${type}:${event}`, context, args); this.eventProcessor.emit(`${type}:${event}`, context, args);
} }
registerChatCommand<const Options extends UnicordCommandOptions>( // TODO: type signature from registerChatCommand needs to transfer to here
event: string, get registerChatCommand() {
handler: UnicordCommandHandler<Options>, return this.commandManager.registerChatCommand;
options: Options = { args: [] } as unknown as Options,
): this {
this.registerEvent(UnicordEventType.ChatCommands, event, handler, options);
return this;
} }
registerApplicationCommand<const Options extends UnicordCommandOptions>( // TODO: type signature from registerApplicationCommand needs to transfer to here
event: string, get registerApplicationCommand() {
handler: UnicordCommandHandler<Options>, return this.commandManager.registerApplicationCommand;
options: Options = { args: [] } as unknown as Options,
): this {
// TODO: Register with Discord API
this.registerEvent(UnicordEventType.ApplicationCommands, event, handler, options);
return this;
} }
// TODO: should have a different signature for events that are not commands // TODO: should have a different signature for events that are not commands
onEvent(event: string, handler: UnicordEventHandler): this { onEvent(event: string, handler: UnicordEventHandler): this {
this.registerEvent(UnicordEventType.SystemEvent, event, handler); this.__onEvent(UnicordEventType.SystemEvent, event, handler);
return this; return this;
} }