diff --git a/src/core/id.ts b/src/core/id.ts index 3f91f92..b795945 100644 --- a/src/core/id.ts +++ b/src/core/id.ts @@ -44,8 +44,8 @@ const TypeMap = new Map([[CommandType.Text, 0], [CommandType.CtxUser, ApplicationCommandType.User], [CommandType.CtxMsg, ApplicationCommandType.Message], [CommandType.Button, ComponentType.Button], - [CommandType.Modal, InteractionType.ModalSubmit], [CommandType.StringSelect, ComponentType.StringSelect], + [CommandType.Modal, InteractionType.ModalSubmit], [CommandType.UserSelect, ComponentType.UserSelect], [CommandType.MentionableSelect, ComponentType.MentionableSelect], [CommandType.RoleSelect, ComponentType.RoleSelect], diff --git a/src/core/operators.ts b/src/core/operators.ts deleted file mode 100644 index 2d06a90..0000000 --- a/src/core/operators.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * This file holds sern's rxjs operators used for processing data. - * Each function should be modular and testable, not bound to discord / sern - * and independent of each other. - */ -import { - concatMap, - EMPTY, - fromEvent, - Observable, - of, - OperatorFunction, - share, -} from 'rxjs'; -import type { Emitter, ErrorHandling, Logging } from './interfaces'; -import util from 'node:util'; -import type { Result } from 'ts-results-es'; - -/** - * if {src} is true, mapTo V, else ignore - * @param item - */ -export function filterMapTo(item: () => V): OperatorFunction { - return concatMap(keep => keep ? of(item()) : EMPTY); -} - -export const arrayifySource = (src: T) => - Array.isArray(src) ? src : [src]; - -export const sharedEventStream = (e: Emitter, eventName: string) => - (fromEvent(e, eventName) as Observable).pipe(share()); - -export function handleError(crashHandler: ErrorHandling, emitter: Emitter, logging?: Logging) { - return (pload: unknown, caught: Observable) => { - // This is done to fit the ErrorHandling contract - if(!emitter.emit('error', pload)) { - const err = pload instanceof Error ? pload : Error(util.inspect(pload, { colors: true })); - logging?.error({ message: util.inspect(pload) }); - crashHandler.updateAlive(err); - } - return caught; - }; -} -//// Temporary until i get rxjs operators working on ts-results-es -export const filterTap = (onErr: (e: R) => void): OperatorFunction, K> => - concatMap(result => { - if(result.isOk()) { - return of(result.value) - } - onErr(result.error); - return EMPTY; - }) diff --git a/src/core/presences.ts b/src/core/presences.ts index 0031c1f..d006c44 100644 --- a/src/core/presences.ts +++ b/src/core/presences.ts @@ -61,4 +61,5 @@ export const Presence = { export type PresenceConfig = { inject?: [...T] execute: (...v: IntoDependencies) => PresenceResult; + }; diff --git a/src/core/structures/context.ts b/src/core/structures/context.ts index 59dcbb0..69b460c 100644 --- a/src/core/structures/context.ts +++ b/src/core/structures/context.ts @@ -13,6 +13,7 @@ import { Result, Ok, Err } from 'ts-results-es'; import * as assert from 'assert'; import type { ReplyOptions } from '../../types/utility'; import { fmt } from '../functions' +import { SernError } from './enums'; /** @@ -21,7 +22,7 @@ import { fmt } from '../functions' * Message and ChatInputCommandInteraction */ export class Context extends CoreContext { - + get options() { if(this.isMessage()) { const [, ...rest] = fmt(this.message.content, this.prefix); @@ -30,6 +31,7 @@ export class Context extends CoreContext { return this.interaction.options; } } + protected constructor(protected ctx: Result, private __prefix?: string) { @@ -94,6 +96,15 @@ export class Context extends CoreContext { .mapErr(i => i.member)); } + get message(): Message { + return this.ctx.expect(SernError.MismatchEvent); + } + + get interaction(): ChatInputCommandInteraction { + return this.ctx.expectErr(SernError.MismatchEvent); + } + + public get client(): Client { return safeUnwrap(this.ctx .map(m => m.client) diff --git a/src/core/structures/core-context.ts b/src/core/structures/core-context.ts index 8ffd6a0..73a92d5 100644 --- a/src/core/structures/core-context.ts +++ b/src/core/structures/core-context.ts @@ -1,5 +1,4 @@ import { Result as Either } from 'ts-results-es'; -import { SernError } from './enums'; import * as assert from 'node:assert'; /** @@ -9,13 +8,6 @@ export abstract class CoreContext { protected constructor(protected ctx: Either) { assert.ok(typeof ctx === 'object' && ctx != null, "Context was nonobject or null"); } - get message(): M { - return this.ctx.expect(SernError.MismatchEvent); - } - get interaction(): I { - return this.ctx.expectErr(SernError.MismatchEvent); - } - public isMessage(): this is CoreContext { return this.ctx.isOk(); } diff --git a/src/handlers/event-utils.ts b/src/handlers/event-utils.ts index 1919a25..129d456 100644 --- a/src/handlers/event-utils.ts +++ b/src/handlers/event-utils.ts @@ -1,11 +1,12 @@ import type { Interaction, Message, BaseInteraction } from 'discord.js'; +import util from 'node:util'; import { EMPTY, type Observable, concatMap, filter, throwError, fromEvent, map, type OperatorFunction, - catchError, finalize, pipe, from, take, + catchError, finalize, pipe, from, take, share, of, } from 'rxjs'; import * as Id from '../core/id' -import type { Emitter } from '../core/interfaces'; +import type { Emitter, ErrorHandling, Logging } from '../core/interfaces'; import { SernError } from '../core/structures/enums' import { Err, Ok, Result } from 'ts-results-es'; import type { UnpackedDependencies } from '../types/utility'; @@ -15,9 +16,22 @@ import { Context } from '../core/structures/context'; import { CommandType } from '../core/structures/enums' import { inspect } from 'node:util' import { disposeAll } from '../core/ioc/base'; -import { arrayifySource, handleError } from '../core/operators'; import { resultPayload, isAutocomplete, treeSearch, fmt } from '../core/functions' +function handleError(crashHandler: ErrorHandling, emitter: Emitter, logging?: Logging) { + return (pload: unknown, caught: Observable) => { + // This is done to fit the ErrorHandling contract + if(!emitter.emit('error', pload)) { + const err = pload instanceof Error ? pload : Error(util.inspect(pload, { colors: true })); + logging?.error({ message: util.inspect(pload) }); + crashHandler.updateAlive(err); + } + return caught; + }; +} +const arrayify= (src: T) => + Array.isArray(src) ? src : [src]; + interface ExecutePayload { module: Module; args: unknown[]; @@ -26,8 +40,20 @@ interface ExecutePayload { [key: string]: unknown } +export const filterTap = (onErr: (e: R) => void): OperatorFunction, K> => + concatMap(result => { + if(result.isOk()) { + return of(result.value) + } + onErr(result.error); + return EMPTY; + }) + +export const sharedEventStream = (e: Emitter, eventName: string) => + (fromEvent(e, eventName) as Observable).pipe(share()); + function intoPayload(module: Module, deps: Dependencies) { - return pipe(map(arrayifySource), + return pipe(map(arrayify), map(args => ({ module, args, deps })), map(p => p.args)); } @@ -151,10 +177,7 @@ export function createMessageHandler( * @param module the module that will be executed with task * @param task the deferred execution which will be called */ -export function executeModule( - emitter: Emitter, - { module, args }: ExecutePayload, -) { +export function executeModule(emitter: Emitter, { module, args }: ExecutePayload) { return from(Result.wrapAsync(async () => module.execute(...args))) .pipe(concatMap(result => { if (result.isOk()) { @@ -181,6 +204,7 @@ export function createResultResolver(config: { const { onStop, onNext } = config; return async (payload: ExecutePayload) => { const task = await callPlugins(payload); + if (!task) throw Error("Plugin did not return anything."); if(task.isOk()) { return onNext(payload, task.value) as Output; } else { @@ -189,18 +213,16 @@ export function createResultResolver(config: { }; }; -export async function callInitPlugins(module: Module, deps: Dependencies, emit?: boolean ) { +export async function callInitPlugins(module: Module, deps: Dependencies, emit?: boolean) { let _module = module; const emitter = deps['@sern/emitter']; for(const plugin of _module.plugins ?? []) { const res = await plugin.execute({ - module, absPath: _module.meta.absPath, - updateModule: (partial: Partial) => { - _module = { ..._module, ...partial }; - return _module; - }, + module: _module, + absPath: _module.meta.absPath, deps }); + if (!res) throw Error("Plugin did not return anything."); if(res.isErr()) { if(emit) { emitter?.emit('module.register', diff --git a/src/handlers/interaction.ts b/src/handlers/interaction.ts index 11696c4..0c95e1d 100644 --- a/src/handlers/interaction.ts +++ b/src/handlers/interaction.ts @@ -1,7 +1,6 @@ import type { Interaction } from 'discord.js'; import { mergeMap, merge, concatMap, EMPTY } from 'rxjs'; -import { filterTap, sharedEventStream } from '../core/operators' -import { createInteractionHandler, executeModule, intoTask } from './event-utils'; +import { createInteractionHandler, executeModule, intoTask, sharedEventStream, filterTap } from './event-utils'; import { SernError } from '../core/structures/enums' import { isAutocomplete, isCommand, isMessageComponent, isModal, resultPayload } from '../core/functions' import { UnpackedDependencies } from '../types/utility'; diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 125889f..60b3b8d 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -1,9 +1,8 @@ import { EMPTY, mergeMap, concatMap } from 'rxjs'; import type { Message } from 'discord.js'; -import { createMessageHandler, executeModule, intoTask } from './event-utils'; -import { PayloadType, SernError } from '../core/structures/enums' +import { createMessageHandler, executeModule, intoTask, sharedEventStream, filterTap} from './event-utils'; +import { SernError } from '../core/structures/enums' import { resultPayload } from '../core/functions' -import { filterTap, sharedEventStream } from '../core/operators' import { UnpackedDependencies } from '../types/utility'; import type { Emitter } from '../core/interfaces'; @@ -36,7 +35,7 @@ function (deps: UnpackedDependencies, defaultPrefix?: string) { const msgCommands$ = handle(isNonBot(defaultPrefix)); return msgCommands$.pipe( - filterTap(e => emitter.emit('warning', resultPayload(PayloadType.Warning, undefined, e))), + filterTap(e => emitter.emit('warning', resultPayload('warning', undefined, e))), concatMap(intoTask(module => { const result = resultPayload('failure', module, SernError.PluginFailure); emitter.emit('module.activate', result); diff --git a/src/sern.ts b/src/sern.ts index 3cd067d..5ea5b1e 100644 --- a/src/sern.ts +++ b/src/sern.ts @@ -10,6 +10,7 @@ import { handleCrash } from './handlers/event-utils'; import { useContainerRaw } from './core/ioc/global'; import { UnpackedDependencies } from './types/utility'; import type { PresenceResult } from './core/presences'; +import fs from 'fs/promises' interface Wrapper { commands: string; @@ -63,3 +64,16 @@ export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) { // listening to the message stream and interaction stream merge(messages$, interactions$).pipe(handleCrash(deps)).subscribe(); } + + +export async function publisher() { + const directoryToWatch = "./src/commands"; + const watcher = fs.watch(directoryToWatch, { recursive: true }, ); + for await (const { eventType, filename } of watcher) { + switch(eventType) { + case 'change': { + console.log('change', filename) + } break; + } + } +} diff --git a/src/types/core-modules.ts b/src/types/core-modules.ts index c887b78..2e8ebb3 100644 --- a/src/types/core-modules.ts +++ b/src/types/core-modules.ts @@ -14,6 +14,7 @@ import type { StringSelectMenuInteraction, UserContextMenuCommandInteraction, UserSelectMenuInteraction, + ChatInputCommandInteraction, } from 'discord.js'; import type { CommandType, EventType } from '../core/structures/enums'; import { Context } from '../core/structures/context' @@ -123,14 +124,14 @@ export interface DiscordEventCommand Awaitable; + execute: (ctx: Context & { get options(): string[] }, tbd: SDT) => Awaitable; } export interface SlashCommand extends Module { type: CommandType.Slash; description: string; options?: SernOptionsData[]; - execute: (ctx: Context, tbd: SDT) => Awaitable; + execute: (ctx: Context & { get options(): ChatInputCommandInteraction['options']}, tbd: SDT) => Awaitable; } export interface BothCommand extends Module { diff --git a/test/handlers/index.test.ts b/test/handlers/index.test.ts index 315eb4c..77a1358 100644 --- a/test/handlers/index.test.ts +++ b/test/handlers/index.test.ts @@ -56,9 +56,11 @@ vi.mock('discord.js', async (importOriginal) => { }); function createRandomPlugin (s: 'go', mut?: Partial) { - return CommandInitPlugin(({ module, updateModule }) => { + return CommandInitPlugin(({ module }) => { if(mut) { - updateModule(mut) + for(const [k,v] of Object.entries(mut)) { + module[k] = v + } } return s == 'go' ? controller.next() @@ -105,8 +107,10 @@ test ('mutate with init plugins', async () => { const deps = mockDeps() const plugins = createRandomPlugin('go', { name: "abc" }) const mod = createRandomModule([plugins]) + console.log(mod) const s = await callInitPlugins(mod, deps, false) - expect(s.name).not.equal(mod.name) + console.log(s) + expect("abc").equal(s.name) })