diff --git a/src/core/module-loading.ts b/src/core/module-loading.ts index 162b75b..ef14536 100644 --- a/src/core/module-loading.ts +++ b/src/core/module-loading.ts @@ -29,6 +29,7 @@ export async function defaultModuleLoader(absPath: string): Mo function checkIsProcessed(m: T): asserts m is Processed { assert.ok(m.name !== undefined, `name is not defined for ${util.format(m)}`); + assert.ok(m.description !== undefined, `description is not defined for ${util.format}`); } export const fmtFileName = (n: string) => n.substring(0, n.length - 3); diff --git a/src/handler/events/dispatchers/dispatchers.ts b/src/handler/events/dispatchers/dispatchers.ts index ed0e403..dd62210 100644 --- a/src/handler/events/dispatchers/dispatchers.ts +++ b/src/handler/events/dispatchers/dispatchers.ts @@ -7,7 +7,7 @@ import { EventEmitter } from 'node:events'; import * as assert from 'node:assert'; import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs'; import { arrayifySource, callPlugin } from '../../../core/operators'; -import { createResultResolver } from '../observableHandling'; +import { createResultResolver } from '../generic'; export function dispatchCommand(module: Processed, createArgs: () => unknown[]) { const args = createArgs(); diff --git a/src/handler/events/dispatchers/index.ts b/src/handler/events/dispatchers/index.ts index b754c3e..581daf2 100644 --- a/src/handler/events/dispatchers/index.ts +++ b/src/handler/events/dispatchers/index.ts @@ -1,2 +1,2 @@ export * from './dispatchers'; -export * from './provideArgs'; +export * from './provide-args'; diff --git a/src/handler/events/dispatchers/provideArgs.ts b/src/handler/events/dispatchers/provide-args.ts similarity index 100% rename from src/handler/events/dispatchers/provideArgs.ts rename to src/handler/events/dispatchers/provide-args.ts diff --git a/src/handler/events/generic.ts b/src/handler/events/generic.ts index 4735f88..47a5af1 100644 --- a/src/handler/events/generic.ts +++ b/src/handler/events/generic.ts @@ -5,13 +5,13 @@ import { InteractionType, Message, } from 'discord.js'; -import { Observable, filter, map } from 'rxjs'; +import { EMPTY, Observable, concatMap, filter, from, map, of, throwError, tap } from 'rxjs'; import { CommandType, ModuleManager } from '../../core'; import { SernError } from '../../core/structures/errors'; -import { filterMap } from '../../core/operators'; +import { callPlugin, everyPluginOk, filterMap, filterMapTo } from '../../core/operators'; import { defaultModuleLoader } from '../../core/module-loading'; -import { Processed } from '../../types/core'; -import { BothCommand, CommandModule } from '../../types/module'; +import { ImportPayload, Processed } from '../../types/core'; +import { BothCommand, CommandModule, EventModule, Module } from '../../types/module'; import { contextArgs, dispatchAutocomplete, dispatchCommand, interactionArg } from './dispatchers'; import { isAutocomplete } from '../../core/predicates'; import { ObservableInput, pipe, switchMap } from 'rxjs'; @@ -23,6 +23,7 @@ import { AnyModule } from '../../types/module'; import { Err, Result } from 'ts-results-es'; import { Awaitable } from '../../types/handler'; import { fmt } from './messages'; +import { ControlPlugin, VoidResult } from '../../types/plugin'; function createGenericHandler( source: Observable, @@ -139,3 +140,112 @@ export function buildModules( map(module => ({ module, absPath: module[sernMeta].fullPath })), ); } + +function hasPrefix(prefix: string, content: string) { + const prefixInContent = content.slice(0, prefix.length); + return prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0; +} + +/** + * Ignores messages from any person / bot except itself + * @param prefix + */ +export function isNonBot(prefix: string) { + return ({ author, content }: Message) => !author.bot && hasPrefix(prefix, content); +} + +/** + * Wraps the task in a Result as a try / catch. + * if the task is ok, an event is emitted and the stream becomes empty + * if the task is an error, throw an error down the stream which will be handled by catchError + * @param emitter reference to SernEmitter that will emit a successful execution of module + * @param module the module that will be executed with task + * @param task the deferred execution which will be called + */ +export function executeModule( + emitter: SernEmitter, + { + module, + task, + }: { + module: Processed; + task: () => Awaitable; + }, +) { + return of(module).pipe( + //converting the task into a promise so rxjs can resolve the Awaitable properly + concatMap(() => Result.wrapAsync(async () => task())), + concatMap(result => { + if (result.ok) { + emitter.emit('module.activate', SernEmitter.success(module)); + return EMPTY; + } else { + return throwError(() => SernEmitter.failure(module, result.val)); + } + }), + ); +} + +/** + * A higher order function that + * - creates a stream of {@link VoidResult} { config.createStream } + * - any failures results to { config.onFailure } being called + * - if all results are ok, the stream is converted to { config.onSuccess } + * emit config.onSuccess Observable + * @param config + * @returns receiver function for flattening a stream of data + */ +export function createResultResolver< + T extends { execute: (...args: any[]) => any; onEvent: ControlPlugin[] }, + Args extends { module: T; [key: string]: unknown }, + Output, +>(config: { + onStop?: (module: T) => unknown; + onNext: (args: Args) => Output; + createStream: (args: Args) => Observable; +}) { + return (args: Args) => { + const task$ = config.createStream(args); + return task$.pipe( + tap(result => { + result.err && config.onStop?.(args.module); + }), + everyPluginOk, + filterMapTo(() => config.onNext(args)), + ); + }; +} + +/** + * Calls a module's init plugins and checks for Err. If so, call { onStop } and + * ignore the module + */ +export function callInitPlugins< + T extends Processed, + Args extends ImportPayload, +>(config: { onStop?: (module: T) => unknown; onNext: (module: Args) => T }) { + return concatMap( + createResultResolver({ + createStream: args => from(args.module.plugins).pipe(callPlugin(args)), + ...config, + }), + ); +} + +/** + * Creates an executable task ( execute the command ) if all control plugins are successful + * @param onStop emits a failure response to the SernEmitter + */ +export function makeModuleExecutor< + M extends Processed, + Args extends { module: M; args: unknown[] }, +>(onStop: (m: M) => unknown) { + const onNext = ({ args, module }: Args) => ({ task: () => module.execute(...args), module }); + return concatMap( + createResultResolver({ + onStop, + createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)), + onNext, + }), + ); +} diff --git a/src/handler/events/interactions.ts b/src/handler/events/interactions.ts index fb04d7a..dcdfc0f 100644 --- a/src/handler/events/interactions.ts +++ b/src/handler/events/interactions.ts @@ -1,23 +1,15 @@ import { Interaction } from 'discord.js'; import { catchError, concatMap, finalize, merge } from 'rxjs'; import { SernError } from '../../core/structures/errors'; -import { executeModule, makeModuleExecutor } from './observableHandling'; -import { ErrorHandling, handleError } from '../../core/contracts/errorHandling'; +import { handleError } from '../../core/contracts/error-handling'; import { SernEmitter } from '../../core'; import { sharedObservable } from '../../core/operators'; import { useContainerRaw } from '../../core/dependencies'; -import type { Logging, ModuleManager } from '../../core/contracts'; -import type { EventEmitter } from 'node:events'; import { isAutocomplete, isCommand, isMessageComponent, isModal } from '../../core/predicates'; -import { createInteractionHandler } from './generic'; +import { createInteractionHandler, executeModule, makeModuleExecutor } from './generic'; +import { DependencyList } from '../../types/core'; -export function makeInteractionCreate([s, err, log, modules, client]: [ - SernEmitter, - ErrorHandling, - Logging | undefined, - ModuleManager, - EventEmitter, -]) { +export function makeInteractionCreate([s, err, log, modules, client]: DependencyList ) { const interactionStream$ = sharedObservable(client, 'interactionCreate'); const handle = createInteractionHandler(interactionStream$, modules); const interactionHandler$ = merge( diff --git a/src/handler/events/messages.ts b/src/handler/events/messages.ts index ebd43d7..31be551 100644 --- a/src/handler/events/messages.ts +++ b/src/handler/events/messages.ts @@ -1,13 +1,13 @@ import { catchError, concatMap, EMPTY, finalize } from 'rxjs'; import { SernError } from '../../core/structures/errors'; import type { Message } from 'discord.js'; -import { executeModule, ignoreNonBot, makeModuleExecutor } from './observableHandling'; -import { ErrorHandling, handleError } from '../../core/contracts/errorHandling'; +import { ErrorHandling, handleError } from '../../core/contracts/error-handling'; import type { Logging, ModuleManager } from '../../core/contracts'; import type { EventEmitter } from 'node:events'; import { SernEmitter, useContainerRaw } from '../../core'; import { sharedObservable } from '../../core/operators'; -import { createMessageHandler } from './generic'; +import { createMessageHandler, executeModule, isNonBot, makeModuleExecutor } from './generic'; +import { DependencyList } from '../../types/core'; /** * Removes the first character(s) _[depending on prefix length]_ of the message @@ -24,13 +24,7 @@ export function fmt(msg: string, prefix: string): string[] { } export function makeMessageCreate( - [s, err, log, modules, client]: [ - SernEmitter, - ErrorHandling, - Logging | undefined, - ModuleManager, - EventEmitter, - ], + [s, err, log, modules, client]: DependencyList, defaultPrefix: string | undefined, ) { if (!defaultPrefix) { @@ -39,7 +33,7 @@ export function makeMessageCreate( } const messageStream$ = sharedObservable(client, 'messageCreate'); const handler = createMessageHandler(messageStream$, defaultPrefix, modules); - const messageHandler = handler(ignoreNonBot(defaultPrefix) as (m: Message) => m is Message); + const messageHandler = handler(isNonBot(defaultPrefix) as (m: Message) => m is Message); return messageHandler.pipe( makeModuleExecutor(module => { s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure)); diff --git a/src/handler/events/observableHandling.ts b/src/handler/events/observableHandling.ts deleted file mode 100644 index ab1a7a8..0000000 --- a/src/handler/events/observableHandling.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { concatMap, EMPTY, from, Observable, of, tap, throwError } from 'rxjs'; -import { Result } from 'ts-results-es'; -import type { CommandModule, EventModule, Module } from '../../types/module'; -import { SernEmitter } from '../../core'; -import { callPlugin, everyPluginOk, filterMapTo } from '../../core/operators'; -import type { ImportPayload, Processed } from '../../types/core'; -import type { ControlPlugin, VoidResult } from '../../types/plugin'; -import { Awaitable } from '../../types/handler'; -import { Message } from 'discord.js'; -function hasPrefix(prefix: string, content: string) { - const prefixInContent = content.slice(0, prefix.length); - return prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0; -} - -/** - * Ignores messages from any person / bot except itself - * @param prefix - */ -export function ignoreNonBot(prefix: string) { - return ({ author, content }: Message) => !author.bot && hasPrefix(prefix, content); -} - -/** - * Wraps the task in a Result as a try / catch. - * if the task is ok, an event is emitted and the stream becomes empty - * if the task is an error, throw an error down the stream which will be handled by catchError - * @param emitter reference to SernEmitter that will emit a successful execution of module - * @param module the module that will be executed with task - * @param task the deferred execution which will be called - */ -export function executeModule( - emitter: SernEmitter, - { - module, - task, - }: { - module: Processed; - task: () => Awaitable; - }, -) { - return of(module).pipe( - //converting the task into a promise so rxjs can resolve the Awaitable properly - concatMap(() => Result.wrapAsync(async () => task())), - concatMap(result => { - if (result.ok) { - emitter.emit('module.activate', SernEmitter.success(module)); - return EMPTY; - } else { - return throwError(() => SernEmitter.failure(module, result.val)); - } - }), - ); -} - -/** - * A higher order function that - * - creates a stream of {@link VoidResult} { config.createStream } - * - any failures results to { config.onFailure } being called - * - if all results are ok, the stream is converted to { config.onSuccess } - * emit config.onSuccess Observable - * @param config - * @returns receiver function for flattening a stream of data - */ -export function createResultResolver< - T extends { execute: (...args: any[]) => any; onEvent: ControlPlugin[] }, - Args extends { module: T; [key: string]: unknown }, - Output, ->(config: { - onStop?: (module: T) => unknown; - onNext: (args: Args) => Output; - createStream: (args: Args) => Observable; -}) { - return (args: Args) => { - const task$ = config.createStream(args); - return task$.pipe( - tap(result => { - result.err && config.onStop?.(args.module); - }), - everyPluginOk, - filterMapTo(() => config.onNext(args)), - ); - }; -} - -/** - * Calls a module's init plugins and checks for Err. If so, call { onStop } and - * ignore the module - */ -export function callInitPlugins< - T extends Processed, - Args extends ImportPayload, ->(config: { onStop?: (module: T) => unknown; onNext: (module: Args) => T }) { - return concatMap( - createResultResolver({ - createStream: args => from(args.module.plugins).pipe(callPlugin(args)), - ...config, - }), - ); -} - -/** - * Creates an executable task ( execute the command ) if all control plugins are successful - * @param onStop emits a failure response to the SernEmitter - */ -export function makeModuleExecutor< - M extends Processed, - Args extends { module: M; args: unknown[] }, ->(onStop: (m: M) => unknown) { - const onNext = ({ args, module }: Args) => ({ task: () => module.execute(...args), module }); - return concatMap( - createResultResolver({ - onStop, - createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)), - onNext, - }), - ); -} diff --git a/src/handler/events/ready.ts b/src/handler/events/ready.ts index b7c98a8..e3c754b 100644 --- a/src/handler/events/ready.ts +++ b/src/handler/events/ready.ts @@ -1,20 +1,17 @@ import { ObservableInput, fromEvent, take } from 'rxjs'; -import { callInitPlugins } from './observableHandling'; import { CommandType } from '../../core/structures'; import { SernError } from '../../core/structures/errors'; import { Result } from 'ts-results-es'; import { ModuleManager } from '../../core/contracts'; import { SernEmitter, } from '../../core'; import { sernMeta } from '../../commands'; -import { Processed, ServerlessDependencyList, WebsocketDependencyList } from '../../types/core'; +import { Processed, DependencyList } from '../../types/core'; import { Module } from '../../types/module'; import * as assert from 'node:assert'; -import { buildModules } from './generic'; +import { buildModules, callInitPlugins } from './generic'; export function startReadyEvent( - [sEmitter, errorHandler, , moduleManager, client]: - | ServerlessDependencyList - | WebsocketDependencyList, + [sEmitter, errorHandler, , moduleManager, client]: DependencyList, input: ObservableInput, ) { const ready$ = fromEvent(client!, 'ready').pipe(take(1)); diff --git a/src/handler/events/userDefined.ts b/src/handler/events/user-defined.ts similarity index 93% rename from src/handler/events/userDefined.ts rename to src/handler/events/user-defined.ts index a4c4869..f01bf20 100644 --- a/src/handler/events/userDefined.ts +++ b/src/handler/events/user-defined.ts @@ -1,6 +1,5 @@ import { catchError, finalize, map, mergeAll, of } from 'rxjs'; import type { Dependencies, Processed, Wrapper } from '../../types/core'; -import { callInitPlugins } from './observableHandling'; import type { CommandModule, EventModule } from '../../types/module'; import type { EventEmitter } from 'node:events'; import { SernEmitter } from '../../core'; @@ -8,9 +7,9 @@ import type { ErrorHandling, Logging } from '../../core/contracts'; import { EventType } from '../../core/structures'; import { SernError } from '../../core/structures/errors'; import { eventDispatcher } from './dispatchers'; -import { handleError } from '../../core/contracts/errorHandling'; +import { handleError } from '../../core/contracts/error-handling'; import { useContainerRaw } from '../../core/dependencies'; -import { buildModules } from './generic'; +import { buildModules, callInitPlugins } from './generic'; export function makeEventsHandler( [s, err, log, client]: [SernEmitter, ErrorHandling, Logging | undefined, EventEmitter], diff --git a/src/handler/sern.ts b/src/handler/sern.ts index a1e7045..a194d58 100644 --- a/src/handler/sern.ts +++ b/src/handler/sern.ts @@ -1,4 +1,4 @@ -import { makeEventsHandler } from './events/userDefined'; +import { makeEventsHandler } from './events/user-defined'; import { makeInteractionCreate } from './events/interactions'; import { startReadyEvent } from './events/ready'; import { makeMessageCreate } from './events/messages';