diff --git a/src/core/plugin.ts b/src/core/plugin.ts index 88c21db..926204c 100644 --- a/src/core/plugin.ts +++ b/src/core/plugin.ts @@ -15,16 +15,107 @@ export function makePlugin( export function EventInitPlugin(execute: (args: InitArgs) => PluginResult) { return makePlugin(PluginType.Init, execute); } + /** + * Creates an initialization plugin for command preprocessing and modification + * * @since 2.5.0 + * @template I - Extends CommandType to enforce type safety for command modules + * + * @param {function} execute - Function to execute during command initialization + * @param {InitArgs} execute.args - The initialization arguments + * @param {T} execute.args.module - The command module being initialized + * @param {string} execute.args.absPath - The absolute path to the module file + * @param {Dependencies} execute.args.deps - Dependency injection container + * + * @returns {Plugin} A plugin that runs during command initialization + * + * @example + * // Plugin to update command description + * export const updateDescription = (description: string) => { + * return CommandInitPlugin(({ deps }) => { + * if(description.length > 100) { + * deps.logger?.info({ message: "Invalid description" }) + * return controller.stop("From updateDescription: description is invalid"); + * } + * module.description = description; + * return controller.next(); + * }); + * }; + * + * @example + * // Plugin to store registration date in module locals + * export const dateRegistered = () => { + * return CommandInitPlugin(({ module }) => { + * module.locals.registered = Date.now() + * return controller.next(); + * }); + * }; + * + * @remarks + * - Init plugins can modify how commands are loaded and perform preprocessing + * - The module.locals object can be used to store custom plugin-specific data + * - Be careful when modifying module fields as multiple plugins may interact with them + * - Use controller.next() to continue to the next plugin + * - Use controller.stop(reason) to halt plugin execution */ export function CommandInitPlugin( execute: (args: InitArgs) => PluginResult -) { +): Plugin { return makePlugin(PluginType.Init, execute); } + /** + * Creates a control plugin for command preprocessing, filtering, and state management + * * @since 2.5.0 + * @template I - Extends CommandType to enforce type safety for command modules + * + * @param {function} execute - Function to execute during command control flow + * @param {CommandArgs} execute.args - The command arguments array + * @param {Context} execute.args[0] - The discord context (e.g., guild, channel, user info, interaction) + * @param {SDT} execute.args[1] - The State, Dependencies, Params, Module, and Type object + * + * @returns {Plugin} A plugin that runs during command execution flow + * + * @example + * // Plugin to restrict command to specific guild + * export const inGuild = (guildId: string) => { + * return CommandControlPlugin((ctx, sdt) => { + * if(ctx.guild.id !== guildId) { + * return controller.stop(); + * } + * return controller.next(); + * }); + * }; + * + * @example + * // Plugins passing state through the chain + * const plugin1 = CommandControlPlugin((ctx, sdt) => { + * return controller.next({ 'plugin1/data': 'from plugin1' }); + * }); + * + * const plugin2 = CommandControlPlugin((ctx, sdt) => { + * return controller.next({ 'plugin2/data': ctx.user.id }); + * }); + * + * export default commandModule({ + * type: CommandType.Slash, + * plugins: [plugin1, plugin2], + * execute: (ctx, sdt) => { + * console.log(sdt.state); // Access accumulated state + * } + * }); + * + * @remarks + * - Control plugins are executed in order when a discord.js event is emitted + * - Use controller.next() to continue to next plugin or controller.stop() to halt execution + * - State can be passed between plugins using controller.next({ key: value }) + * - State keys should be namespaced to avoid collisions (e.g., 'plugin-name/key') + * - Final accumulated state is passed to the command's execute function + * - All plugins must succeed for the command to execute + * - Plugins have access to dependencies through the sdt.deps object + * - Useful for implementing preconditions, filters, and command preprocessing */ export function CommandControlPlugin( execute: (...args: CommandArgs) => PluginResult, diff --git a/src/handlers/event-utils.ts b/src/handlers/event-utils.ts index b97486a..4d1e247 100644 --- a/src/handlers/event-utils.ts +++ b/src/handlers/event-utils.ts @@ -1,10 +1,9 @@ -import type { Emitter, Logging } from '../core/interfaces'; +import type { Emitter } from '../core/interfaces'; import { SernError } from '../core/structures/enums' import { Ok, wrapAsync} from '../core/structures/result'; import type { Module } from '../types/core-modules'; import { inspect } from 'node:util' -import { resultPayload, } from '../core/functions' - +import { resultPayload } from '../core/functions' import merge from 'deepmerge' diff --git a/src/handlers/interaction.ts b/src/handlers/interaction.ts index ea63efa..09c37d4 100644 --- a/src/handlers/interaction.ts +++ b/src/handlers/interaction.ts @@ -2,7 +2,7 @@ import type { Module } from '../types/core-modules' import { callPlugins, executeModule } from './event-utils'; import { SernError } from '../core/structures/enums' import { createSDT, isAutocomplete, isCommand, isMessageComponent, isModal, resultPayload, treeSearch } from '../core/functions' -import { UnpackedDependencies } from '../types/utility'; +import type { UnpackedDependencies } from '../types/utility'; import * as Id from '../core/id' import { Context } from '../core/structures/context'; @@ -41,22 +41,20 @@ export function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: s } else if (isModal(event) || isMessageComponent(event)) { payload={ module, args: [event, createSDT(module, deps, params)] } } else { - throw Error("Invalid event") + throw Error("Unknown interaction while handling in interactionCreate event " + event) } const result = await callPlugins(payload) if(!result.ok) { reporter.emit('module.activate', resultPayload('failure', module, result.error ?? SernError.PluginFailure)) return } - if(payload.args.length != 2) { + if(payload.args.length !== 2) { throw Error ('Invalid payload') } //@ts-ignore assigning final state from plugin payload.args[1].state = result.value - // will be blocking if long task + await - // todo, add to task queue - + // note: do not await this. will be blocking if long task (ie waiting for modal input) executeModule(reporter, { module, args: payload.args }); }); } diff --git a/src/handlers/message.ts b/src/handlers/message.ts index a4b7fc5..4d241f6 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -2,7 +2,7 @@ import type { Message } from 'discord.js'; import { callPlugins, executeModule } from './event-utils'; import { SernError } from '../core/structures/enums' import { createSDT, fmt, resultPayload } from '../core/functions' -import { UnpackedDependencies } from '../types/utility'; +import type { UnpackedDependencies } from '../types/utility'; import type { Module } from '../types/core-modules'; import { Context } from '../core/structures/context'; diff --git a/src/handlers/presence.ts b/src/handlers/presence.ts index 1966d47..5b4ca3c 100644 --- a/src/handlers/presence.ts +++ b/src/handlers/presence.ts @@ -23,7 +23,8 @@ const parseConfig = async (conf: Promise, setPresence: SetPrese // If it's a promise, await it, otherwise use the value directly return result instanceof Promise ? await result : result; } catch (error) { - console.error(error); + // TODO process error + //console.error(error); return state; // Return previous state on error } }; @@ -43,7 +44,6 @@ const parseConfig = async (conf: Promise, setPresence: SetPrese processState(currentState) .then(newState => { - //console.log(newState) currentState = newState; return setPresence(currentState) }) diff --git a/src/handlers/user-defined-events.ts b/src/handlers/user-defined-events.ts index 6a89f49..be31a95 100644 --- a/src/handlers/user-defined-events.ts +++ b/src/handlers/user-defined-events.ts @@ -6,10 +6,11 @@ import type { UnpackedDependencies } from '../types/utility'; import type { Emitter } from '../core/interfaces'; import { inspect } from 'util' import { resultPayload } from '../core/functions'; +import { Wrapper } from '../' -export default async function(deps: UnpackedDependencies, eventDir: string) { +export default async function(deps: UnpackedDependencies, wrapper: Wrapper) { const eventModules: EventModule[] = []; - for await (const path of Files.readRecursive(eventDir)) { + for await (const path of Files.readRecursive(wrapper.events!)) { let { module } = await Files.importModule(path); await callInitPlugins(module, deps) eventModules.push(module as EventModule); @@ -40,17 +41,20 @@ export default async function(deps: UnpackedDependencies, eventDir: string) { const execute = async (...args: any[]) => { try { if(args) { - if('once' in module) { source.removeListener(module.name!, execute); } + if('once' in module) { source.removeListener(String(module.name!), execute); } await Reflect.apply(module.execute, null, args); } } catch(e) { const err = e instanceof Error ? e : Error(inspect(e, { colors: true })); + + //@ts-ignore if(!report.emit('error', resultPayload('failure', module, err))) { logger?.error({ message: inspect(err) }); } } } - source.addListener(module.name!, execute) + source.addListener(String(module.name!), execute) } } + diff --git a/src/index.ts b/src/index.ts index 7de8cdc..c0a01ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,7 +38,7 @@ export type { } from './types/core-plugin'; -export type { Payload, SernEventsMapping } from './types/utility'; +export type { Payload, SernEventsMapping, Wrapper } from './types/utility'; export { commandModule, diff --git a/src/sern.ts b/src/sern.ts index 92fd493..a9a461e 100644 --- a/src/sern.ts +++ b/src/sern.ts @@ -11,16 +11,11 @@ import ready from './handlers/ready'; import { interactionHandler } from './handlers/interaction'; import { messageHandler } from './handlers/message' import { presenceHandler } from './handlers/presence'; -import { UnpackedDependencies } from './types/utility'; +import { UnpackedDependencies, Wrapper } from './types/utility'; import type { Presence} from './core/presences'; import { registerTasks } from './handlers/tasks'; -interface Wrapper { - commands: string; - defaultPrefix?: string; - events?: string; - tasks?: string; -} + /** * @since 1.0.0 * @param maybeWrapper Options to pass into sern. @@ -39,7 +34,7 @@ export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) { const deps = useContainerRaw().deps(); if (maybeWrapper.events !== undefined) { - eventsHandler(deps, maybeWrapper.events) + eventsHandler(deps, maybeWrapper) .then(() => { deps['@sern/logger']?.info({ message: "Events registered" }); }); diff --git a/src/types/utility.ts b/src/types/utility.ts index 595aa2d..681ade7 100644 --- a/src/types/utility.ts +++ b/src/types/utility.ts @@ -1,11 +1,9 @@ import type { InteractionReplyOptions, MessageReplyOptions } from 'discord.js'; import type { Module } from './core-modules'; -import type { Result } from '../core/structures/result'; export type Awaitable = PromiseLike | T; export type Dictionary = Record -export type VoidResult = Result; export type AnyFunction = (...args: any[]) => unknown; export interface SernEventsMapping { @@ -26,3 +24,11 @@ export type UnpackedDependencies = { [K in keyof Dependencies]: UnpackFunction } export type ReplyOptions = string | Omit | MessageReplyOptions; + + +export interface Wrapper { + commands: string; + defaultPrefix?: string; + events?: string; + tasks?: string; +}