mirror of
https://github.com/tiramisulabs/seyfert.git
synced 2025-07-01 20:46:08 +00:00
feat: custom events
This commit is contained in:
parent
625e400c20
commit
349628a587
@ -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<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: {
|
||||
event: T;
|
||||
idle?: number;
|
||||
timeout?: number;
|
||||
onStop?: (reason: string) => unknown;
|
||||
onStopError?: (reason: string, error: unknown) => unknown;
|
||||
filter: (arg: Awaited<ClientEvents[CamelCase<Lowercase<T>>]>) => Awaitable<boolean>;
|
||||
run: (arg: Awaited<ClientEvents[CamelCase<Lowercase<T>>]>, stop: (reason?: string) => void) => unknown;
|
||||
filter: (arg: Awaited<Parameters<CallbackEventHandler[ParseClientEventName<T>]>[0]>) => Awaitable<boolean>;
|
||||
run: (
|
||||
arg: Awaited<Parameters<CallbackEventHandler[ParseClientEventName<T>]>[0]>,
|
||||
stop: (reason?: string) => void,
|
||||
) => unknown;
|
||||
onRunError?: (
|
||||
arg: Awaited<ClientEvents[CamelCase<Lowercase<T>>]>,
|
||||
arg: Awaited<Parameters<CallbackEventHandler[ParseClientEventName<T>]>[0]>,
|
||||
error: unknown,
|
||||
stop: (reason?: string) => void,
|
||||
) => unknown;
|
||||
@ -27,9 +30,9 @@ type RunData<T extends SnakeCaseClientNameEvents> = {
|
||||
};
|
||||
|
||||
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);
|
||||
if (!collectors) return '*';
|
||||
|
||||
@ -42,7 +45,7 @@ export class Collectors {
|
||||
return nonce;
|
||||
}
|
||||
|
||||
create<T extends SnakeCaseClientNameEvents>(options: RunData<T>['options']) {
|
||||
create<T extends AllClientEvents>(options: RunData<T>['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<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);
|
||||
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;
|
||||
}
|
||||
|
@ -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<keyof ClientEvents, string>;
|
||||
|
||||
export interface ClientDataEvent {
|
||||
name: ClientNameEvents;
|
||||
once: boolean;
|
||||
}
|
||||
|
||||
export type CallbackEventHandler = {
|
||||
[K in keyof ClientEvents]: (...data: [Awaited<ClientEvents[K]>, UsingClient, number]) => unknown;
|
||||
};
|
||||
export type EventContext<T extends { data: { name: ClientNameEvents } }> = Parameters<
|
||||
CallbackEventHandler[T['data']['name']]
|
||||
>;
|
||||
export interface ClientEvent {
|
||||
data: ClientDataEvent;
|
||||
run(...args: EventContext<any>): any;
|
||||
/**@internal */
|
||||
__filePath?: string;
|
||||
}
|
||||
import type { UsingClient } from '../commands';
|
||||
import type { ClientEvents } from './hooks';
|
||||
|
||||
export interface CustomEvents {}
|
||||
export type ClientNameEvents = Extract<keyof ClientEvents, string>;
|
||||
export type CustomEventsKeys = Extract<keyof CustomEvents, string>;
|
||||
|
||||
export interface ClientDataEvent {
|
||||
name: ClientNameEvents;
|
||||
once: boolean;
|
||||
}
|
||||
|
||||
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<
|
||||
CallbackEventHandler[T['data']['name']]
|
||||
>;
|
||||
export interface ClientEvent {
|
||||
data: ClientDataEvent;
|
||||
run(...args: EventContext<any>): any;
|
||||
/**@internal */
|
||||
__filePath?: string;
|
||||
}
|
||||
|
@ -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<ClientEvent, '__filePath'> & { fired?: boolean };
|
||||
|
||||
@ -19,12 +20,17 @@ export class EventHandler extends BaseHandler {
|
||||
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'));
|
||||
|
||||
values: Partial<Record<GatewayEvents, EventValue>> = {};
|
||||
values: Partial<Record<GatewayEvents | CustomEventsKeys, EventValue>> = {};
|
||||
|
||||
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)))) {
|
||||
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<T extends CustomEventsKeys>(name: T, ...args: Parameters<CustomEvents[T]>) {
|
||||
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;
|
||||
|
224
src/index.ts
224
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<E extends ClientNameEvents>(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<number>(
|
||||
(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<T extends {}>(
|
||||
cb: (interaction: Parameters<NonNullable<BaseClientOptions['context']>>[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<E extends ClientNameEvents | CustomEventsKeys>(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<number>(
|
||||
(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<T extends {}>(
|
||||
cb: (interaction: Parameters<NonNullable<BaseClientOptions['context']>>[0]) => T,
|
||||
) {
|
||||
return cb;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user