diff --git a/src/core/_internal.ts b/src/core/_internal.ts index 5066fa3..b7ab4f3 100644 --- a/src/core/_internal.ts +++ b/src/core/_internal.ts @@ -2,7 +2,6 @@ import type { Result } from 'ts-results-es' export * from './operators'; export * from './functions'; -export { SernError } from './structures/enums'; export type _Module = { meta: { diff --git a/src/core/functions.ts b/src/core/functions.ts index b9f241d..dfbb2d8 100644 --- a/src/core/functions.ts +++ b/src/core/functions.ts @@ -61,10 +61,7 @@ export function treeSearch( { if ('autocomplete' in cur && cur.autocomplete) { const choice = iAutocomplete.options.getFocused(true); - assert( - 'command' in cur, - 'No `command` property found for autocomplete option', - ); + assert( 'command' in cur, 'No `command` property found for option ' + cur.name); if (subcommands.size > 0) { const parent = iAutocomplete.options.getSubcommand(); const parentAndOptionMatches = diff --git a/src/core/module-loading.ts b/src/core/module-loading.ts index ccac73d..32bf7b5 100644 --- a/src/core/module-loading.ts +++ b/src/core/module-loading.ts @@ -1,5 +1,6 @@ import path from 'node:path'; import { existsSync } from 'fs'; +import assert from 'node:assert'; export const parseCallsite = (site: string) => { @@ -33,17 +34,17 @@ export const shouldHandle = (pth: string, filenam: string) => { * esm javascript, typescript, and commonjs typescript * export default commandModule({}) */ -//async function importModule(absPath: string) { -// let fileModule = await import(absPath); -// -// let commandModule = fileModule.default; -// -// assert(commandModule , `No export @ ${absPath}. Forgot to ignore with "!"? (!${path.basename(absPath)})?`); -// if ('default' in commandModule) { -// commandModule = commandModule.default; -// } -// return { module: commandModule } as T; -//} +export async function importModule(absPath: string) { + let fileModule = await import(absPath); + + let commandModule = fileModule.default; + + assert(commandModule , `No export @ ${absPath}. Forgot to ignore with "!"? (!${path.basename(absPath)})?`); + if ('default' in commandModule) { + commandModule = commandModule.default; + } + return { module: commandModule } as T; +} export const fmtFileName = (fileName: string) => path.parse(fileName).name; diff --git a/src/core/modules.ts b/src/core/modules.ts index 1015cde..a8c87c5 100644 --- a/src/core/modules.ts +++ b/src/core/modules.ts @@ -21,7 +21,6 @@ export function commandModule(mod: InputCommand): _Module { const [onEvent, plugins] = partitionPlugins(mod.plugins); const initCallsite = get_callsite(callsites()).at(-2); if(!initCallsite) throw Error("initCallsite is null"); - const { name, absPath } = Files.parseCallsite(initCallsite); mod.name ??= name; mod.description ??= '...' //@ts-ignore diff --git a/src/core/operators.ts b/src/core/operators.ts index e2d0113..4bb95c7 100644 --- a/src/core/operators.ts +++ b/src/core/operators.ts @@ -19,6 +19,7 @@ import type { Emitter, ErrorHandling, Logging } from './interfaces'; import util from 'node:util'; import type { PluginResult } from '../types/core-plugin'; import type { VoidResult } from './_internal'; +import { Result } from 'ts-results-es'; /** * if {src} is true, mapTo V, else ignore * @param item @@ -70,11 +71,11 @@ export function handleError(crashHandler: ErrorHandling, emitter: Emitter, lo }; } //// Temporary until i get rxjs operators working on ts-results-es -//const filterTap = (onErr: (e: R) => void): OperatorFunction, K> => -// pipe(concatMap(result => { -// if(result.isOk()) { -// return of(result.value) -// } -// onErr(result.error); -// return EMPTY -// })) +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/structures/core-context.ts b/src/core/structures/core-context.ts index acff87b..e5eed99 100644 --- a/src/core/structures/core-context.ts +++ b/src/core/structures/core-context.ts @@ -1,5 +1,5 @@ import { Result as Either } from 'ts-results-es'; -import { SernError } from '../_internal'; +import { SernError } from './enums'; import * as assert from 'node:assert'; /** diff --git a/src/handlers/event-utils.ts b/src/handlers/event-utils.ts index dd5ae4b..c199128 100644 --- a/src/handlers/event-utils.ts +++ b/src/handlers/event-utils.ts @@ -18,7 +18,6 @@ import { everyPluginOk, filterMapTo, handleError, - SernError, type VoidResult, resultPayload, arrayifySource, @@ -28,7 +27,7 @@ import { } from '../core/_internal'; import * as Id from '../core/id' import type { Emitter, ErrorHandling, Logging } from '../core/interfaces'; -import { PayloadType } from '../core/structures/enums' +import { PayloadType, SernError } from '../core/structures/enums' import { Err, Ok, Result } from 'ts-results-es'; import type { Awaitable } from '../types/utility'; import type { ControlPlugin } from '../types/core-plugin'; @@ -90,7 +89,6 @@ export function createDispatcher(payload: { module: Processed; ev const { command } = option; return { - ...payload, module: command as Processed, //autocomplete is not a true "module" warning cast! args: [payload.event], }; @@ -256,16 +254,13 @@ export function callInitPlugins>(sernEmitter: Emitte } /** - * Creates an executable task ( execute the command ) if all control plugins are successful + * 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) { + Args extends { module: M; args: unknown[]; }> +(onStop: (m: M) => unknown) { const onNext = ({ args, module }: Args) => ({ task: () => module.execute(...args), module, diff --git a/src/handlers/interaction.ts b/src/handlers/interaction.ts new file mode 100644 index 0000000..d9b3c71 --- /dev/null +++ b/src/handlers/interaction.ts @@ -0,0 +1,30 @@ +import type { Interaction } from 'discord.js'; +import { mergeMap, merge, concatMap } from 'rxjs'; +import { PayloadType } from '../core/structures/enums'; +import { filterTap } from '../core/operators' +import { + isAutocomplete, + isCommand, + isMessageComponent, + isModal, + sharedEventStream, + resultPayload, +} from '../core/_internal'; +import { createInteractionHandler, executeModule, makeModuleExecutor } from './event-utils'; +import type { DependencyList } from '../types/ioc'; +import { SernError } from '../core/structures/enums' +export function interactionHandler([emitter, err, log, client]: DependencyList) { + const interactionStream$ = sharedEventStream(client, 'interactionCreate'); + const modules = new Map(); + const handle = createInteractionHandler(interactionStream$, modules); + + const interactionHandler$ = merge(handle(isMessageComponent), + handle(isAutocomplete), + handle(isCommand), + handle(isModal)); + return interactionHandler$ + .pipe(filterTap(e => emitter.emit('warning', resultPayload(PayloadType.Warning, undefined, e))), + concatMap(makeModuleExecutor(module => + emitter.emit('module.activate', resultPayload(PayloadType.Failure, module, SernError.PluginFailure)))), + mergeMap(payload => executeModule(emitter, log, err, payload))); +} diff --git a/src/handlers/message-event.ts b/src/handlers/message.ts similarity index 51% rename from src/handlers/message-event.ts rename to src/handlers/message.ts index 4448648..bcc6a66 100644 --- a/src/handlers/message-event.ts +++ b/src/handlers/message.ts @@ -1,8 +1,11 @@ -import { EMPTY } from 'rxjs'; +import { EMPTY, mergeMap, concatMap } from 'rxjs'; import type { Message } from 'discord.js'; import { sharedEventStream } from '../core/_internal'; import type { DependencyList } from '../types/ioc'; - +import { createMessageHandler, executeModule, makeModuleExecutor } from './event-utils'; +import { PayloadType, SernError } from '../core/structures/enums' +import { resultPayload } from '../core/functions' +import { filterTap } from '../core/operators' /** * Ignores messages from any person / bot except itself * @param prefix @@ -24,16 +27,17 @@ export function messageHandler( log?.debug({ message: 'No prefix found. message handler shutting down' }); return EMPTY; } + const modules = new Map() const messageStream$ = sharedEventStream(client, 'messageCreate'); -// const handle = createMessageHandler(messageStream$, defaultPrefix, modules); -// -// const msgCommands$ = handle(isNonBot(defaultPrefix)); -// -// return msgCommands$.pipe( -// filterTap((e) => emitter.emit('warning', resultPayload(PayloadType.Warning, undefined, e))), -// concatMap(makeModuleExecutor(module => { -// const result = resultPayload(PayloadType.Failure, module, SernError.PluginFailure); -// emitter.emit('module.activate', result); -// })), -// mergeMap(payload => executeModule(emitter, log, err, payload))); + const handle = createMessageHandler(messageStream$, defaultPrefix, modules); + + const msgCommands$ = handle(isNonBot(defaultPrefix)); + + return msgCommands$.pipe( + filterTap((e) => emitter.emit('warning', resultPayload(PayloadType.Warning, undefined, e))), + concatMap(makeModuleExecutor(module => { + const result = resultPayload(PayloadType.Failure, module, SernError.PluginFailure); + emitter.emit('module.activate', result); + })), + mergeMap(payload => executeModule(emitter, log, err, payload))); } diff --git a/src/handlers/presence.ts b/src/handlers/presence.ts index 821858e..b393e3d 100644 --- a/src/handlers/presence.ts +++ b/src/handlers/presence.ts @@ -1,7 +1,8 @@ -import { interval, scan, startWith, fromEvent, take, of } from "rxjs" +import { concatMap, from, interval, of, map, scan, startWith, fromEvent, take } from "rxjs" import { PresenceConfig, PresenceResult } from "../core/presences"; +import { Services } from "../core/ioc"; import assert from "node:assert"; - +import * as Files from "../core/module-loading"; type SetPresence = (conf: PresenceResult) => Promise const parseConfig = async (conf: Promise) => { @@ -21,19 +22,23 @@ const parseConfig = async (conf: Promise) => { }) }; -// const presence = Files -// .importModule(path) -// .then(({ module }) => { -// //fetch services with the order preserved, passing it to the execute fn -// const fetchedServices = Services(...module.inject ?? []); -// return async () => module.execute(...fetchedServices); -// }) -// const module$ = from(presence); -// return module$.pipe( -// //compose: -// //call the execute function, passing that result into parseConfig. -// //concatMap resolves the promise, and passes it to the next concatMap. -// concatMap(fn => parseConfig(fn())), -// // subscribe to the observable parseConfig yields, and set the presence. -// concatMap(conf => conf.pipe(map(setPresence)))); - +export const presenceHandler = (path: string, setPresence: SetPresence) => { + interface PresenceModule { + module: PresenceConfig<(keyof Dependencies)[]> + } + const presence = Files + .importModule(path) + .then(({ module }) => { + //fetch services with the order preserved, passing it to the execute fn + const fetchedServices = Services(...module.inject ?? []); + return async () => module.execute(...fetchedServices); + }) + const module$ = from(presence); + return module$.pipe( + //compose: + //call the execute function, passing that result into parseConfig. + //concatMap resolves the promise, and passes it to the next concatMap. + concatMap(fn => parseConfig(fn())), + // subscribe to the observable parseConfig yields, and set the presence. + concatMap(conf => conf.pipe(map(setPresence)))); +} diff --git a/src/handlers/ready-event.ts b/src/handlers/ready-event.ts index 2be7d27..b92aa9e 100644 --- a/src/handlers/ready-event.ts +++ b/src/handlers/ready-event.ts @@ -2,6 +2,7 @@ import { ObservableInput, concat, first, fromEvent, ignoreElements, pipe, tap } import { _Module } from '../core/_internal'; import { Logging } from '../core/interfaces'; import type { DependencyList } from '../types/ioc'; +import { callInitPlugins } from './event-utils'; const once = (log: Logging | undefined) => pipe( tap(() => { log?.info({ message: "Waiting on discord client to be ready..." }) }), @@ -15,8 +16,7 @@ export function readyHandler( //Todo: add module manager on on ready const ready$ = fromEvent(client!, 'ready').pipe(once(log)); - concat(ready$) - //.pipe(callInitPlugins(sEmitter)) + return concat(ready$).pipe(callInitPlugins(sEmitter)) // const validModuleType = module.type >= 0 && module.type <= 1 << 10; // assert.ok(validModuleType, // `Found ${module.name} at ${module.meta.fullPath}, which does not have a valid type`); diff --git a/src/handlers/user-defined-events.ts b/src/handlers/user-defined-events.ts index a62974a..7cefb6a 100644 --- a/src/handlers/user-defined-events.ts +++ b/src/handlers/user-defined-events.ts @@ -1,6 +1,5 @@ import { ObservableInput } from 'rxjs'; -import { EventType } from '../core/structures/enums'; -import { SernError } from '../core/_internal'; +import { EventType, SernError } from '../core/structures/enums'; import { eventDispatcher } from './event-utils' import { Service } from '../core/ioc'; import type { DependencyList } from '../types/ioc'; @@ -23,14 +22,14 @@ export function eventsHandler( throw Error(SernError.InvalidModuleType + ' while creating event handler'); } }; -// buildModules(allPaths) -// .pipe( -// callInitPlugins(emitter), -// map(intoDispatcher), -// /** -// * Where all events are turned on -// */ -// mergeAll(), -// handleCrash(err, emitter, log)) -// .subscribe(); + buildModules(allPaths) + .pipe( + callInitPlugins(emitter), + map(intoDispatcher), + /** + * Where all events are turned on + */ + mergeAll(), + handleCrash(err, emitter, log)) + .subscribe(); } diff --git a/src/sern.ts b/src/sern.ts index 4045caf..fbf8f39 100644 --- a/src/sern.ts +++ b/src/sern.ts @@ -1,44 +1,65 @@ import callsites from 'callsites'; -import * as Files from './core/module-loading'; +import * as Files from './core/module-loading'; +import { merge } from 'rxjs'; import { Services } from './core/ioc'; -import type { DependencyList } from './types/ioc'; +import { eventsHandler } from './handlers/user-defined-events'; +import { readyHandler } from './handlers/ready-event'; +import { messageHandler } from './handlers/message'; +import { interactionHandler } from './handlers/interaction'; +import { presenceHandler } from './handlers/presence'; +import { Client } from 'discord.js'; +import { handleCrash } from './handlers/event-utils'; + interface Wrapper { commands?: string; defaultPrefix?: string; events?: string; } - -const __start = (entryPoint: string, - wrapper: { defaultPrefix?: string }, - dependencies: DependencyList) => { - //@ts-ignore sern handler generates handler.js - import(entryPoint) - .then(({ commands=new Map(), events=new Map() }) => { - console.log(commands, events) - }) - .catch(err => dependencies[2]?.error({ message: err })); -} /** * @since 1.0.0 * @param wrapper Options to pass into sern. * Function to start the handler up * @example * ```ts title="src/index.ts" - * Sern.init() + * Sern.init({ + * commands: 'dist/commands', + * events: 'dist/events', + * }) * ``` */ -export function init(wrapper: Wrapper) { +export function init(wrapper?: Wrapper) { + wrapper ??= { commands: "./dist/commands", events: "./dist/events" }; const startTime = performance.now(); const dependencies = Services('@sern/emitter', '@sern/errors', '@sern/logger', '@sern/client'); + const logger = dependencies[2], + errorHandler = dependencies[1]; + + if (wrapper.events !== undefined) { + eventsHandler(dependencies, Files.getFullPathTree(wrapper.events)); + } const initCallsite = callsites()[1].getFileName(); - const handlerModule = Files.shouldHandle(initCallsite!, "handler"); - if(!handlerModule.exists) { - throw Error("Could not find handler module, did you run sern build?") - } - __start(handlerModule.path, wrapper, dependencies); -} + const presencePath = Files.shouldHandle(initCallsite!, "presence"); + //Ready event: load all modules and when finished, time should be taken and logged + readyHandler(dependencies, Files.getFullPathTree(wrapper.commands)) + .add(() => { + logger?.info({ message: "Client signaled ready, registering modules" }); + const time = ((performance.now() - startTime) / 1000).toFixed(2); + dependencies[0].emit('modulesLoaded'); + logger?.info({ message: `sern: registered in ${time} s`, }); + if(presencePath.exists) { + const setPresence = async (p: any) => { + return (dependencies[4] as Client).user?.setPresence(p); + } + presenceHandler(presencePath.path, setPresence).subscribe(); + } + }); + const messages$ = messageHandler(dependencies, wrapper.defaultPrefix); + const interactions$ = interactionHandler(dependencies); + // listening to the message stream and interaction stream + merge(messages$, interactions$).pipe(handleCrash(errorHandler, dependencies[0], logger)).subscribe(); +}