diff --git a/src/client/collectors.ts b/src/client/collectors.ts index db4f2c2..09bb3da 100644 --- a/src/client/collectors.ts +++ b/src/client/collectors.ts @@ -1,22 +1,25 @@ import { randomUUID } from 'node:crypto'; -import type { Awaitable, CamelCase, SnakeCase } from '../common'; -import type { ClientNameEvents, GatewayEvents } from '../events'; -import type { ClientEvents } from '../events/hooks'; +import type { Awaitable, CamelCase } from '../common'; +import type { CallbackEventHandler, CustomEventsKeys, GatewayEvents } from '../events'; import { error } from 'node:console'; -type SnakeCaseClientNameEvents = Uppercase>; +export type AllClientEvents = CustomEventsKeys | GatewayEvents; +export type ParseClientEventName = T extends CustomEventsKeys ? T : CamelCase; -type RunData = { +type RunData = { options: { event: T; idle?: number; timeout?: number; onStop?: (reason: string) => unknown; onStopError?: (reason: string, error: unknown) => unknown; - filter: (arg: Awaited>]>) => Awaitable; - run: (arg: Awaited>]>, stop: (reason?: string) => void) => unknown; + filter: (arg: Awaited]>[0]>) => Awaitable; + run: ( + arg: Awaited]>[0]>, + stop: (reason?: string) => void, + ) => unknown; onRunError?: ( - arg: Awaited>]>, + arg: Awaited]>[0]>, error: unknown, stop: (reason?: string) => void, ) => unknown; @@ -27,9 +30,9 @@ type RunData = { }; export class Collectors { - readonly values = new Map[]>(); + readonly values = new Map[]>(); - private generateRandomUUID(name: SnakeCaseClientNameEvents) { + private generateRandomUUID(name: AllClientEvents) { const collectors = this.values.get(name); if (!collectors) return '*'; @@ -42,7 +45,7 @@ export class Collectors { return nonce; } - create(options: RunData['options']) { + create(options: RunData['options']) { const nonce = this.generateRandomUUID(options.event); if (!this.values.has(options.event)) { @@ -71,7 +74,7 @@ export class Collectors { 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); if (!collectors?.length) { @@ -93,20 +96,23 @@ export class Collectors { } /**@internal */ - async run(name: T, data: Awaited>]>) { + async run( + name: T, + data: Awaited]>[0]>, + ) { const collectors = this.values.get(name); if (!collectors) return; for (const i of collectors) { - if (await i.options.filter(data)) { + if (await i.options.filter(data as never)) { i.idle?.refresh(); const stop = (reason = 'unknown') => { return this.delete(i.options.event, i.nonce, reason); }; try { - await i.options.run(data, stop); + await i.options.run(data as never, stop); } catch (e) { - await i.options.onRunError?.(data, e, stop); + await i.options.onRunError?.(data as never, e, stop); } break; } diff --git a/src/events/event.ts b/src/events/event.ts index 1f26af3..8ff46c4 100644 --- a/src/events/event.ts +++ b/src/events/event.ts @@ -1,26 +1,26 @@ -import type { UsingClient } from '../commands'; -import type { ClientEvents } from './hooks'; - -export interface DeclareEventsOptions { - name: `${keyof ClientEvents}`; - once?: boolean; -} -export type ClientNameEvents = Extract; - -export interface ClientDataEvent { - name: ClientNameEvents; - once: boolean; -} - -export type CallbackEventHandler = { - [K in keyof ClientEvents]: (...data: [Awaited, UsingClient, number]) => unknown; -}; -export type EventContext = Parameters< - CallbackEventHandler[T['data']['name']] ->; -export interface ClientEvent { - data: ClientDataEvent; - run(...args: EventContext): any; - /**@internal */ - __filePath?: string; -} +import type { UsingClient } from '../commands'; +import type { ClientEvents } from './hooks'; + +export interface CustomEvents {} +export type ClientNameEvents = Extract; +export type CustomEventsKeys = Extract; + +export interface ClientDataEvent { + name: ClientNameEvents; + once: boolean; +} + +export type CallbackEventHandler = { + [K in keyof ClientEvents]: (...data: [Awaited, UsingClient, number]) => unknown; +} & { + [K in keyof CustomEvents]: (...data: [Parameters, UsingClient, number]) => unknown; +}; +export type EventContext = Parameters< + CallbackEventHandler[T['data']['name']] +>; +export interface ClientEvent { + data: ClientDataEvent; + run(...args: EventContext): any; + /**@internal */ + __filePath?: string; +} diff --git a/src/events/handler.ts b/src/events/handler.ts index fe10896..e5c5de4 100644 --- a/src/events/handler.ts +++ b/src/events/handler.ts @@ -1,14 +1,15 @@ -import type { - GatewayDispatchPayload, - GatewayMessageCreateDispatch, - GatewayMessageDeleteBulkDispatch, - GatewayMessageDeleteDispatch, +import { + type GatewayDispatchPayload, + type GatewayMessageCreateDispatch, + type GatewayMessageDeleteBulkDispatch, + type GatewayMessageDeleteDispatch, + GatewayDispatchEvents, } from 'discord-api-types/v10'; import type { Client, WorkerClient } from '../client'; import { BaseHandler, ReplaceRegex, magicImport, type MakeRequired, type SnakeCase } from '../common'; import type { ClientEvents } 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 & { fired?: boolean }; @@ -19,12 +20,17 @@ export class EventHandler extends BaseHandler { super(client.logger); } - onFail = (event: GatewayEvents, err: unknown) => this.logger.warn('.events.onFail', err, event); + onFail = (event: GatewayEvents | CustomEventsKeys, err: unknown) => + this.logger.warn('.events.onFail', err, event); protected filter = (path: string) => path.endsWith('.js') || (!path.endsWith('.d.ts') && path.endsWith('.ts')); - values: Partial> = {}; + values: Partial> = {}; 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(await this.getFiles(eventsDir)))) { const instance = this.callback(i.file); if (!instance) continue; @@ -36,7 +42,11 @@ export class EventHandler extends BaseHandler { continue; } 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, d: packet, } as GatewayDispatchPayload); - await Event.run(...[hook, client, shardId]); + await Event.run(hook, client, shardId); } catch (e) { await this.onFail(name, e); } } - async reload(name: ClientNameEvents) { - const eventName = ReplaceRegex.snake(name).toUpperCase() as GatewayEvents; - const event = this.values[eventName]; + async runCustom(name: T, ...args: Parameters) { + const Event = this.values[name]; + 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; delete require.cache[event.__filePath]; const imported = await magicImport(event.__filePath).then(x => x.default ?? x); imported.__filePath = event.__filePath; - this.values[eventName] = imported; + this.values[name] = imported; return imported; } async reloadAll(stopIfFail = true) { for (const i in this.values) { try { - await this.reload(ReplaceRegex.camel(i) as ClientNameEvents); + await this.reload(i as GatewayEvents | CustomEventsKeys); } catch (e) { if (stopIfFail) { throw e; diff --git a/src/index.ts b/src/index.ts index 15f4a8b..790f7ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,112 +1,112 @@ -import { GatewayIntentBits } from 'discord-api-types/gateway/v10'; -import { - BaseClient, - type BaseClientOptions, - type InternalRuntimeConfig, - type InternalRuntimeConfigHTTP, - type RuntimeConfig, - type RuntimeConfigHTTP, -} from './client/base'; -import type { ClientNameEvents, EventContext } from './events'; -import { isCloudfareWorker } from './common'; -export { Logger, PermissionStrings, Watcher } from './common'; -// -export { Collection, LimitedCollection } from './collection'; -// -export * from './api'; -export * from './builders'; -export * from './cache'; -export * from './commands'; -export * from './components'; -export * from './events'; -export * from './langs'; -// -export { ShardManager, WorkerManager } from './websocket/discord'; -// -export * from './structures'; -// -export * from './client'; -// - -export function throwError(msg: string): never { - throw new Error(msg); -} - -/** - * Creates an event with the specified data and run function. - * - * @param data - The event data. - * @returns The created event. - * - * @example - * const myEvent = createEvent({ - * data: { name: 'ready', once: true }, - * run: (user, client, shard) => { - * client.logger.info(`Start ${user.username} on shard #${shard}`); - * } - * }); - */ -export function createEvent(data: { - data: { name: E; once?: boolean }; - run: (...args: EventContext<{ data: { name: E } }>) => any; -}) { - data.data.once ??= false; - return data; -} - -export const config = { - /** - * Configurations for the bot. - * - * @param data - The runtime configuration data for gateway connections. - * @returns The internal runtime configuration. - */ - bot(data: RuntimeConfig) { - return { - ...data, - intents: - 'intents' in data - ? typeof data.intents === 'number' - ? data.intents - : data.intents?.reduce( - (pr, acc) => pr | (typeof acc === 'number' ? acc : GatewayIntentBits[acc]), - 0, - ) ?? 0 - : 0, - } as InternalRuntimeConfig; - }, - /** - * Configurations for the HTTP server. - * - * @param data - The runtime configuration data for http server. - * @returns The internal runtime configuration for HTTP. - */ - http(data: RuntimeConfigHTTP) { - const obj = { - port: 8080, - ...data, - } as InternalRuntimeConfigHTTP; - if (isCloudfareWorker()) BaseClient._seyfertConfig = obj; - return obj; - }, -}; - -/** - * Extends the context of a command interaction. - * - * @param cb - The callback function to extend the context. - * @returns The extended context. - * - * @example - * const customContext = extendContext((interaction) => { - * return { - * owner: '123456789012345678', - * // Add your custom properties here - * }; - * }); - */ -export function extendContext( - cb: (interaction: Parameters>[0]) => T, -) { - return cb; -} +import { GatewayIntentBits } from 'discord-api-types/gateway/v10'; +import { + BaseClient, + type BaseClientOptions, + type InternalRuntimeConfig, + type InternalRuntimeConfigHTTP, + type RuntimeConfig, + type RuntimeConfigHTTP, +} from './client/base'; +import type { CustomEventsKeys, ClientNameEvents, EventContext } from './events'; +import { isCloudfareWorker } from './common'; +export { Logger, PermissionStrings, Watcher } from './common'; +// +export { Collection, LimitedCollection } from './collection'; +// +export * from './api'; +export * from './builders'; +export * from './cache'; +export * from './commands'; +export * from './components'; +export * from './events'; +export * from './langs'; +// +export { ShardManager, WorkerManager } from './websocket/discord'; +// +export * from './structures'; +// +export * from './client'; +// + +export function throwError(msg: string): never { + throw new Error(msg); +} + +/** + * Creates an event with the specified data and run function. + * + * @param data - The event data. + * @returns The created event. + * + * @example + * const myEvent = createEvent({ + * data: { name: 'ready', once: true }, + * run: (user, client, shard) => { + * client.logger.info(`Start ${user.username} on shard #${shard}`); + * } + * }); + */ +export function createEvent(data: { + data: { name: E; once?: boolean }; + run: (...args: EventContext<{ data: { name: E } }>) => any; +}) { + data.data.once ??= false; + return data; +} + +export const config = { + /** + * Configurations for the bot. + * + * @param data - The runtime configuration data for gateway connections. + * @returns The internal runtime configuration. + */ + bot(data: RuntimeConfig) { + return { + ...data, + intents: + 'intents' in data + ? typeof data.intents === 'number' + ? data.intents + : data.intents?.reduce( + (pr, acc) => pr | (typeof acc === 'number' ? acc : GatewayIntentBits[acc]), + 0, + ) ?? 0 + : 0, + } as InternalRuntimeConfig; + }, + /** + * Configurations for the HTTP server. + * + * @param data - The runtime configuration data for http server. + * @returns The internal runtime configuration for HTTP. + */ + http(data: RuntimeConfigHTTP) { + const obj = { + port: 8080, + ...data, + } as InternalRuntimeConfigHTTP; + if (isCloudfareWorker()) BaseClient._seyfertConfig = obj; + return obj; + }, +}; + +/** + * Extends the context of a command interaction. + * + * @param cb - The callback function to extend the context. + * @returns The extended context. + * + * @example + * const customContext = extendContext((interaction) => { + * return { + * owner: '123456789012345678', + * // Add your custom properties here + * }; + * }); + */ +export function extendContext( + cb: (interaction: Parameters>[0]) => T, +) { + return cb; +}