import type { GatewayDispatchPayload, GatewayMessageCreateDispatch, GatewayMessageDeleteBulkDispatch, GatewayMessageDeleteDispatch, } from '../types'; import type { Client, WorkerClient } from '../client'; import { BaseHandler, type CamelCase, ReplaceRegex, isCloudfareWorker, magicImport, type MakeRequired, type SnakeCase, } from '../common'; import type { ClientEvents } from '../events/hooks'; import * as RawEvents from '../events/hooks'; import type { ClientEvent, CustomEvents, CustomEventsKeys, ClientNameEvents, EventContext } from './event'; import type { FileLoaded } from '../commands/handler'; import type { UsingClient } from '../commands'; export type EventValue = MakeRequired & { fired?: boolean }; export type GatewayEvents = Uppercase>; export type ResolveEventParams = T extends CustomEventsKeys ? [...Parameters, UsingClient] : T extends GatewayEvents ? EventContext<{ data: { name: CamelCase } }> : T extends ClientNameEvents ? EventContext<{ data: { name: T } }> : never; export type ResolveEventRunParams = T extends CustomEventsKeys ? Parameters : T extends GatewayEvents ? EventContext<{ data: { name: CamelCase } }> : T extends ClientNameEvents ? EventContext<{ data: { name: T } }> : never; export type EventValues = { [K in CustomEventsKeys | GatewayEvents]: Omit & { run(...args: ResolveEventRunParams): any; }; }; export class EventHandler extends BaseHandler { constructor(protected client: Client | WorkerClient) { super(client.logger); } 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 = {}; discordEvents = Object.keys(RawEvents).map(x => ReplaceRegex.camel(x.toLowerCase())) as ClientNameEvents[]; set(events: ClientEvent[]) { for (const event of events) { const instance = this.callback(event); if (!instance) continue; if (typeof instance?.run !== 'function') { this.logger.warn('Missing event run function'); continue; } this.values[ this.discordEvents.includes(instance.data.name) ? (ReplaceRegex.snake(instance.data.name).toUpperCase() as GatewayEvents) : (instance.data.name as CustomEventsKeys) ] = instance as EventValue; } } async load(eventsDir: string) { const paths = await this.loadFilesK<{ file: ClientEvent }>(await this.getFiles(eventsDir)); for (const { events, file } of paths.map(x => ({ events: this.onFile(x.file), file: x }))) { if (!events) continue; for (const i of events) { const instance = this.callback(i); if (!instance) continue; if (typeof instance?.run !== 'function') { this.logger.warn( file.path.split(process.cwd()).slice(1).join(process.cwd()), 'Missing run function, use `export default {...}` syntax', ); continue; } instance.__filePath = file.path; this.values[ this.discordEvents.includes(instance.data.name) ? (ReplaceRegex.snake(instance.data.name).toUpperCase() as GatewayEvents) : (instance.data.name as CustomEventsKeys) ] = instance as EventValue; } } } async execute(name: GatewayEvents, ...args: [GatewayDispatchPayload, Client | WorkerClient, number]) { switch (name) { case 'MESSAGE_CREATE': { const { d: data } = args[0] as GatewayMessageCreateDispatch; if (args[1].components?.values.has(data.interaction_metadata?.id ?? data.id)) { args[1].components.values.get(data.interaction_metadata!.id ?? data.id)!.messageId = data.id; } } break; case 'MESSAGE_DELETE': { const { d: data } = args[0] as GatewayMessageDeleteDispatch; const value = [...(args[1].components?.values ?? [])].find(x => x[1].messageId === data.id); if (value) { args[1].components!.onMessageDelete(value[0]); } } break; case 'MESSAGE_DELETE_BULK': { const { d: data } = args[0] as GatewayMessageDeleteBulkDispatch; const values = [...(args[1].components?.values ?? [])]; data.ids.forEach(id => { const value = values.find(x => x[1].messageId === id); if (value) { args[1].components!.onMessageDelete(value[0]); } }); } break; } await Promise.all([ this.runEvent(args[0].t as never, args[1], args[0].d, args[2]), this.client.collectors.run(args[0].t as never, args[0].d as never, this.client), ]); } async runEvent(name: GatewayEvents, client: Client | WorkerClient, packet: any, shardId: number, runCache = true) { const Event = this.values[name]; if (!Event) { return runCache ? this.client.cache.onPacket({ t: name, d: packet, } as GatewayDispatchPayload) : undefined; } try { if (Event.data.once && Event.fired) { return runCache ? this.client.cache.onPacket({ t: name, d: packet, } as GatewayDispatchPayload) : undefined; } Event.fired = true; const hook = await RawEvents[name]?.(client, packet as never); if (runCache) await this.client.cache.onPacket({ t: name, d: packet, } as GatewayDispatchPayload); await Event.run(hook, client, shardId); } catch (e) { await this.onFail(name, e); } } async runCustom(name: T, ...args: ResolveEventRunParams) { const Event = this.values[name]; if (!Event) { // @ts-expect-error working with non-existent types is hard return this.client.collectors.run(name, args, this.client); } try { if (Event.data.once && Event.fired) { // @ts-expect-error working with non-existent types is hard return this.client.collectors.run(name, args, this.client); } Event.fired = true; this.logger.debug(`executed a custom event [${name}]`, Event.data.once ? 'once' : ''); await Promise.all([ // @ts-expect-error working with non-existent types is hard Event.run(...args, this.client), // @ts-expect-error working with non-existent types is hard this.client.collectors.run(name, args, this.client), ]); } catch (e) { await this.onFail(name, e); } } async reload(name: GatewayEvents | CustomEventsKeys) { if (isCloudfareWorker()) { throw new Error('Reload in cloudfare worker is not supported'); } 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[name] = imported; return imported; } async reloadAll(stopIfFail = true) { for (const i in this.values) { try { await this.reload(i as GatewayEvents | CustomEventsKeys); } catch (e) { if (stopIfFail) { throw e; } } } } onFile(file: FileLoaded): ClientEvent[] | undefined { return file.default ? [file.default] : undefined; } callback = (file: ClientEvent): ClientEvent | false => file; }