import type { Client, WorkerClient } from '../client'; import type { UsingClient } from '../commands'; import type { FileLoaded } from '../commands/handler'; import { BaseHandler, type CamelCase, isCloudfareWorker, type MakeRequired, magicImport, ReplaceRegex, type SnakeCase, } from '../common'; import type { ClientEvents } from '../events/hooks'; import * as RawEvents from '../events/hooks'; import { type APIThreadChannel, ChannelType, type GatewayDispatchPayload } from '../types'; import type { ClientEvent, ClientNameEvents, CustomEvents, CustomEventsKeys, EventContext } from './event'; 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); 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(raw: GatewayDispatchPayload, client: Client | WorkerClient, shardId: number) { switch (raw.t) { case 'MESSAGE_DELETE': { if (!client.components.values.size) break; const value = client.components.values.get(raw.d.id); if (value) { client.components.deleteValue(value.messageId, 'messageDelete'); } } break; case 'MESSAGE_DELETE_BULK': { if (!client.components.values.size) break; for (const id of raw.d.ids) { const value = client.components.values.get(id); if (value) { client.components.deleteValue(value.messageId, 'messageDelete'); } } } break; case 'GUILD_DELETE': { if (!client.components.values.size) break; // ignore unavailable guilds? if (raw.d.unavailable) break; for (const [messageId, value] of client.components.values) { if (value.guildId === raw.d.id) client.components.deleteValue(messageId, 'guildDelete'); } } break; case 'CHANNEL_DELETE': { if (!client.components.values.size) break; if (raw.d.type === ChannelType.DM || raw.d.type === ChannelType.GroupDM) { for (const value of client.components.values) { if (raw.d.id === value[1].channelId) client.components.deleteValue(value[0], 'channelDelete'); } } else { if (!raw.d.guild_id) break; // this is why we dont recommend to use collectors, use ComponentCommand instead const channels = await client.cache.channels?.valuesRaw(raw.d.guild_id); const threads = channels ?.filter( x => [ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.AnnouncementThread].includes( x.type, ) && (x as APIThreadChannel).parent_id === raw.d.id, ) .map(x => x.id); for (const value of client.components.values) { const channelId = value[1].channelId; if (raw.d.id === channelId || threads?.includes(channelId)) { client.components.deleteValue(value[0], 'channelDelete'); } } } } break; case 'THREAD_DELETE': { if (!client.components.values.size) break; for (const value of client.components.values) { if (value[1].channelId === raw.d.id) { client.components.deleteValue(value[0], 'channelDelete'); } } } break; } await Promise.all([ this.runEvent(raw.t as never, client, raw.d, shardId), this.client.collectors.run(raw.t as never, raw.d as never, this.client), ]); } async runEvent( name: GatewayEvents, client: Client | WorkerClient, packet: unknown, shardId: number, runCache = true, ) { const Event = this.values[name]; try { if (!Event || (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 as any)(hook, client, shardId); } catch (e) { await this.onFail(name, e); } } async runCustom(name: T, ...args: ResolveEventRunParams) { const Event = this.values[name]; try { if (!Event || (Event.data.once && Event.fired)) { // @ts-expect-error 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([ (Event.run as any)(...args, this.client), // @ts-expect-error 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 ? (Array.isArray(file.default) ? file.default : [file.default]) : undefined; } callback = (file: ClientEvent): ClientEvent | false => file; }