feat: custom events

This commit is contained in:
MARCROCK22 2024-06-04 21:29:02 +00:00
parent 625e400c20
commit 349628a587
4 changed files with 201 additions and 169 deletions

View File

@ -1,22 +1,25 @@
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import type { Awaitable, CamelCase, SnakeCase } from '../common'; import type { Awaitable, CamelCase } from '../common';
import type { ClientNameEvents, GatewayEvents } from '../events'; import type { CallbackEventHandler, CustomEventsKeys, GatewayEvents } from '../events';
import type { ClientEvents } from '../events/hooks';
import { error } from 'node:console'; import { error } from 'node:console';
type SnakeCaseClientNameEvents = Uppercase<SnakeCase<ClientNameEvents>>; export type AllClientEvents = CustomEventsKeys | GatewayEvents;
export type ParseClientEventName<T extends AllClientEvents> = T extends CustomEventsKeys ? T : CamelCase<T>;
type RunData<T extends SnakeCaseClientNameEvents> = { type RunData<T extends AllClientEvents> = {
options: { options: {
event: T; event: T;
idle?: number; idle?: number;
timeout?: number; timeout?: number;
onStop?: (reason: string) => unknown; onStop?: (reason: string) => unknown;
onStopError?: (reason: string, error: unknown) => unknown; onStopError?: (reason: string, error: unknown) => unknown;
filter: (arg: Awaited<ClientEvents[CamelCase<Lowercase<T>>]>) => Awaitable<boolean>; filter: (arg: Awaited<Parameters<CallbackEventHandler[ParseClientEventName<T>]>[0]>) => Awaitable<boolean>;
run: (arg: Awaited<ClientEvents[CamelCase<Lowercase<T>>]>, stop: (reason?: string) => void) => unknown; run: (
arg: Awaited<Parameters<CallbackEventHandler[ParseClientEventName<T>]>[0]>,
stop: (reason?: string) => void,
) => unknown;
onRunError?: ( onRunError?: (
arg: Awaited<ClientEvents[CamelCase<Lowercase<T>>]>, arg: Awaited<Parameters<CallbackEventHandler[ParseClientEventName<T>]>[0]>,
error: unknown, error: unknown,
stop: (reason?: string) => void, stop: (reason?: string) => void,
) => unknown; ) => unknown;
@ -27,9 +30,9 @@ type RunData<T extends SnakeCaseClientNameEvents> = {
}; };
export class Collectors { export class Collectors {
readonly values = new Map<SnakeCaseClientNameEvents, RunData<any>[]>(); readonly values = new Map<AllClientEvents, RunData<any>[]>();
private generateRandomUUID(name: SnakeCaseClientNameEvents) { private generateRandomUUID(name: AllClientEvents) {
const collectors = this.values.get(name); const collectors = this.values.get(name);
if (!collectors) return '*'; if (!collectors) return '*';
@ -42,7 +45,7 @@ export class Collectors {
return nonce; return nonce;
} }
create<T extends SnakeCaseClientNameEvents>(options: RunData<T>['options']) { create<T extends AllClientEvents>(options: RunData<T>['options']) {
const nonce = this.generateRandomUUID(options.event); const nonce = this.generateRandomUUID(options.event);
if (!this.values.has(options.event)) { if (!this.values.has(options.event)) {
@ -71,7 +74,7 @@ export class Collectors {
return options; return options;
} }
private async delete(name: SnakeCaseClientNameEvents, nonce: string, reason = 'unknown') { private async delete(name: AllClientEvents, nonce: string, reason = 'unknown') {
const collectors = this.values.get(name); const collectors = this.values.get(name);
if (!collectors?.length) { if (!collectors?.length) {
@ -93,20 +96,23 @@ export class Collectors {
} }
/**@internal */ /**@internal */
async run<T extends GatewayEvents>(name: T, data: Awaited<ClientEvents[CamelCase<Lowercase<T>>]>) { async run<T extends AllClientEvents>(
name: T,
data: Awaited<Parameters<CallbackEventHandler[ParseClientEventName<T>]>[0]>,
) {
const collectors = this.values.get(name); const collectors = this.values.get(name);
if (!collectors) return; if (!collectors) return;
for (const i of collectors) { for (const i of collectors) {
if (await i.options.filter(data)) { if (await i.options.filter(data as never)) {
i.idle?.refresh(); i.idle?.refresh();
const stop = (reason = 'unknown') => { const stop = (reason = 'unknown') => {
return this.delete(i.options.event, i.nonce, reason); return this.delete(i.options.event, i.nonce, reason);
}; };
try { try {
await i.options.run(data, stop); await i.options.run(data as never, stop);
} catch (e) { } catch (e) {
await i.options.onRunError?.(data, e, stop); await i.options.onRunError?.(data as never, e, stop);
} }
break; break;
} }

View File

@ -1,26 +1,26 @@
import type { UsingClient } from '../commands'; import type { UsingClient } from '../commands';
import type { ClientEvents } from './hooks'; import type { ClientEvents } from './hooks';
export interface DeclareEventsOptions { export interface CustomEvents {}
name: `${keyof ClientEvents}`; export type ClientNameEvents = Extract<keyof ClientEvents, string>;
once?: boolean; export type CustomEventsKeys = Extract<keyof CustomEvents, string>;
}
export type ClientNameEvents = Extract<keyof ClientEvents, string>; export interface ClientDataEvent {
name: ClientNameEvents;
export interface ClientDataEvent { once: boolean;
name: ClientNameEvents; }
once: boolean;
} export type CallbackEventHandler = {
[K in keyof ClientEvents]: (...data: [Awaited<ClientEvents[K]>, UsingClient, number]) => unknown;
export type CallbackEventHandler = { } & {
[K in keyof ClientEvents]: (...data: [Awaited<ClientEvents[K]>, UsingClient, number]) => unknown; [K in keyof CustomEvents]: (...data: [Parameters<CustomEvents[K]>, UsingClient, number]) => unknown;
}; };
export type EventContext<T extends { data: { name: ClientNameEvents } }> = Parameters< export type EventContext<T extends { data: { name: ClientNameEvents } }> = Parameters<
CallbackEventHandler[T['data']['name']] CallbackEventHandler[T['data']['name']]
>; >;
export interface ClientEvent { export interface ClientEvent {
data: ClientDataEvent; data: ClientDataEvent;
run(...args: EventContext<any>): any; run(...args: EventContext<any>): any;
/**@internal */ /**@internal */
__filePath?: string; __filePath?: string;
} }

View File

@ -1,14 +1,15 @@
import type { import {
GatewayDispatchPayload, type GatewayDispatchPayload,
GatewayMessageCreateDispatch, type GatewayMessageCreateDispatch,
GatewayMessageDeleteBulkDispatch, type GatewayMessageDeleteBulkDispatch,
GatewayMessageDeleteDispatch, type GatewayMessageDeleteDispatch,
GatewayDispatchEvents,
} from 'discord-api-types/v10'; } from 'discord-api-types/v10';
import type { Client, WorkerClient } from '../client'; import type { Client, WorkerClient } from '../client';
import { BaseHandler, ReplaceRegex, magicImport, type MakeRequired, type SnakeCase } from '../common'; import { BaseHandler, ReplaceRegex, magicImport, type MakeRequired, type SnakeCase } from '../common';
import type { ClientEvents } from '../events/hooks'; import type { ClientEvents } from '../events/hooks';
import * as RawEvents from '../events/hooks'; import * as RawEvents from '../events/hooks';
import type { ClientEvent, ClientNameEvents } from './event'; import type { ClientEvent, CustomEvents, CustomEventsKeys, ClientNameEvents } from './event';
export type EventValue = MakeRequired<ClientEvent, '__filePath'> & { fired?: boolean }; export type EventValue = MakeRequired<ClientEvent, '__filePath'> & { fired?: boolean };
@ -19,12 +20,17 @@ export class EventHandler extends BaseHandler {
super(client.logger); super(client.logger);
} }
onFail = (event: GatewayEvents, err: unknown) => this.logger.warn('<Client>.events.onFail', err, event); onFail = (event: GatewayEvents | CustomEventsKeys, err: unknown) =>
this.logger.warn('<Client>.events.onFail', err, event);
protected filter = (path: string) => path.endsWith('.js') || (!path.endsWith('.d.ts') && path.endsWith('.ts')); protected filter = (path: string) => path.endsWith('.js') || (!path.endsWith('.d.ts') && path.endsWith('.ts'));
values: Partial<Record<GatewayEvents, EventValue>> = {}; values: Partial<Record<GatewayEvents | CustomEventsKeys, EventValue>> = {};
async load(eventsDir: string, instances?: { file: ClientEvent; path: string }[]) { async load(eventsDir: string, instances?: { file: ClientEvent; path: string }[]) {
const discordEvents = Object.values(GatewayDispatchEvents).map(x =>
ReplaceRegex.camel(x.toLowerCase()),
) as ClientNameEvents[];
for (const i of instances ?? (await this.loadFilesK<ClientEvent>(await this.getFiles(eventsDir)))) { for (const i of instances ?? (await this.loadFilesK<ClientEvent>(await this.getFiles(eventsDir)))) {
const instance = this.callback(i.file); const instance = this.callback(i.file);
if (!instance) continue; if (!instance) continue;
@ -36,7 +42,11 @@ export class EventHandler extends BaseHandler {
continue; continue;
} }
instance.__filePath = i.path; instance.__filePath = i.path;
this.values[ReplaceRegex.snake(instance.data.name).toUpperCase() as GatewayEvents] = instance as EventValue; this.values[
discordEvents.includes(instance.data.name)
? (ReplaceRegex.snake(instance.data.name).toUpperCase() as GatewayEvents)
: (instance.data.name as CustomEventsKeys)
] = instance as EventValue;
} }
} }
@ -101,27 +111,43 @@ export class EventHandler extends BaseHandler {
t: name, t: name,
d: packet, d: packet,
} as GatewayDispatchPayload); } as GatewayDispatchPayload);
await Event.run(...[hook, client, shardId]); await Event.run(hook, client, shardId);
} catch (e) { } catch (e) {
await this.onFail(name, e); await this.onFail(name, e);
} }
} }
async reload(name: ClientNameEvents) { async runCustom<T extends CustomEventsKeys>(name: T, ...args: Parameters<CustomEvents[T]>) {
const eventName = ReplaceRegex.snake(name).toUpperCase() as GatewayEvents; const Event = this.values[name];
const event = this.values[eventName]; if (!Event) {
return this.client.collectors.run(name, args as never);
}
try {
if (Event.data.once && Event.fired) {
return this.client.collectors.run(name, args as never);
}
Event.fired = true;
this.logger.debug(`executed a custom event [${name}]`, Event.data.once ? 'once' : '');
await Promise.all([Event.run(args, this.client), this.client.collectors.run(name, args as never)]);
} catch (e) {
await this.onFail(name, e);
}
}
async reload(name: GatewayEvents | CustomEventsKeys) {
const event = this.values[name];
if (!event?.__filePath) return null; if (!event?.__filePath) return null;
delete require.cache[event.__filePath]; delete require.cache[event.__filePath];
const imported = await magicImport(event.__filePath).then(x => x.default ?? x); const imported = await magicImport(event.__filePath).then(x => x.default ?? x);
imported.__filePath = event.__filePath; imported.__filePath = event.__filePath;
this.values[eventName] = imported; this.values[name] = imported;
return imported; return imported;
} }
async reloadAll(stopIfFail = true) { async reloadAll(stopIfFail = true) {
for (const i in this.values) { for (const i in this.values) {
try { try {
await this.reload(ReplaceRegex.camel(i) as ClientNameEvents); await this.reload(i as GatewayEvents | CustomEventsKeys);
} catch (e) { } catch (e) {
if (stopIfFail) { if (stopIfFail) {
throw e; throw e;

View File

@ -1,112 +1,112 @@
import { GatewayIntentBits } from 'discord-api-types/gateway/v10'; import { GatewayIntentBits } from 'discord-api-types/gateway/v10';
import { import {
BaseClient, BaseClient,
type BaseClientOptions, type BaseClientOptions,
type InternalRuntimeConfig, type InternalRuntimeConfig,
type InternalRuntimeConfigHTTP, type InternalRuntimeConfigHTTP,
type RuntimeConfig, type RuntimeConfig,
type RuntimeConfigHTTP, type RuntimeConfigHTTP,
} from './client/base'; } from './client/base';
import type { ClientNameEvents, EventContext } from './events'; import type { CustomEventsKeys, ClientNameEvents, EventContext } from './events';
import { isCloudfareWorker } from './common'; import { isCloudfareWorker } from './common';
export { Logger, PermissionStrings, Watcher } from './common'; export { Logger, PermissionStrings, Watcher } from './common';
// //
export { Collection, LimitedCollection } from './collection'; export { Collection, LimitedCollection } from './collection';
// //
export * from './api'; export * from './api';
export * from './builders'; export * from './builders';
export * from './cache'; export * from './cache';
export * from './commands'; export * from './commands';
export * from './components'; export * from './components';
export * from './events'; export * from './events';
export * from './langs'; export * from './langs';
// //
export { ShardManager, WorkerManager } from './websocket/discord'; export { ShardManager, WorkerManager } from './websocket/discord';
// //
export * from './structures'; export * from './structures';
// //
export * from './client'; export * from './client';
// //
export function throwError(msg: string): never { export function throwError(msg: string): never {
throw new Error(msg); throw new Error(msg);
} }
/** /**
* Creates an event with the specified data and run function. * Creates an event with the specified data and run function.
* *
* @param data - The event data. * @param data - The event data.
* @returns The created event. * @returns The created event.
* *
* @example * @example
* const myEvent = createEvent({ * const myEvent = createEvent({
* data: { name: 'ready', once: true }, * data: { name: 'ready', once: true },
* run: (user, client, shard) => { * run: (user, client, shard) => {
* client.logger.info(`Start ${user.username} on shard #${shard}`); * client.logger.info(`Start ${user.username} on shard #${shard}`);
* } * }
* }); * });
*/ */
export function createEvent<E extends ClientNameEvents>(data: { export function createEvent<E extends ClientNameEvents | CustomEventsKeys>(data: {
data: { name: E; once?: boolean }; data: { name: E; once?: boolean };
run: (...args: EventContext<{ data: { name: E } }>) => any; run: (...args: EventContext<{ data: { name: E } }>) => any;
}) { }) {
data.data.once ??= false; data.data.once ??= false;
return data; return data;
} }
export const config = { export const config = {
/** /**
* Configurations for the bot. * Configurations for the bot.
* *
* @param data - The runtime configuration data for gateway connections. * @param data - The runtime configuration data for gateway connections.
* @returns The internal runtime configuration. * @returns The internal runtime configuration.
*/ */
bot(data: RuntimeConfig) { bot(data: RuntimeConfig) {
return { return {
...data, ...data,
intents: intents:
'intents' in data 'intents' in data
? typeof data.intents === 'number' ? typeof data.intents === 'number'
? data.intents ? data.intents
: data.intents?.reduce<number>( : data.intents?.reduce<number>(
(pr, acc) => pr | (typeof acc === 'number' ? acc : GatewayIntentBits[acc]), (pr, acc) => pr | (typeof acc === 'number' ? acc : GatewayIntentBits[acc]),
0, 0,
) ?? 0 ) ?? 0
: 0, : 0,
} as InternalRuntimeConfig; } as InternalRuntimeConfig;
}, },
/** /**
* Configurations for the HTTP server. * Configurations for the HTTP server.
* *
* @param data - The runtime configuration data for http server. * @param data - The runtime configuration data for http server.
* @returns The internal runtime configuration for HTTP. * @returns The internal runtime configuration for HTTP.
*/ */
http(data: RuntimeConfigHTTP) { http(data: RuntimeConfigHTTP) {
const obj = { const obj = {
port: 8080, port: 8080,
...data, ...data,
} as InternalRuntimeConfigHTTP; } as InternalRuntimeConfigHTTP;
if (isCloudfareWorker()) BaseClient._seyfertConfig = obj; if (isCloudfareWorker()) BaseClient._seyfertConfig = obj;
return obj; return obj;
}, },
}; };
/** /**
* Extends the context of a command interaction. * Extends the context of a command interaction.
* *
* @param cb - The callback function to extend the context. * @param cb - The callback function to extend the context.
* @returns The extended context. * @returns The extended context.
* *
* @example * @example
* const customContext = extendContext((interaction) => { * const customContext = extendContext((interaction) => {
* return { * return {
* owner: '123456789012345678', * owner: '123456789012345678',
* // Add your custom properties here * // Add your custom properties here
* }; * };
* }); * });
*/ */
export function extendContext<T extends {}>( export function extendContext<T extends {}>(
cb: (interaction: Parameters<NonNullable<BaseClientOptions['context']>>[0]) => T, cb: (interaction: Parameters<NonNullable<BaseClientOptions['context']>>[0]) => T,
) { ) {
return cb; return cb;
} }