mirror of
https://github.com/sern-handler/handler
synced 2026-06-28 02:32:15 +00:00
refactor: move things to core, imports not fixed yet
This commit is contained in:
50
src/core/contracts/errorHandling.ts
Normal file
50
src/core/contracts/errorHandling.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { Logging } from './logging';
|
||||
import util from 'util';
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export interface ErrorHandling {
|
||||
/**
|
||||
* Number of times the process should throw an error until crashing and exiting
|
||||
*/
|
||||
keepAlive: number;
|
||||
|
||||
/**
|
||||
* Utility function to crash
|
||||
* @param error
|
||||
*/
|
||||
crash(error: Error): never;
|
||||
|
||||
/**
|
||||
* A function that is called on every crash. Updates keepAlive
|
||||
* @param error
|
||||
*/
|
||||
updateAlive(error: Error): void;
|
||||
}
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export class DefaultErrorHandling implements ErrorHandling {
|
||||
keepAlive = 5;
|
||||
crash(error: Error): never {
|
||||
throw error;
|
||||
}
|
||||
updateAlive(_: Error) {
|
||||
this.keepAlive--;
|
||||
}
|
||||
}
|
||||
|
||||
export function handleError<C>(crashHandler: ErrorHandling, logging?: Logging) {
|
||||
return (pload: unknown, caught: Observable<C>) => {
|
||||
// This is done to fit the ErrorHandling contract
|
||||
const err = pload instanceof Error ? pload : Error(util.format(pload));
|
||||
if (crashHandler.keepAlive == 0) {
|
||||
crashHandler.crash(err);
|
||||
}
|
||||
//formatted payload
|
||||
logging?.error({ message: util.format(pload) });
|
||||
crashHandler.updateAlive(err);
|
||||
return caught;
|
||||
};
|
||||
}
|
||||
3
src/core/contracts/index.ts
Normal file
3
src/core/contracts/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { ErrorHandling, DefaultErrorHandling } from './errorHandling';
|
||||
export { Logging, DefaultLogging } from './logging';
|
||||
export { ModuleManager, DefaultModuleManager } from './moduleManager';
|
||||
31
src/core/contracts/logging.ts
Normal file
31
src/core/contracts/logging.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { LogPayload } from '../../types/handler';
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export interface Logging<T = unknown> {
|
||||
error(payload: LogPayload<T>): void;
|
||||
warning(payload: LogPayload<T>): void;
|
||||
info(payload: LogPayload<T>): void;
|
||||
debug(payload: LogPayload<T>): void;
|
||||
}
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export class DefaultLogging implements Logging {
|
||||
private date = () => new Date();
|
||||
debug(payload: LogPayload): void {
|
||||
console.debug(`DEBUG: ${this.date().toISOString()} -> ${payload.message}`);
|
||||
}
|
||||
|
||||
error(payload: LogPayload): void {
|
||||
console.error(`ERROR: ${this.date().toISOString()} -> ${payload.message}`);
|
||||
}
|
||||
|
||||
info(payload: LogPayload): void {
|
||||
console.info(`INFO: ${this.date().toISOString()} -> ${payload.message}`);
|
||||
}
|
||||
|
||||
warning(payload: LogPayload): void {
|
||||
console.warn(`WARN: ${this.date().toISOString()} -> ${payload.message}`);
|
||||
}
|
||||
}
|
||||
27
src/core/contracts/moduleManager.ts
Normal file
27
src/core/contracts/moduleManager.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { CommandModuleDefs } from '../../types/module';
|
||||
import type { CommandType, ModuleStore } from '../structures';
|
||||
import type { Processed } from '../../types/handler';
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export interface ModuleManager {
|
||||
get<T extends CommandType>(
|
||||
strat: (ms: ModuleStore) => Processed<CommandModuleDefs[T]> | undefined,
|
||||
): Processed<CommandModuleDefs[T]> | undefined;
|
||||
set(strat: (ms: ModuleStore) => void): void;
|
||||
}
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export class DefaultModuleManager implements ModuleManager {
|
||||
constructor(private moduleStore: ModuleStore) {}
|
||||
get<T extends CommandType>(
|
||||
strat: (ms: ModuleStore) => Processed<CommandModuleDefs[T]> | undefined,
|
||||
) {
|
||||
return strat(this.moduleStore);
|
||||
}
|
||||
|
||||
set(strat: (ms: ModuleStore) => void): void {
|
||||
strat(this.moduleStore);
|
||||
}
|
||||
}
|
||||
2
src/core/dependencies/index.ts
Normal file
2
src/core/dependencies/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { single, transient, many } from './lifetimeFunctions';
|
||||
export { useContainerRaw } from './provider';
|
||||
63
src/core/dependencies/lifetimeFunctions.ts
Normal file
63
src/core/dependencies/lifetimeFunctions.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { _const } from '../utilities/functions';
|
||||
|
||||
type NotFunction =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| undefined
|
||||
| bigint
|
||||
| readonly any[]
|
||||
| { apply?: never; [k: string]: any }
|
||||
| { call?: never; [k: string]: any };
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @param cb
|
||||
*/
|
||||
export function single<T extends NotFunction>(cb: T): () => T;
|
||||
/**
|
||||
* New signature
|
||||
* @since 2.0.0
|
||||
* @param cb
|
||||
*/
|
||||
export function single<T extends () => unknown>(cb: T): T;
|
||||
/**
|
||||
* @__PURE__
|
||||
* @since 2.0.0.
|
||||
* Please note that on intellij, the deprecation is for all signatures, which is unintended behavior (and
|
||||
* very annoying).
|
||||
* For future versions, ensure that single is being passed as a **callback!!**
|
||||
* @param cb
|
||||
*/
|
||||
export function single<T>(cb: T) {
|
||||
if (typeof cb === 'function') return cb;
|
||||
return () => cb;
|
||||
}
|
||||
/**
|
||||
* @deprecated
|
||||
* @param cb
|
||||
* Deprecated signature
|
||||
*/
|
||||
export function transient<T extends NotFunction>(cb: T): () => () => T;
|
||||
export function transient<T extends () => () => unknown>(cb: T): T;
|
||||
/**
|
||||
* @__PURE__
|
||||
* @since 2.0.0
|
||||
* Following iti's singleton and transient implementation,
|
||||
* use transient if you want a new dependency every time your container getter is called
|
||||
* @param cb
|
||||
*/
|
||||
export function transient<T>(cb: (() => () => T) | T) {
|
||||
if (typeof cb !== 'function') return () => () => cb;
|
||||
return cb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @__PURE__
|
||||
* @deprecated
|
||||
* @param value
|
||||
* Please use the transient function instead
|
||||
*/
|
||||
// prettier-ignore
|
||||
export const many = <T>(value: T) => () => _const(value);
|
||||
86
src/core/dependencies/provider.ts
Normal file
86
src/core/dependencies/provider.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { Container } from 'iti';
|
||||
import type { Dependencies, DependencyConfiguration, MapDeps } from '../../types/handler';
|
||||
import SernEmitter from '../sernEmitter';
|
||||
import { DefaultErrorHandling, DefaultLogging, DefaultModuleManager } from '../contracts';
|
||||
import { Result } from 'ts-results-es';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { createContainer } from 'iti';
|
||||
import { type Wrapper, ModuleStore, SernError } from '../structures';
|
||||
|
||||
export const containerSubject = new BehaviorSubject(defaultContainer());
|
||||
|
||||
/**
|
||||
* Given the user's conf, check for any excluded dependency keys.
|
||||
* Then, call conf.build to get the rest of the users' dependencies.
|
||||
* Finally, update the containerSubject with the new container state
|
||||
* @param conf
|
||||
*/
|
||||
export function composeRoot<T extends Dependencies>(conf: DependencyConfiguration<T>) {
|
||||
//Get the current container. This should have no client or possible logger yet.
|
||||
const currentContainer = containerSubject.getValue();
|
||||
const excludeLogger = conf.exclude?.has('@sern/logger');
|
||||
if (!excludeLogger) {
|
||||
currentContainer.add({
|
||||
'@sern/logger': () => new DefaultLogging(),
|
||||
});
|
||||
}
|
||||
//Build the container based on the callback provided by the user
|
||||
const container = conf.build(currentContainer);
|
||||
//Check if the built container contains @sern/client or throw
|
||||
// a runtime exception
|
||||
Result.wrap(() => container.get('@sern/client')).expect(SernError.MissingRequired);
|
||||
|
||||
if (!excludeLogger) {
|
||||
container.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
|
||||
}
|
||||
//I'm sorry little one
|
||||
containerSubject.next(container as any);
|
||||
}
|
||||
|
||||
export function useContainer<T extends Dependencies>() {
|
||||
const container = containerSubject.getValue() as Container<T, {}>;
|
||||
return <V extends (keyof T)[]>(...keys: [...V]) =>
|
||||
keys.map(key => Result.wrap(() => container.get(key)).unwrapOr(undefined)) as MapDeps<T, V>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying data structure holding all dependencies.
|
||||
* Please be careful as this only gets the client's current state.
|
||||
* Exposes some methods from iti
|
||||
*/
|
||||
export function useContainerRaw<T extends Dependencies>() {
|
||||
return containerSubject.getValue() as Container<T, {}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides all the defaults for sern to function properly.
|
||||
* The only user provided dependency needs to be @sern/client
|
||||
*/
|
||||
function defaultContainer() {
|
||||
return createContainer()
|
||||
.add({ '@sern/errors': () => new DefaultErrorHandling() })
|
||||
.add({ '@sern/store': () => new ModuleStore() })
|
||||
.add(ctx => {
|
||||
return {
|
||||
'@sern/modules': () => new DefaultModuleManager(ctx['@sern/store']),
|
||||
};
|
||||
})
|
||||
.add({ '@sern/emitter': () => new SernEmitter() }) as Container<
|
||||
Omit<Dependencies, '@sern/client' | '@sern/logger'>,
|
||||
{}
|
||||
>;
|
||||
}
|
||||
|
||||
export function makeFetcher(wrapper: Wrapper) {
|
||||
const requiredDependencyKeys = [
|
||||
'@sern/emitter',
|
||||
'@sern/client',
|
||||
'@sern/errors',
|
||||
'@sern/logger',
|
||||
] as ['@sern/emitter', '@sern/client', '@sern/errors', '@sern/logger'];
|
||||
return <Keys extends (keyof Dependencies)[]>(otherKeys: [...Keys]) =>
|
||||
wrapper.containerConfig.get(...requiredDependencyKeys, ...otherKeys) as MapDeps<
|
||||
Dependencies,
|
||||
[...typeof requiredDependencyKeys, ...Keys]
|
||||
>;
|
||||
}
|
||||
3
src/core/index.ts
Normal file
3
src/core/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import SernEmitter from './sernEmitter'
|
||||
|
||||
export { SernEmitter };
|
||||
64
src/core/module-loading/readFile.ts
Normal file
64
src/core/module-loading/readFile.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { type Observable, from, mergeMap } from 'rxjs';
|
||||
import { SernError } from '../structures/errors';
|
||||
import { type Result, Err, Ok } from 'ts-results-es';
|
||||
import { ImportPayload } from '../../types/handler';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
// Courtesy @Townsy45
|
||||
function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
|
||||
try {
|
||||
const files = readdirSync(dir);
|
||||
for (const file of files) {
|
||||
if (statSync(dir + '/' + file).isDirectory()) readPath(dir + '/' + file, arrayOfFiles);
|
||||
else arrayOfFiles.push(join(dir, '/', file));
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
return arrayOfFiles;
|
||||
}
|
||||
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
|
||||
// export const isLazy = (n: string) => n.indexOf(".lazy.", n.length-9) !== -1;
|
||||
|
||||
export async function defaultModuleLoader<T>(
|
||||
absPath: string,
|
||||
): Promise<Result<ImportPayload<T>, SernError>> {
|
||||
// prettier-ignore
|
||||
let module: T | undefined
|
||||
/// #if MODE === 'esm'
|
||||
= (await import(pathToFileURL(absPath).toString())).default
|
||||
/// #elif MODE === 'cjs'
|
||||
= require(absPath).default; // eslint-disable-line
|
||||
/// #endif
|
||||
if (module === undefined) {
|
||||
return Err(SernError.UndefinedModule);
|
||||
}
|
||||
try {
|
||||
module = new (module as unknown as new () => T)();
|
||||
} catch {}
|
||||
return Ok({ module, absPath });
|
||||
}
|
||||
|
||||
/**
|
||||
* a directory string is converted into a stream of modules.
|
||||
* starts the stream of modules that sern needs to process on init
|
||||
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
|
||||
* @param commandDir
|
||||
*/
|
||||
export function buildModuleStream<T>(
|
||||
commandDir: string,
|
||||
): Observable<Result<ImportPayload<T>, SernError>> {
|
||||
const commands = getCommands(commandDir);
|
||||
return from(commands).pipe(mergeMap(defaultModuleLoader<T>));
|
||||
}
|
||||
|
||||
export function fullPathFrom(dir: string) {
|
||||
return join(process.cwd(), dir);
|
||||
}
|
||||
|
||||
export function getCommands(dir: string): string[] {
|
||||
return readPath(fullPathFrom(dir));
|
||||
}
|
||||
117
src/core/plugins/args.ts
Normal file
117
src/core/plugins/args.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { CommandType } from '../structures/enums';
|
||||
import type { PluginType } from '../structures/enums';
|
||||
import type { ClientEvents } from 'discord.js';
|
||||
import type {
|
||||
BothCommand,
|
||||
ButtonCommand,
|
||||
ChannelSelectCommand,
|
||||
ContextMenuUser,
|
||||
DiscordEventCommand,
|
||||
ExternalEventCommand,
|
||||
MentionableSelectCommand,
|
||||
ModalSubmitCommand,
|
||||
RoleSelectCommand,
|
||||
SernEventCommand,
|
||||
SlashCommand,
|
||||
StringSelectCommand,
|
||||
TextCommand,
|
||||
UserSelectCommand,
|
||||
ContextMenuMsg,
|
||||
Module,
|
||||
} from '../../types/module';
|
||||
import type { Args, Payload, Processed, SlashOptions } from '../../types/handler';
|
||||
import type Context from '../structures/context';
|
||||
import type { MessageContextMenuCommandInteraction } from 'discord.js';
|
||||
import type {
|
||||
ButtonInteraction,
|
||||
RoleSelectMenuInteraction,
|
||||
StringSelectMenuInteraction,
|
||||
UserContextMenuCommandInteraction,
|
||||
} from 'discord.js';
|
||||
import type {
|
||||
ChannelSelectMenuInteraction,
|
||||
MentionableSelectMenuInteraction,
|
||||
ModalSubmitInteraction,
|
||||
UserSelectMenuInteraction,
|
||||
} from 'discord.js';
|
||||
import { EventType } from '../structures/enums';
|
||||
|
||||
type CommandArgsMatrix = {
|
||||
[CommandType.Text]: {
|
||||
[PluginType.Control]: [Context, ['text', string[]]];
|
||||
[PluginType.Init]: [InitArgs<Processed<TextCommand>>];
|
||||
};
|
||||
[CommandType.Slash]: {
|
||||
[PluginType.Control]: [Context, ['slash', /* library coupled */ SlashOptions]];
|
||||
[PluginType.Init]: [InitArgs<Processed<SlashCommand>>];
|
||||
};
|
||||
[CommandType.Both]: {
|
||||
[PluginType.Control]: [Context, Args];
|
||||
[PluginType.Init]: [InitArgs<Processed<BothCommand>>];
|
||||
};
|
||||
[CommandType.CtxMsg]: {
|
||||
[PluginType.Control]: [/* library coupled */ MessageContextMenuCommandInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ContextMenuMsg>>];
|
||||
};
|
||||
[CommandType.CtxUser]: {
|
||||
[PluginType.Control]: [/* library coupled */ UserContextMenuCommandInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ContextMenuUser>>];
|
||||
};
|
||||
[CommandType.Button]: {
|
||||
[PluginType.Control]: [/* library coupled */ ButtonInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ButtonCommand>>];
|
||||
};
|
||||
[CommandType.StringSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ StringSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<StringSelectCommand>>];
|
||||
};
|
||||
[CommandType.RoleSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ RoleSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<RoleSelectCommand>>];
|
||||
};
|
||||
[CommandType.ChannelSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ ChannelSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ChannelSelectCommand>>];
|
||||
};
|
||||
[CommandType.MentionableSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ MentionableSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<MentionableSelectCommand>>];
|
||||
};
|
||||
[CommandType.UserSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ UserSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<UserSelectCommand>>];
|
||||
};
|
||||
[CommandType.Modal]: {
|
||||
[PluginType.Control]: [/* library coupled */ ModalSubmitInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ModalSubmitCommand>>];
|
||||
};
|
||||
};
|
||||
|
||||
type EventArgsMatrix = {
|
||||
[EventType.Discord]: {
|
||||
[PluginType.Control]: /* library coupled */ ClientEvents[keyof ClientEvents];
|
||||
[PluginType.Init]: [InitArgs<Processed<DiscordEventCommand>>];
|
||||
};
|
||||
[EventType.Sern]: {
|
||||
[PluginType.Control]: [Payload];
|
||||
[PluginType.Init]: [InitArgs<Processed<SernEventCommand>>];
|
||||
};
|
||||
[EventType.External]: {
|
||||
[PluginType.Control]: unknown[];
|
||||
[PluginType.Init]: [InitArgs<Processed<ExternalEventCommand>>];
|
||||
};
|
||||
};
|
||||
|
||||
export interface InitArgs<T extends Processed<Module>> {
|
||||
module: T;
|
||||
absPath: string;
|
||||
}
|
||||
|
||||
export type CommandArgs<
|
||||
I extends CommandType = CommandType,
|
||||
J extends PluginType = PluginType,
|
||||
> = CommandArgsMatrix[I][J];
|
||||
export type EventArgs<
|
||||
I extends EventType = EventType,
|
||||
J extends PluginType = PluginType,
|
||||
> = EventArgsMatrix[I][J];
|
||||
64
src/core/plugins/createPlugin.ts
Normal file
64
src/core/plugins/createPlugin.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { CommandType, EventType, PluginType } from '../structures';
|
||||
import type { Plugin, PluginResult } from '../../types/plugin';
|
||||
import type { CommandArgs, EventArgs } from './args';
|
||||
import type { ClientEvents } from 'discord.js';
|
||||
export const guayin = Symbol('twice<3');
|
||||
export function makePlugin<V extends unknown[]>(
|
||||
type: PluginType,
|
||||
execute: (...args: any[]) => any,
|
||||
): Plugin<V> {
|
||||
return {
|
||||
type,
|
||||
execute,
|
||||
[guayin]: undefined,
|
||||
} as Plugin<V>;
|
||||
}
|
||||
/**
|
||||
* @since 2.5.0
|
||||
*
|
||||
*/
|
||||
export function EventInitPlugin<I extends EventType>(
|
||||
execute: (...args: EventArgs<I, PluginType.Init>) => PluginResult,
|
||||
) {
|
||||
return makePlugin(PluginType.Init, execute);
|
||||
}
|
||||
/**
|
||||
* @since 2.5.0
|
||||
*
|
||||
*/
|
||||
export function CommandInitPlugin<I extends CommandType>(
|
||||
execute: (...args: CommandArgs<I, PluginType.Init>) => PluginResult,
|
||||
) {
|
||||
return makePlugin(PluginType.Init, execute);
|
||||
}
|
||||
/**
|
||||
* @since 2.5.0
|
||||
*
|
||||
*/
|
||||
export function CommandControlPlugin<I extends CommandType>(
|
||||
execute: (...args: CommandArgs<I, PluginType.Control>) => PluginResult,
|
||||
) {
|
||||
return makePlugin(PluginType.Control, execute);
|
||||
}
|
||||
/**
|
||||
* @since 2.5.0
|
||||
*
|
||||
*/
|
||||
export function EventControlPlugin<I extends EventType>(
|
||||
execute: (...args: EventArgs<I, PluginType.Control>) => PluginResult,
|
||||
) {
|
||||
return makePlugin(PluginType.Control, execute);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.5.0
|
||||
* @Experimental
|
||||
* A specialized function for creating control plugins with discord.js ClientEvents.
|
||||
* Will probably be moved one day!
|
||||
*/
|
||||
export function DiscordEventControlPlugin<T extends keyof ClientEvents>(
|
||||
name: T,
|
||||
execute: (...args: ClientEvents[T]) => PluginResult,
|
||||
) {
|
||||
return makePlugin(PluginType.Control, execute);
|
||||
}
|
||||
2
src/core/plugins/index.ts
Normal file
2
src/core/plugins/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { EventArgs, InitArgs, CommandArgs } from './args';
|
||||
export * from './createPlugin';
|
||||
88
src/core/sernEmitter.ts
Normal file
88
src/core/sernEmitter.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import type { Payload, SernEventsMapping } from '../types/handler';
|
||||
import { PayloadType } from '../handler/structures';
|
||||
import type { Module } from '../types/module';
|
||||
|
||||
/**
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class SernEmitter extends EventEmitter {
|
||||
/**
|
||||
* Listening to sern events with on. This event stays on until a crash or a normal exit
|
||||
* @param eventName
|
||||
* @param listener what to do with the data
|
||||
*/
|
||||
public override on<T extends keyof SernEventsMapping>(
|
||||
eventName: T,
|
||||
listener: (...args: SernEventsMapping[T][]) => void,
|
||||
): this {
|
||||
return super.on(eventName, listener);
|
||||
}
|
||||
/**
|
||||
* Listening to sern events with on. This event stays on until a crash or a normal exit
|
||||
* @param eventName
|
||||
* @param listener what to do with the data
|
||||
*/
|
||||
public override once<T extends keyof SernEventsMapping>(
|
||||
eventName: T,
|
||||
listener: (...args: SernEventsMapping[T][]) => void,
|
||||
): this {
|
||||
return super.once(eventName, listener);
|
||||
}
|
||||
/**
|
||||
* Listening to sern events with on. This event stays on until a crash or a normal exit
|
||||
* @param eventName
|
||||
* @param args the arguments for emitting the eventName
|
||||
*/
|
||||
public override emit<T extends keyof SernEventsMapping>(
|
||||
eventName: T,
|
||||
...args: SernEventsMapping[T]
|
||||
): boolean {
|
||||
return super.emit(eventName, ...args);
|
||||
}
|
||||
private static payload<T extends Payload>(
|
||||
type: PayloadType,
|
||||
module?: Module,
|
||||
reason?: unknown,
|
||||
) {
|
||||
return { type, module, reason } as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a compliant SernEmitter failure payload
|
||||
* @param module
|
||||
* @param reason
|
||||
*/
|
||||
static failure(module?: Module, reason?: unknown) {
|
||||
//The generic cast Payload & { type : PayloadType.* } coerces the type to be a failure payload
|
||||
// same goes to the other methods below
|
||||
return SernEmitter.payload<Payload & { type: PayloadType.Failure }>(
|
||||
PayloadType.Failure,
|
||||
module,
|
||||
reason,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Creates a compliant SernEmitter module success payload
|
||||
* @param module
|
||||
*/
|
||||
static success(module: Module) {
|
||||
return SernEmitter.payload<Payload & { type: PayloadType.Success }>(
|
||||
PayloadType.Success,
|
||||
module,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Creates a compliant SernEmitter module warning payload
|
||||
* @param reason
|
||||
*/
|
||||
static warning(reason: unknown) {
|
||||
return SernEmitter.payload<Payload & { type: PayloadType.Warning }>(
|
||||
PayloadType.Warning,
|
||||
undefined,
|
||||
reason,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SernEmitter;
|
||||
140
src/core/structures/enums.ts
Normal file
140
src/core/structures/enums.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @since 1.0.0
|
||||
* A bitfield that discriminates command modules
|
||||
* @enum { number }
|
||||
* @example
|
||||
* ```ts
|
||||
* export default commandModule({
|
||||
* // highlight-next-line
|
||||
* type : CommandType.Text,
|
||||
* name : 'a text command'
|
||||
* execute(message) {
|
||||
* console.log(message.content)
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export enum CommandType {
|
||||
/**
|
||||
* The CommandType for text commands
|
||||
*/
|
||||
Text = 1,
|
||||
/**
|
||||
* The CommandType for slash commands
|
||||
*/
|
||||
Slash = 2,
|
||||
/**
|
||||
* The CommandType for hybrid commands, text and slash
|
||||
*/
|
||||
Both = 3,
|
||||
/**
|
||||
* The CommandType for UserContextMenuInteraction commands
|
||||
*/
|
||||
CtxUser = 4,
|
||||
/**
|
||||
* The CommandType for MessageContextMenuInteraction commands
|
||||
*/
|
||||
CtxMsg = 8,
|
||||
/**
|
||||
* The CommandType for ButtonInteraction commands
|
||||
*/
|
||||
Button = 16,
|
||||
/**
|
||||
* The CommandType for StringSelectMenuInteraction commands
|
||||
*/
|
||||
StringSelect = 32,
|
||||
/**
|
||||
* The CommandType for ModalSubmitInteraction commands
|
||||
*/
|
||||
Modal = 64,
|
||||
/**
|
||||
* The CommandType for the other SelectMenuInteractions
|
||||
*/
|
||||
ChannelSelect = 256,
|
||||
MentionableSelect = 512,
|
||||
RoleSelect = 1024,
|
||||
UserSelect = 2048,
|
||||
}
|
||||
|
||||
/**
|
||||
* A bitfield that discriminates event modules
|
||||
* @enum { number }
|
||||
* @example
|
||||
* ```ts
|
||||
* export default eventModule({
|
||||
* //highlight-next-line
|
||||
* type : EventType.Discord,
|
||||
* name : 'guildMemberAdd'
|
||||
* execute(member : GuildMember) {
|
||||
* console.log(member)
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export enum EventType {
|
||||
/**
|
||||
* The EventType for handling discord events
|
||||
*/
|
||||
Discord = 1,
|
||||
/**
|
||||
* The EventType for handling sern events
|
||||
*/
|
||||
Sern = 2,
|
||||
/**
|
||||
* The EventType for handling external events.
|
||||
* Could be for example, `process` events, database events
|
||||
*/
|
||||
External = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* A bitfield that discriminates plugins
|
||||
* @enum { number }
|
||||
* @example
|
||||
* ```ts
|
||||
* export default function myPlugin() : EventPlugin<CommandType.Text> {
|
||||
* //highlight-next-line
|
||||
* type : PluginType.Event,
|
||||
* execute([ctx, args], controller) {
|
||||
* return controller.next();
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export enum PluginType {
|
||||
/**
|
||||
* The PluginType for InitPlugins
|
||||
*/
|
||||
Init = 1,
|
||||
/**
|
||||
* @deprecated
|
||||
* Use PluginType.Init instead
|
||||
*/
|
||||
Command = 1,
|
||||
/**
|
||||
* @deprecated
|
||||
* Use PluginType.Control instead
|
||||
*/
|
||||
Event = 2,
|
||||
/**
|
||||
* The PluginType for EventPlugins
|
||||
*/
|
||||
Control = 2,
|
||||
}
|
||||
/**
|
||||
* @enum { string }
|
||||
*/
|
||||
export enum PayloadType {
|
||||
/**
|
||||
* The PayloadType for a SernEmitter success event
|
||||
*/
|
||||
Success = 'success',
|
||||
/**
|
||||
* The PayloadType for a SernEmitter failure event
|
||||
*/
|
||||
Failure = 'failure',
|
||||
/**
|
||||
* The PayloadType for a SernEmitter warning event
|
||||
*/
|
||||
Warning = 'warning',
|
||||
}
|
||||
38
src/core/structures/errors.ts
Normal file
38
src/core/structures/errors.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @enum { string }
|
||||
*/
|
||||
export enum SernError {
|
||||
/**
|
||||
* Throws when registering an invalid module.
|
||||
* This means it is undefined or an invalid command type was provided
|
||||
*/
|
||||
InvalidModuleType = 'Detected an unknown module type',
|
||||
/**
|
||||
* Attempted to lookup module in command module store. Nothing was found!
|
||||
*/
|
||||
UndefinedModule = `A module could not be detected`,
|
||||
/**
|
||||
* Attempted to lookup module in command module store. Nothing was found!
|
||||
*/
|
||||
MismatchModule = `A module type mismatched with event emitted!`,
|
||||
/**
|
||||
* Unsupported interaction at this moment.
|
||||
*/
|
||||
NotSupportedInteraction = `This interaction is not supported.`,
|
||||
/**
|
||||
* One plugin called `controller.stop()` (end command execution / loading)
|
||||
*/
|
||||
PluginFailure = `A plugin failed to call controller.next()`,
|
||||
/**
|
||||
* A crash that occurs when accessing an invalid property of Context
|
||||
*/
|
||||
MismatchEvent = `You cannot use message when an interaction fired or vice versa`,
|
||||
/**
|
||||
* Unsupported feature attempted to access at this time
|
||||
*/
|
||||
NotSupportedYet = `This feature is not supported yet`,
|
||||
/**
|
||||
* Required Dependency not found
|
||||
*/
|
||||
MissingRequired = `@sern/client is required but was not found`,
|
||||
}
|
||||
6
src/core/structures/index.ts
Normal file
6
src/core/structures/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import Context from './context';
|
||||
import type Wrapper from './wrapper';
|
||||
import { ModuleStore } from './moduleStore';
|
||||
export * from './errors';
|
||||
export * from './enums';
|
||||
export { Context, Wrapper, ModuleStore };
|
||||
27
src/core/structures/moduleStore.ts
Normal file
27
src/core/structures/moduleStore.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { CommandModule } from '../../types/module';
|
||||
import { ApplicationCommandType, ComponentType } from 'discord.js';
|
||||
import type { Processed } from '../../types/handler';
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
* Storing all command modules
|
||||
* This dependency is usually injected into ModuleManager
|
||||
*/
|
||||
export class ModuleStore {
|
||||
readonly BothCommands = new Map<string, Processed<CommandModule>>();
|
||||
readonly ApplicationCommands = {
|
||||
[ApplicationCommandType.User]: new Map<string, Processed<CommandModule>>(),
|
||||
[ApplicationCommandType.Message]: new Map<string, Processed<CommandModule>>(),
|
||||
[ApplicationCommandType.ChatInput]: new Map<string, Processed<CommandModule>>(),
|
||||
};
|
||||
readonly ModalSubmit = new Map<string, Processed<CommandModule>>();
|
||||
readonly TextCommands = new Map<string, Processed<CommandModule>>();
|
||||
readonly InteractionHandlers = {
|
||||
[ComponentType.Button]: new Map<string, Processed<CommandModule>>(),
|
||||
[ComponentType.StringSelect]: new Map<string, Processed<CommandModule>>(),
|
||||
[ComponentType.ChannelSelect]: new Map<string, Processed<CommandModule>>(),
|
||||
[ComponentType.MentionableSelect]: new Map<string, Processed<CommandModule>>(),
|
||||
[ComponentType.RoleSelect]: new Map<string, Processed<CommandModule>>(),
|
||||
[ComponentType.UserSelect]: new Map<string, Processed<CommandModule>>(),
|
||||
};
|
||||
}
|
||||
16
src/core/structures/wrapper.ts
Normal file
16
src/core/structures/wrapper.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Dependencies } from '../../types/handler';
|
||||
|
||||
/**
|
||||
* @since 1.0.0
|
||||
* An object to be passed into Sern#init() function.
|
||||
* @typedef {object} Wrapper
|
||||
*/
|
||||
interface Wrapper {
|
||||
readonly defaultPrefix?: string;
|
||||
readonly commands: string;
|
||||
readonly events?: string;
|
||||
readonly containerConfig: {
|
||||
get: (...keys: (keyof Dependencies)[]) => unknown[];
|
||||
};
|
||||
}
|
||||
export default Wrapper;
|
||||
36
src/core/utilities/functions.ts
Normal file
36
src/core/utilities/functions.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as Files from '../module-loading/readFile';
|
||||
import { basename } from 'path';
|
||||
import { Err, Ok } from 'ts-results-es';
|
||||
/**
|
||||
* A function that returns whatever value is provided.
|
||||
* Warning: this evaluates { @param value }. It does not defer a value.
|
||||
* @param value
|
||||
* @__PURE__
|
||||
*/
|
||||
// prettier-ignore
|
||||
export const _const = <T>(value: T) => () => value;
|
||||
/**
|
||||
*
|
||||
* @param modName
|
||||
* @param absPath
|
||||
*/
|
||||
export function nameOrFilename(modName: string | undefined, absPath: string) {
|
||||
return modName ?? Files.fmtFileName(basename(absPath));
|
||||
}
|
||||
|
||||
//function wrappers for empty ok / err
|
||||
export const ok = _const(Ok.EMPTY);
|
||||
export const err = _const(Err.EMPTY);
|
||||
|
||||
export function partition<T, V>(arr: (T & V)[], condition: (e: T & V) => boolean): [T[], V[]] {
|
||||
const t: T[] = [];
|
||||
const v: V[] = [];
|
||||
for (const el of arr) {
|
||||
if (condition(el)) {
|
||||
t.push(el as T);
|
||||
} else {
|
||||
v.push(el as V);
|
||||
}
|
||||
}
|
||||
return [t, v];
|
||||
}
|
||||
47
src/core/utilities/treeSearch.ts
Normal file
47
src/core/utilities/treeSearch.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
|
||||
import type { SernAutocompleteData, SernOptionsData } from '../../types/module';
|
||||
|
||||
/**
|
||||
* Uses an iterative DFS to check if an autocomplete node exists
|
||||
* @param iAutocomplete
|
||||
* @param options
|
||||
*/
|
||||
export default function treeSearch(
|
||||
iAutocomplete: AutocompleteInteraction,
|
||||
options: SernOptionsData[] | undefined,
|
||||
): SernAutocompleteData | undefined {
|
||||
if (options === undefined) return undefined;
|
||||
const _options = options.slice(); // required to prevent direct mutation of options
|
||||
let autocompleteData: SernAutocompleteData | undefined;
|
||||
|
||||
while (_options.length > 0) {
|
||||
const cur = _options.pop()!;
|
||||
switch (cur.type) {
|
||||
case ApplicationCommandOptionType.Subcommand:
|
||||
{
|
||||
for (const option of cur.options ?? []) {
|
||||
_options.push(option);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ApplicationCommandOptionType.SubcommandGroup:
|
||||
{
|
||||
for (const command of cur.options ?? []) {
|
||||
_options.push(command);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
{
|
||||
if (cur.autocomplete) {
|
||||
const choice = iAutocomplete.options.getFocused(true);
|
||||
if (cur.name === choice.name && cur.autocomplete) {
|
||||
autocompleteData = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return autocompleteData;
|
||||
}
|
||||
Reference in New Issue
Block a user