mirror of
https://github.com/jaronpate/unicord.git
synced 2026-10-18 21:47:40 +00:00
refactor: update command and event handling, improve logging, and enhance argument validation, add client inheritance
This commit is contained in:
+67
-29
@@ -10,13 +10,14 @@ import { UnicordCommandContext } from './context';
|
|||||||
import type { UnicordEventProcessor } from './eventProcessor';
|
import type { UnicordEventProcessor } from './eventProcessor';
|
||||||
import { Embed } from './types';
|
import { Embed } from './types';
|
||||||
import { UnicordArgumentType, UnicordEventType } from './types/common';
|
import { UnicordArgumentType, UnicordEventType } from './types/common';
|
||||||
|
import { log } from './utils';
|
||||||
|
|
||||||
type UnicordCommandDefinition<Options extends UnicordCommandOptions> = {
|
type UnicordCommandDefinition<Options extends UnicordCommandOptions> = {
|
||||||
options: Options;
|
options: Options;
|
||||||
handler: UnicordHandler<Options>;
|
handler: UnicordCommandHandler<Options>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OptionConstructorMap = {
|
export const ArgumentConstructorMap = {
|
||||||
[UnicordArgumentType.String]: String,
|
[UnicordArgumentType.String]: String,
|
||||||
[UnicordArgumentType.Integer]: Number,
|
[UnicordArgumentType.Integer]: Number,
|
||||||
[UnicordArgumentType.Boolean]: Boolean,
|
[UnicordArgumentType.Boolean]: Boolean,
|
||||||
@@ -32,8 +33,14 @@ export const OptionConstructorMap = {
|
|||||||
|
|
||||||
export class UnicordCommandManager {
|
export class UnicordCommandManager {
|
||||||
private handlers = {
|
private handlers = {
|
||||||
[UnicordEventType.ChatCommands]: new Map<string, UnicordCommandDefinition<any>[]>(),
|
[UnicordEventType.ChatCommands]: new Map<
|
||||||
[UnicordEventType.ApplicationCommands]: new Map<string, UnicordCommandDefinition<any>[]>(),
|
string,
|
||||||
|
UnicordCommandDefinition<any> & { wrappedHandler: UnicordCommandHandler<any> }
|
||||||
|
>(),
|
||||||
|
[UnicordEventType.ApplicationCommands]: new Map<
|
||||||
|
string,
|
||||||
|
UnicordCommandDefinition<any> & { wrappedHandler: UnicordCommandHandler<any> }
|
||||||
|
>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(private readonly self: Unicord, private readonly eventProcessor: UnicordEventProcessor) {
|
constructor(private readonly self: Unicord, private readonly eventProcessor: UnicordEventProcessor) {
|
||||||
@@ -42,15 +49,36 @@ export class UnicordCommandManager {
|
|||||||
[UnicordEventType.ApplicationCommands]: new Map(),
|
[UnicordEventType.ApplicationCommands]: new Map(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register the default help command
|
this.registerDefaultHelpCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async validateAndResolveArgs<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']>);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerDefaultHelpCommand() {
|
||||||
this.registerChatCommand(
|
this.registerChatCommand(
|
||||||
'help',
|
'help',
|
||||||
async (context) => {
|
async (context) => {
|
||||||
// Just list chat commands for now
|
// Just list chat commands for now
|
||||||
const reply = new Embed().setTitle('Available Commands');
|
const reply = new Embed().setTitle('Available Commands');
|
||||||
|
|
||||||
for (const [command, defs] of this.handlers[UnicordEventType.ChatCommands]) {
|
for (const [command, def] of this.handlers[UnicordEventType.ChatCommands]) {
|
||||||
for (const def of defs) {
|
|
||||||
let fieldValue = def.options.description ?? 'No description';
|
let fieldValue = def.options.description ?? 'No description';
|
||||||
fieldValue += '\n';
|
fieldValue += '\n';
|
||||||
|
|
||||||
@@ -74,7 +102,6 @@ export class UnicordCommandManager {
|
|||||||
value: fieldValue,
|
value: fieldValue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await context.reply(reply.toMessage());
|
await context.reply(reply.toMessage());
|
||||||
},
|
},
|
||||||
@@ -85,24 +112,35 @@ export class UnicordCommandManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private validateAndResolveArgs = async <const Options extends UnicordCommandOptions>(
|
all() {
|
||||||
args: any[],
|
return this.handlers;
|
||||||
definitions: Options['args'],
|
}
|
||||||
): Promise<ArgumentTypeFromOptions<Options['args']>> => {
|
|
||||||
// Zip the args and definition together
|
has(type: UnicordEventType.ChatCommands | UnicordEventType.ApplicationCommands, event: string) {
|
||||||
const zipped: [UnicordArgumentDefinition, any][] = args.map((arg, i) => [definitions[i], arg]);
|
return this.handlers[type].has(event);
|
||||||
// Validate the args
|
}
|
||||||
const validated: Record<string, any> = {};
|
|
||||||
// TODO: Attempt some type coresion/resolution here (mostly for text commands)
|
merge(other: UnicordCommandManager) {
|
||||||
for (const [def, arg] of zipped) {
|
const handlers = other.all();
|
||||||
if (false) {
|
|
||||||
} else {
|
for (const type in handlers) {
|
||||||
validated[def.name] = arg;
|
const typeKey = type as UnicordEventType.ChatCommands | UnicordEventType.ApplicationCommands;
|
||||||
|
for (const [event, definition] of handlers[typeKey]) {
|
||||||
|
this.register(typeKey, event, definition.handler, definition.options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: Fix types so we don't need to cast here
|
|
||||||
return Promise.resolve(validated as ArgumentTypeFromOptions<Options['args']>);
|
this.registerDefaultHelpCommand();
|
||||||
};
|
}
|
||||||
|
|
||||||
|
unregister(type: UnicordEventType.ChatCommands | UnicordEventType.ApplicationCommands, event: string) {
|
||||||
|
const handler = this.handlers[type].get(event);
|
||||||
|
if (handler) {
|
||||||
|
this.eventProcessor.unregister(`${type}:${event}`, handler.wrappedHandler);
|
||||||
|
this.handlers[type].delete(event);
|
||||||
|
}
|
||||||
|
return this.self;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Register application commands with the DiscordAPI
|
// TODO: Register application commands with the DiscordAPI
|
||||||
register<const Options extends UnicordCommandOptions>(
|
register<const Options extends UnicordCommandOptions>(
|
||||||
@@ -111,19 +149,19 @@ export class UnicordCommandManager {
|
|||||||
handler: UnicordCommandHandler<Options>,
|
handler: UnicordCommandHandler<Options>,
|
||||||
options: Options = { args: [] } as unknown as Options,
|
options: Options = { args: [] } as unknown as Options,
|
||||||
) {
|
) {
|
||||||
if (this.handlers[type].has(event) === false) {
|
if (this.has(type, event)) {
|
||||||
this.handlers[type].set(event, []);
|
this.unregister(type, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handlers[type].get(event)!.push({ options, handler });
|
|
||||||
|
|
||||||
// Generate a closure to wrap the handler
|
|
||||||
const wrappedHandler = (async (context: UnicordCommandContext, args: any[]) => {
|
const wrappedHandler = (async (context: UnicordCommandContext, args: any[]) => {
|
||||||
const resolvedArgs = await this.validateAndResolveArgs<Options>(args, options.args);
|
const resolvedArgs = await this.validateAndResolveArgs<Options>(args, options.args);
|
||||||
return handler(context, resolvedArgs);
|
return handler(context, resolvedArgs);
|
||||||
}) as UnicordCommandHandler<Options>;
|
}) as UnicordCommandHandler<Options>;
|
||||||
|
|
||||||
|
this.handlers[type].set(event, { options, handler, wrappedHandler });
|
||||||
|
|
||||||
this.eventProcessor.register(`${type}:${event}`, wrappedHandler);
|
this.eventProcessor.register(`${type}:${event}`, wrappedHandler);
|
||||||
|
|
||||||
return this.self;
|
return this.self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export class UnicordDiscordAPI {
|
|||||||
const limitBucket = response.headers.get('X-RateLimit-Bucket');
|
const limitBucket = response.headers.get('X-RateLimit-Bucket');
|
||||||
|
|
||||||
// Log rate limit info for debugging
|
// Log rate limit info for debugging
|
||||||
log(`Rate Limit: ${remaining}/${limit} - Reset: ${reset} (${resetSeconds}s) - Bucket: ${limitBucket}`);
|
log(`Rate Limit: ${remaining}/${limit} - Reset: ${resetSeconds}s - Bucket: ${limitBucket}`);
|
||||||
|
|
||||||
// If status is 429, we are being rate limited
|
// If status is 429, we are being rate limited
|
||||||
if (response.status === 429) {
|
if (response.status === 429) {
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ export class UnicordEventProcessor extends EventEmitter {
|
|||||||
return super.on(event, handler);
|
return super.on(event, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unregister<const Options extends UnicordCommandOptions>(event: string, handler: UnicordHandler<Options>) {
|
||||||
|
return super.off(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
emit(event: string, context: UnicordCommandContext | UnicordEventContext, args: any[]): boolean {
|
emit(event: string, context: UnicordCommandContext | UnicordEventContext, args: any[]): boolean {
|
||||||
return super.emit(event, context, args);
|
return super.emit(event, context, args);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export class UnicordGatewayManager {
|
|||||||
this.indentify();
|
this.indentify();
|
||||||
} else if (code === 11) {
|
} else if (code === 11) {
|
||||||
// Heartbeat ACK
|
// Heartbeat ACK
|
||||||
log('Heartbeat ACK');
|
// log('Heartbeat ACK');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+34
-7
@@ -17,7 +17,7 @@ import type {
|
|||||||
import { Intent, UnicordEventType } from './types/common';
|
import { Intent, UnicordEventType } from './types/common';
|
||||||
import type { DiscordMessage } from './types/message';
|
import type { DiscordMessage } from './types/message';
|
||||||
import { Message } from './types/message';
|
import { Message } from './types/message';
|
||||||
import { defaults } from './utils';
|
import { setDefaults } from './utils';
|
||||||
|
|
||||||
const UNICORD_CONFIG_DEFAULTS = {
|
const UNICORD_CONFIG_DEFAULTS = {
|
||||||
intents: [Intent.DEFAULT],
|
intents: [Intent.DEFAULT],
|
||||||
@@ -36,7 +36,7 @@ export class Unicord {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor(userConfig: UnicordConfig = {}) {
|
constructor(userConfig: UnicordConfig = {}) {
|
||||||
this.config = defaults(userConfig, UNICORD_CONFIG_DEFAULTS) as UnicordConfigWithDefaults;
|
this.config = setDefaults<UnicordConfigWithDefaults>(userConfig, UNICORD_CONFIG_DEFAULTS);
|
||||||
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);
|
||||||
@@ -70,6 +70,30 @@ export class Unicord {
|
|||||||
return this.eventProcessor.register(`${type}:${event}`, handler);
|
return this.eventProcessor.register(`${type}:${event}`, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use(client: Unicord): this {
|
||||||
|
// const handlers = client.commandManager.all();
|
||||||
|
|
||||||
|
// for (const type in handlers) {
|
||||||
|
// const typeKey = type as UnicordEventType.ChatCommands | UnicordEventType.ApplicationCommands;
|
||||||
|
// for (const [event, definition] of handlers[typeKey]) {
|
||||||
|
// this.commandManager.register(
|
||||||
|
// typeKey,
|
||||||
|
// event,
|
||||||
|
// definition.handler.bind(this.commandManager),
|
||||||
|
// definition.options,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.commandManager.registerDefaultHelpCommand();
|
||||||
|
|
||||||
|
this.commandManager.merge(client.commandManager);
|
||||||
|
|
||||||
|
// TODO: inherit event listeners too
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -79,16 +103,18 @@ export class Unicord {
|
|||||||
event: string,
|
event: string,
|
||||||
handler: UnicordCommandHandler<Options>,
|
handler: UnicordCommandHandler<Options>,
|
||||||
options: Options = { args: [] } as unknown as Options,
|
options: Options = { args: [] } as unknown as Options,
|
||||||
) {
|
): this {
|
||||||
return this.commandManager.registerChatCommand(event, handler, options);
|
this.commandManager.registerChatCommand(event, handler, options);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerApplicationCommand<const Options extends UnicordCommandOptions>(
|
registerApplicationCommand<const Options extends UnicordCommandOptions>(
|
||||||
event: string,
|
event: string,
|
||||||
handler: UnicordCommandHandler<Options>,
|
handler: UnicordCommandHandler<Options>,
|
||||||
options: Options = { args: [] } as unknown as Options,
|
options: Options = { args: [] } as unknown as Options,
|
||||||
) {
|
): this {
|
||||||
return this.commandManager.registerApplicationCommand(event, handler, options);
|
this.commandManager.registerApplicationCommand(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
|
||||||
@@ -107,7 +133,7 @@ export class Unicord {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
login(token: string | undefined = this.config.token) {
|
login(token: string | undefined = this.config.token): this {
|
||||||
if (token === null || token === undefined) {
|
if (token === null || token === undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'You must provide a token to login. Either in the Unicord config or when calling `login()`',
|
'You must provide a token to login. Either in the Unicord config or when calling `login()`',
|
||||||
@@ -117,6 +143,7 @@ export class Unicord {
|
|||||||
this.config.token = token;
|
this.config.token = token;
|
||||||
|
|
||||||
this.gatewayManager.connect();
|
this.gatewayManager.connect();
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+28
-5
@@ -30,11 +30,30 @@ export enum UnicordArgumentType {
|
|||||||
Attachment = 11,
|
Attachment = 11,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Old type
|
||||||
|
// export type ApplicationCommandOption = {
|
||||||
|
// type: ApplicationCommandOptionType;
|
||||||
|
// name: string;
|
||||||
|
// id: string;
|
||||||
|
// name_localizations?: Record<string, string> | null;
|
||||||
|
// description: string;
|
||||||
|
// description_localizations?: Record<string, string> | null;
|
||||||
|
// required?: boolean;
|
||||||
|
// choices?: Array<{ name: string; value: string | number }>;
|
||||||
|
// options?: ApplicationCommandOption[]; // Recursive structure for subcommands
|
||||||
|
// channel_types?: ChannelType[];
|
||||||
|
// min_value?: number;
|
||||||
|
// max_value?: number;
|
||||||
|
// min_length?: number;
|
||||||
|
// max_length?: number;
|
||||||
|
// autocomplete?: boolean;
|
||||||
|
// };
|
||||||
export interface UnicordArgumentDefinition {
|
export interface UnicordArgumentDefinition {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
type: UnicordArgumentType;
|
type: UnicordArgumentType;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
|
choices?: Array<{ name: string; value: string | number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnicordCommandOptions {
|
export interface UnicordCommandOptions {
|
||||||
@@ -56,7 +75,7 @@ export interface UnicordBaseContext extends Record<string, any> {
|
|||||||
|
|
||||||
export interface UnicordEventContext extends UnicordBaseContext {}
|
export interface UnicordEventContext extends UnicordBaseContext {}
|
||||||
|
|
||||||
export type OptionTypeMap = {
|
export type DefinitionTypeMap = {
|
||||||
[UnicordArgumentType.String]: string;
|
[UnicordArgumentType.String]: string;
|
||||||
[UnicordArgumentType.Integer]: number;
|
[UnicordArgumentType.Integer]: number;
|
||||||
[UnicordArgumentType.Boolean]: boolean;
|
[UnicordArgumentType.Boolean]: boolean;
|
||||||
@@ -71,10 +90,14 @@ export type OptionTypeMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ArgumentTypeFromOptions<T extends readonly UnicordArgumentDefinition[]> = {
|
export type ArgumentTypeFromOptions<T extends readonly UnicordArgumentDefinition[]> = {
|
||||||
[Option in T[number] as Option['name']]: Option['type'] extends keyof OptionTypeMap
|
[Definition in T[number] as Definition['name']]: Definition['type'] extends keyof DefinitionTypeMap
|
||||||
? Option['required'] extends true // If required, keep as is; otherwise, make optional
|
? Definition['choices'] extends readonly { value: infer U }[]
|
||||||
? OptionTypeMap[Option['type']]
|
? Definition['required'] extends true // If required, keep as is; otherwise, make optional
|
||||||
: OptionTypeMap[Option['type']] | undefined
|
? U
|
||||||
|
: U | undefined
|
||||||
|
: Definition['required'] extends true // If required, keep as is; otherwise, make optional
|
||||||
|
? DefinitionTypeMap[Definition['type']]
|
||||||
|
: DefinitionTypeMap[Definition['type']] | undefined
|
||||||
: unknown;
|
: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+8
-2
@@ -1,4 +1,10 @@
|
|||||||
export function defaults(original: Record<string, any>, defaults: Record<string, any>) {
|
/**
|
||||||
|
* Sets top level properties when undefined on the original object — cretates a new object, does not modify the original
|
||||||
|
* @param original original object
|
||||||
|
* @param defaults defailts to set on the object
|
||||||
|
* @returns a new object with top level defaults set
|
||||||
|
*/
|
||||||
|
export function setDefaults<T>(original: Record<string, any>, defaults: Record<string, any>): T {
|
||||||
const final: Record<string, any> = {};
|
const final: Record<string, any> = {};
|
||||||
|
|
||||||
for (const prop of Object.keys(defaults)) {
|
for (const prop of Object.keys(defaults)) {
|
||||||
@@ -9,7 +15,7 @@ export function defaults(original: Record<string, any>, defaults: Record<string,
|
|||||||
final[prop] = original[prop];
|
final[prop] = original[prop];
|
||||||
}
|
}
|
||||||
|
|
||||||
return final;
|
return final as T;
|
||||||
}
|
}
|
||||||
export const isNil = (value: any): value is null | undefined => value === null || value === undefined;
|
export const isNil = (value: any): value is null | undefined => value === null || value === undefined;
|
||||||
export const exists = <T>(value: T | null | undefined): value is NonNullable<T> => !isNil(value);
|
export const exists = <T>(value: T | null | undefined): value is NonNullable<T> => !isNil(value);
|
||||||
|
|||||||
@@ -73,11 +73,32 @@ const client = new Unicord({
|
|||||||
console.log(`Logged in as ${payload.user.username}`);
|
console.log(`Logged in as ${payload.user.username}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.login(DISCORD_CLIENT_TOKEN);
|
// client.login(DISCORD_CLIENT_TOKEN);
|
||||||
|
|
||||||
// Allow use of "child" instances inside parent instance
|
// Allow use of "child" instances inside parent instance
|
||||||
// const client2 = new Unicord().chatCommand('ping', async (context, _) => {
|
const etc = new Unicord().registerChatCommand(
|
||||||
// await context.reply('Pong!');
|
'zing',
|
||||||
// });
|
async (context, args) => {
|
||||||
|
await context.reply(`Zing! You got served!` + args.fav_amimal ? ` One ${args.fav_amimal} please!` : '');
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'fav_amimal',
|
||||||
|
description: 'Your favorite animal',
|
||||||
|
type: UnicordArgumentType.String,
|
||||||
|
required: false,
|
||||||
|
choices: [
|
||||||
|
{ name: 'Dog', value: 'dog' },
|
||||||
|
{ name: 'Cat', value: 'cat' },
|
||||||
|
{ name: 'Hamster', value: 'hamster' },
|
||||||
|
{ name: 'Dolphin', value: 'dolphin' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// new Unicord().use(client2).login(DISCORD_CLIENT_TOKEN);
|
client.use(etc);
|
||||||
|
|
||||||
|
client.login();
|
||||||
Reference in New Issue
Block a user