mirror of
https://github.com/jaronpate/unicord.git
synced 2026-10-18 21:47:40 +00:00
refactored command management into a seperate service for future features
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user