mirror of
https://github.com/sern-handler/handler
synced 2026-06-21 07:12:15 +00:00
add deps to plugin calls and execute
This commit is contained in:
@@ -24,7 +24,6 @@ export function filterMapTo<V>(item: () => V): OperatorFunction<boolean, V> {
|
||||
return concatMap(keep => keep ? of(item()) : EMPTY);
|
||||
}
|
||||
|
||||
|
||||
export const arrayifySource = <T>(src: T) =>
|
||||
Array.isArray(src) ? src : [src];
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
|
||||
);
|
||||
}
|
||||
|
||||
static override wrap(wrappable: BaseInteraction | Message, prefix?: string): Context {
|
||||
static wrap(wrappable: BaseInteraction | Message, prefix?: string): Context {
|
||||
if ('interaction' in wrappable) {
|
||||
return new Context(Ok(wrappable), prefix);
|
||||
}
|
||||
|
||||
@@ -23,10 +23,4 @@ export abstract class CoreContext<M, I> {
|
||||
public isSlash(): this is CoreContext<never, I> {
|
||||
return !this.isMessage();
|
||||
}
|
||||
//todo: add agnostic options resolver for Context
|
||||
abstract get options(): unknown;
|
||||
|
||||
static wrap(_: unknown): unknown {
|
||||
throw Error('You need to override this method; cannot wrap an abstract class');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,10 +65,9 @@ export class Cron implements Emitter {
|
||||
const retrievedModule = this.modules.get(eventName);
|
||||
if(!retrievedModule) throw Error("Adding task: module " +eventName +"was not found");
|
||||
const { pattern, name, runOnInit, timezone } = retrievedModule;
|
||||
const task = cron.schedule(pattern,
|
||||
(date) => listener({ date, deps: this.deps }),
|
||||
{ name, runOnInit, timezone, scheduled: true });
|
||||
task.on('task-failed', console.error)
|
||||
cron.schedule(pattern,
|
||||
(date) => listener({ date, deps: this.deps }),
|
||||
{ name, runOnInit, timezone, scheduled: true });
|
||||
return this;
|
||||
}
|
||||
removeListener(eventName: string | symbol, listener: AnyFunction) {
|
||||
|
||||
@@ -4,19 +4,20 @@ import {
|
||||
Observable,
|
||||
concatMap,
|
||||
filter,
|
||||
of,
|
||||
throwError,
|
||||
fromEvent, map, OperatorFunction,
|
||||
fromEvent,
|
||||
map,
|
||||
type OperatorFunction,
|
||||
catchError,
|
||||
finalize,
|
||||
pipe,
|
||||
from,
|
||||
} from 'rxjs';
|
||||
import * as Id from '../core/id'
|
||||
import type { Emitter, ErrorHandling, Logging } from '../core/interfaces';
|
||||
import type { Emitter } from '../core/interfaces';
|
||||
import { PayloadType, SernError } from '../core/structures/enums'
|
||||
import { Err, Ok, Result } from 'ts-results-es';
|
||||
import type { Awaitable, UnpackedDependencies, VoidResult } from '../types/utility';
|
||||
import type { ControlPlugin } from '../types/core-plugin';
|
||||
import type { UnpackedDependencies, VoidResult } from '../types/utility';
|
||||
import type { CommandModule, Module, Processed } from '../types/core-modules';
|
||||
import * as assert from 'node:assert';
|
||||
import { Context } from '../core/structures/context';
|
||||
@@ -26,59 +27,75 @@ import { disposeAll } from '../core/ioc/base';
|
||||
import { arrayifySource, handleError } from '../core/operators';
|
||||
import { resultPayload, isAutocomplete, treeSearch } from '../core/functions'
|
||||
|
||||
function intoPayload(module: Module) {
|
||||
return pipe(map(arrayifySource),
|
||||
map(args => ({ module, args })));
|
||||
interface ExecutePayload {
|
||||
module: Module;
|
||||
args: unknown[];
|
||||
deps: Dependencies
|
||||
[key: string]: unknown
|
||||
}
|
||||
const createResult = createResultResolver<
|
||||
Processed<Module>,
|
||||
{ module: Processed<Module>; args: unknown[] },
|
||||
unknown[]
|
||||
>({ onNext: (p) => p.args, });
|
||||
|
||||
function intoPayload(module: Module, deps: Dependencies) {
|
||||
return pipe(map(arrayifySource),
|
||||
map(args => ({ module, args, deps })));
|
||||
}
|
||||
const createResult = (deps: Dependencies) =>
|
||||
createResultResolver<unknown[]>({
|
||||
onNext: (p) => p.args,
|
||||
onStop: (module) => {
|
||||
//maybe do something when plugins fail?
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Creates an observable from { source }
|
||||
* @param module
|
||||
* @param source
|
||||
*/
|
||||
export function eventDispatcher(module: Module, source: unknown) {
|
||||
export function eventDispatcher(deps: Dependencies, module: Module, source: unknown) {
|
||||
assert.ok(source && typeof source === 'object',
|
||||
`${source} cannot be constructed into an event listener`);
|
||||
const execute: OperatorFunction<unknown[], unknown> =
|
||||
concatMap(async args => module.execute(...args));
|
||||
const execute: OperatorFunction<unknown[]|undefined, unknown> =
|
||||
concatMap(async args => {
|
||||
if(args)
|
||||
return module.execute.apply(null, args);
|
||||
});
|
||||
//@ts-ignore
|
||||
return fromEvent(source, module.name!)
|
||||
//@ts-ignore
|
||||
.pipe(intoPayload(module),
|
||||
concatMap(createResult),
|
||||
.pipe(intoPayload(module, deps),
|
||||
concatMap(createResult(deps)),
|
||||
execute);
|
||||
}
|
||||
|
||||
interface DispatchPayload {
|
||||
module: Processed<CommandModule>;
|
||||
event: BaseInteraction;
|
||||
defaultPrefix?: string
|
||||
defaultPrefix?: string;
|
||||
deps: Dependencies
|
||||
};
|
||||
export function createDispatcher({ module, event, defaultPrefix }: DispatchPayload) {
|
||||
export function createDispatcher({ module, event, defaultPrefix, deps }: DispatchPayload): ExecutePayload {
|
||||
assert.ok(CommandType.Text !== module.type,
|
||||
SernError.MismatchEvent + 'Found text command in interaction stream');
|
||||
|
||||
if(isAutocomplete(event)) {
|
||||
assert.ok(module.type === CommandType.Slash
|
||||
|| module.type === CommandType.Both);
|
||||
|| module.type === CommandType.Both, "Autocomplete option on non command interaction");
|
||||
const option = treeSearch(event, module.options);
|
||||
assert.ok(option, SernError.NotSupportedInteraction + ` There is no autocomplete tag for ` + inspect(module));
|
||||
const { command } = option;
|
||||
return { module: command as Processed<Module>, //autocomplete is not a true "module" warning cast!
|
||||
args: [event] };
|
||||
args: [event],
|
||||
deps };
|
||||
}
|
||||
|
||||
switch (module.type) {
|
||||
case CommandType.Slash:
|
||||
case CommandType.Both: {
|
||||
return {
|
||||
module,
|
||||
args: [Context.wrap(event, defaultPrefix)]
|
||||
args: [Context.wrap(event, defaultPrefix)],
|
||||
deps
|
||||
};
|
||||
}
|
||||
default: return { module, args: [event] };
|
||||
default: return { module, args: [event], deps };
|
||||
}
|
||||
}
|
||||
function createGenericHandler<Source, Narrowed extends Source, Output>(
|
||||
@@ -114,25 +131,27 @@ export function fmt(msg: string, prefix: string): string[] {
|
||||
*/
|
||||
export function createInteractionHandler<T extends Interaction>(
|
||||
source: Observable<Interaction>,
|
||||
mg: Map<string, Module>,
|
||||
deps: Dependencies,
|
||||
defaultPrefix?: string
|
||||
) {
|
||||
const mg = deps['@sern/modules']
|
||||
return createGenericHandler<Interaction, T, Result<ReturnType<typeof createDispatcher>, void>>(
|
||||
source,
|
||||
async event => {
|
||||
const possibleIds = Id.reconstruct(event);
|
||||
let fullPaths= possibleIds
|
||||
let modules = possibleIds
|
||||
.map(id => mg.get(id))
|
||||
.filter((id): id is Module => id !== undefined);
|
||||
|
||||
if(fullPaths.length == 0) {
|
||||
if(modules.length == 0) {
|
||||
return Err.EMPTY;
|
||||
}
|
||||
const [ path ] = fullPaths;
|
||||
const [ module ] = modules;
|
||||
return Ok(createDispatcher({
|
||||
module: path as Processed<CommandModule>,
|
||||
module: module as Processed<CommandModule>,
|
||||
event,
|
||||
defaultPrefix
|
||||
defaultPrefix,
|
||||
deps
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -140,24 +159,20 @@ export function createInteractionHandler<T extends Interaction>(
|
||||
export function createMessageHandler(
|
||||
source: Observable<Message>,
|
||||
defaultPrefix: string,
|
||||
mg: Map<string, Module>,
|
||||
deps: Dependencies
|
||||
) {
|
||||
const mg = deps['@sern/modules'];
|
||||
return createGenericHandler(source, async event => {
|
||||
const [prefix] = fmt(event.content, defaultPrefix);
|
||||
let module= mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`) as Module;
|
||||
if(!module) {
|
||||
return Err('Possibly undefined behavior: could not find a static id to resolve');
|
||||
}
|
||||
return Ok({ args: [Context.wrap(event, defaultPrefix)], module })
|
||||
return Ok({ args: [Context.wrap(event, defaultPrefix)], module, deps })
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface ExecutePayload {
|
||||
module: Processed<Module>;
|
||||
task: () => Awaitable<unknown>;
|
||||
}
|
||||
/**
|
||||
* Wraps the task in a Result as a try / catch.
|
||||
* if the task is ok, an event is emitted and the stream becomes empty
|
||||
@@ -169,20 +184,16 @@ interface ExecutePayload {
|
||||
*/
|
||||
export function executeModule(
|
||||
emitter: Emitter,
|
||||
logger: Logging|undefined,
|
||||
errHandler: ErrorHandling,
|
||||
{ module, task }: ExecutePayload,
|
||||
{ module, args }: ExecutePayload,
|
||||
) {
|
||||
return of(module).pipe(
|
||||
//converting the task into a promise so rxjs can resolve the Awaitable properly
|
||||
concatMap(() => Result.wrapAsync(async () => task())),
|
||||
concatMap(result => {
|
||||
return from(Result.wrapAsync(async () => module.execute(...args)))
|
||||
.pipe(concatMap(result => {
|
||||
if (result.isOk()) {
|
||||
emitter.emit('module.activate', resultPayload(PayloadType.Success, module));
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
||||
return throwError(() => resultPayload(PayloadType.Failure, module, result.error));
|
||||
}));
|
||||
}))
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -194,29 +205,25 @@ export function executeModule(
|
||||
* @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, map: Record<string, unknown>) => Output;
|
||||
export function createResultResolver<Output>(config: {
|
||||
onStop?: (module: Module) => unknown;
|
||||
onNext: (args: ExecutePayload, map: Record<string, unknown>) => Output;
|
||||
}) {
|
||||
return async (payload: Args) => {
|
||||
//@ts-ignore fix later
|
||||
const { onStop, onNext } = config;
|
||||
return async (payload: ExecutePayload) => {
|
||||
const task = await callPlugins(payload);
|
||||
if(task.isOk()) {
|
||||
return config.onNext(payload, task.value) as ExecutePayload;
|
||||
return onNext(payload, task.value) as Output;
|
||||
} else {
|
||||
config.onStop?.(payload.module);
|
||||
onStop?.(payload.module);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
async function callPlugins({ args, module }: { args: unknown[], module: Module }) {
|
||||
async function callPlugins({ args, module, deps }: ExecutePayload) {
|
||||
let state = {};
|
||||
for(const plugin of module.onEvent) {
|
||||
const result = await plugin.execute.apply(null, arrayifySource(args));
|
||||
const result = await plugin.execute(...args, { state, deps });
|
||||
if(result.isErr()) {
|
||||
return result;
|
||||
}
|
||||
@@ -230,11 +237,11 @@ async function callPlugins({ args, module }: { args: unknown[], module: Module }
|
||||
* 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 Module, Args extends { module: M; args: unknown[]; }>
|
||||
(onStop: (m: M) => unknown) {
|
||||
const onNext = ({ args, module }: Args, state: Record<string, unknown>) => ({
|
||||
task: () => module.execute(...args, state),
|
||||
export function intoTask(onStop: (m: Module) => unknown) {
|
||||
const onNext = ({ args, module, deps }: ExecutePayload, state: Record<string, unknown>) => ({
|
||||
module,
|
||||
args: [...args, { state }],
|
||||
deps
|
||||
});
|
||||
return createResultResolver({ onStop, onNext })
|
||||
}
|
||||
@@ -243,8 +250,6 @@ export const handleCrash =
|
||||
({ "@sern/errors": err, '@sern/emitter': sem, '@sern/logger': log } : UnpackedDependencies) =>
|
||||
pipe(catchError(handleError(err, sem, log)),
|
||||
finalize(() => {
|
||||
log?.info({
|
||||
message: 'A stream closed or reached end of lifetime',
|
||||
});
|
||||
log?.info({ message: 'A stream closed or reached end of lifetime' });
|
||||
disposeAll(log);
|
||||
}))
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Interaction } from 'discord.js';
|
||||
import { mergeMap, merge, concatMap, EMPTY } from 'rxjs';
|
||||
import { PayloadType } from '../core/structures/enums';
|
||||
import { filterTap, sharedEventStream } from '../core/operators'
|
||||
import { createInteractionHandler, executeModule, makeModuleExecutor } from './event-utils';
|
||||
import { createInteractionHandler, executeModule, intoTask, } from './event-utils';
|
||||
import { SernError } from '../core/structures/enums'
|
||||
import { isAutocomplete, isCommand, isMessageComponent, isModal, resultPayload, } from '../core/functions'
|
||||
import { UnpackedDependencies } from '../types/utility';
|
||||
@@ -10,13 +10,10 @@ import { Emitter } from '../core/interfaces';
|
||||
|
||||
export default function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: string) {
|
||||
//i wish javascript had clojure destructuring
|
||||
const { '@sern/modules': modules,
|
||||
'@sern/client': client,
|
||||
'@sern/logger': log,
|
||||
'@sern/errors': err,
|
||||
const { '@sern/client': client,
|
||||
'@sern/emitter': emitter } = deps
|
||||
const interactionStream$ = sharedEventStream<Interaction>(client as unknown as Emitter, 'interactionCreate');
|
||||
const handle = createInteractionHandler(interactionStream$, modules, defaultPrefix);
|
||||
const handle = createInteractionHandler(interactionStream$, deps, defaultPrefix);
|
||||
|
||||
const interactionHandler$ = merge(handle(isMessageComponent),
|
||||
handle(isAutocomplete),
|
||||
@@ -24,11 +21,12 @@ export default function interactionHandler(deps: UnpackedDependencies, defaultPr
|
||||
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)))),
|
||||
concatMap(intoTask(module => {
|
||||
emitter.emit('module.activate', resultPayload(PayloadType.Failure, module, SernError.PluginFailure))
|
||||
})),
|
||||
mergeMap(payload => {
|
||||
if(payload)
|
||||
return executeModule(emitter, log, err, payload)
|
||||
return executeModule(emitter, payload)
|
||||
return EMPTY;
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EMPTY, mergeMap, concatMap } from 'rxjs';
|
||||
import type { Message } from 'discord.js';
|
||||
import { createMessageHandler, executeModule, makeModuleExecutor } from './event-utils';
|
||||
import { createMessageHandler, executeModule, intoTask } from './event-utils';
|
||||
import { PayloadType, SernError } from '../core/structures/enums'
|
||||
import { resultPayload } from '../core/functions'
|
||||
import { filterTap, sharedEventStream } from '../core/operators'
|
||||
@@ -21,29 +21,31 @@ function hasPrefix(prefix: string, content: string) {
|
||||
}
|
||||
|
||||
export default function message(
|
||||
{"@sern/emitter": emitter, '@sern/errors':err,
|
||||
'@sern/logger': log, '@sern/client': client,
|
||||
'@sern/modules': commands}: UnpackedDependencies,
|
||||
deps: UnpackedDependencies,
|
||||
defaultPrefix: string | undefined
|
||||
) {
|
||||
const {"@sern/emitter": emitter,
|
||||
'@sern/logger': log,
|
||||
'@sern/client': client } = deps
|
||||
|
||||
if (!defaultPrefix) {
|
||||
log?.debug({ message: 'No prefix found. message handler shutting down' });
|
||||
return EMPTY;
|
||||
}
|
||||
const messageStream$ = sharedEventStream<Message>(client as unknown as Emitter, 'messageCreate');
|
||||
const handle = createMessageHandler(messageStream$, defaultPrefix, commands);
|
||||
const handle = createMessageHandler(messageStream$, defaultPrefix, deps);
|
||||
|
||||
const msgCommands$ = handle(isNonBot(defaultPrefix));
|
||||
|
||||
return msgCommands$.pipe(
|
||||
filterTap((e) => emitter.emit('warning', resultPayload(PayloadType.Warning, undefined, e))),
|
||||
concatMap(makeModuleExecutor(module => {
|
||||
concatMap(intoTask(module => {
|
||||
const result = resultPayload(PayloadType.Failure, module, SernError.PluginFailure);
|
||||
emitter.emit('module.activate', result);
|
||||
})),
|
||||
mergeMap(payload => {
|
||||
if(payload)
|
||||
return executeModule(emitter, log, err, payload)
|
||||
return executeModule(emitter, payload)
|
||||
return EMPTY;
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -10,19 +10,18 @@ const intoDispatcher = (deps: UnpackedDependencies) =>
|
||||
(module : EventModule) => {
|
||||
switch (module.type) {
|
||||
case EventType.Sern:
|
||||
return eventDispatcher(module, deps['@sern/emitter']);
|
||||
return eventDispatcher(deps, module, deps['@sern/emitter']);
|
||||
case EventType.Discord:
|
||||
return eventDispatcher(module, deps['@sern/client']);
|
||||
return eventDispatcher(deps, module, deps['@sern/client']);
|
||||
case EventType.External:
|
||||
return eventDispatcher(module, deps[module.emitter]);
|
||||
return eventDispatcher(deps, module, deps[module.emitter]);
|
||||
case EventType.Cron: {
|
||||
//@ts-ignore
|
||||
const cron = deps['@sern/cron'];
|
||||
cron.addCronModule(module);
|
||||
return eventDispatcher(module, cron);
|
||||
return eventDispatcher(deps, module, cron);
|
||||
}
|
||||
default:
|
||||
throw Error(SernError.InvalidModuleType + ' while creating event handler');
|
||||
default: throw Error(SernError.InvalidModuleType + ' while creating event handler');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -33,7 +32,7 @@ export default async function(deps: UnpackedDependencies, eventDir: string) {
|
||||
for(const plugin of module.plugins) {
|
||||
const res = await plugin.execute({
|
||||
module,
|
||||
absPath: module.meta.absPath ,
|
||||
absPath: module.meta.absPath,
|
||||
updateModule: (partial: Partial<Module>) => {
|
||||
module = { ...module, ...partial };
|
||||
return module;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { presenceHandler } from './handlers/presence';
|
||||
import { handleCrash } from './handlers/event-utils';
|
||||
import { useContainerRaw } from './core/ioc/global';
|
||||
import { UnpackedDependencies } from './types/utility';
|
||||
import type { PresenceResult } from './core/presences';
|
||||
|
||||
interface Wrapper {
|
||||
commands: string;
|
||||
@@ -49,7 +50,7 @@ export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) {
|
||||
const time = ((performance.now() - startTime) / 1000).toFixed(2);
|
||||
deps['@sern/logger']?.info({ message: `sern: registered in ${time} s` });
|
||||
if(presencePath.exists) {
|
||||
const setPresence = async (p: any) => {
|
||||
const setPresence = async (p: PresenceResult) => {
|
||||
return deps['@sern/client'].user?.setPresence(p);
|
||||
}
|
||||
presenceHandler(presencePath.path, setPresence).subscribe();
|
||||
|
||||
Reference in New Issue
Block a user