mirror of
https://github.com/sern-handler/handler
synced 2026-06-06 01:16:55 +00:00
plugin data reduction & args changes
This commit is contained in:
@@ -57,7 +57,6 @@ export async function importModule<T>(absPath: string) {
|
||||
|
||||
export async function* readRecursive(dir: string): AsyncGenerator<string> {
|
||||
const files = await readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const file of files) {
|
||||
const fullPath = path.posix.join(dir, file.name);
|
||||
if (file.isDirectory()) {
|
||||
@@ -65,7 +64,7 @@ export async function* readRecursive(dir: string): AsyncGenerator<string> {
|
||||
yield* readRecursive(fullPath);
|
||||
}
|
||||
} else if (!file.name.startsWith('!')) {
|
||||
yield fullPath;
|
||||
yield "file:///"+path.resolve(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,21 +5,17 @@
|
||||
*/
|
||||
import {
|
||||
concatMap,
|
||||
defaultIfEmpty,
|
||||
EMPTY,
|
||||
every,
|
||||
fromEvent,
|
||||
Observable,
|
||||
of,
|
||||
OperatorFunction,
|
||||
pipe,
|
||||
share,
|
||||
} from 'rxjs';
|
||||
import type { Emitter, ErrorHandling, Logging } from './interfaces';
|
||||
import util from 'node:util';
|
||||
import type { PluginResult } from '../types/core-plugin';
|
||||
import { Result } from 'ts-results-es';
|
||||
import { VoidResult } from '../types/utility';
|
||||
import type { Result } from 'ts-results-es';
|
||||
|
||||
/**
|
||||
* if {src} is true, mapTo V, else ignore
|
||||
* @param item
|
||||
@@ -28,10 +24,6 @@ export function filterMapTo<V>(item: () => V): OperatorFunction<boolean, V> {
|
||||
return concatMap(keep => keep ? of(item()) : EMPTY);
|
||||
}
|
||||
|
||||
interface PluginExecutable {
|
||||
execute: (...args: unknown[]) => PluginResult;
|
||||
};
|
||||
|
||||
|
||||
export const arrayifySource = <T>(src: T) =>
|
||||
Array.isArray(src) ? src : [src];
|
||||
|
||||
@@ -13,6 +13,10 @@ import { Result, Ok, Err } from 'ts-results-es';
|
||||
import * as assert from 'assert';
|
||||
import { ReplyOptions } from '../../types/utility';
|
||||
|
||||
function fmt(msg: string, prefix?: string): string[] {
|
||||
if(!prefix) throw Error("Unable to parse message without prefix");
|
||||
return msg.slice(prefix.length).trim().split(/\s+/g);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.0.0
|
||||
@@ -20,14 +24,25 @@ import { ReplyOptions } from '../../types/utility';
|
||||
* Message and ChatInputCommandInteraction
|
||||
*/
|
||||
export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
|
||||
/*
|
||||
* @Experimental
|
||||
*/
|
||||
prefix: string|undefined;
|
||||
|
||||
get options() {
|
||||
return this.interaction.options;
|
||||
}
|
||||
protected constructor(protected ctx: Result<Message, ChatInputCommandInteraction>) {
|
||||
|
||||
args() {
|
||||
return {
|
||||
message: <T = string[]>() => {
|
||||
const [, ...rest] = fmt(this.message.content, this.prefix);
|
||||
return rest as T;
|
||||
},
|
||||
interaction: () => this.interaction.options
|
||||
}
|
||||
}
|
||||
|
||||
protected constructor(protected ctx: Result<Message, ChatInputCommandInteraction>, prefix?: string) {
|
||||
super(ctx);
|
||||
this.prefix = prefix
|
||||
}
|
||||
|
||||
public get id(): Snowflake {
|
||||
@@ -109,12 +124,12 @@ export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
|
||||
);
|
||||
}
|
||||
|
||||
static override wrap(wrappable: BaseInteraction | Message): Context {
|
||||
static override wrap(wrappable: BaseInteraction | Message, prefix?: string): Context {
|
||||
if ('interaction' in wrappable) {
|
||||
return new Context(Ok(wrappable));
|
||||
return new Context(Ok(wrappable), prefix);
|
||||
}
|
||||
assert.ok(wrappable.isChatInputCommand(), "Context created with bad interaction.");
|
||||
return new Context(Err(wrappable));
|
||||
return new Context(Err(wrappable), prefix);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { LogPayload, Logging, ErrorHandling, Emitter } from '../interfaces';
|
||||
import { AnyFunction, UnpackedDependencies } from '../../types/utility';
|
||||
import cron from 'node-cron'
|
||||
import { EventEmitter } from 'events';
|
||||
import type { CronEventCommand, Module } from '../../types/core-modules'
|
||||
import { EventType } from './enums';
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
fromEvent, map, OperatorFunction,
|
||||
catchError,
|
||||
finalize,
|
||||
pipe
|
||||
pipe,
|
||||
} from 'rxjs';
|
||||
import * as Id from '../core/id'
|
||||
import type { Emitter, ErrorHandling, Logging } from '../core/interfaces';
|
||||
@@ -21,16 +21,14 @@ import type { CommandModule, Module, Processed } from '../types/core-modules';
|
||||
import * as assert from 'node:assert';
|
||||
import { Context } from '../core/structures/context';
|
||||
import { CommandType } from '../core/structures/enums'
|
||||
import type { Args } from '../types/utility';
|
||||
import { inspect } from 'node:util'
|
||||
import { disposeAll } from '../core/ioc/base';
|
||||
import { arrayifySource, handleError } from '../core/operators';
|
||||
import { resultPayload, isAutocomplete, treeSearch } from '../core/functions'
|
||||
|
||||
function contextArgs(wrappable: Message | BaseInteraction, messageArgs?: string[]) {
|
||||
const ctx = Context.wrap(wrappable);
|
||||
const args = ctx.isMessage() ? ['text', messageArgs!] : ['slash', ctx.options];
|
||||
return [ctx, args] as [Context, Args];
|
||||
function contextArgs(wrappable: Message | BaseInteraction, prefix?: string) {
|
||||
const ctx = Context.wrap(wrappable, prefix);
|
||||
return [ctx] as [Context];
|
||||
}
|
||||
|
||||
function intoPayload(module: Module) {
|
||||
@@ -44,7 +42,7 @@ const createResult = createResultResolver<
|
||||
>({
|
||||
//@ts-ignore fix later
|
||||
callPlugins,
|
||||
onNext: ({ args }) => args,
|
||||
onNext: (p) => p.args,
|
||||
});
|
||||
/**
|
||||
* Creates an observable from { source }
|
||||
@@ -52,19 +50,21 @@ const createResult = createResultResolver<
|
||||
* @param source
|
||||
*/
|
||||
export function eventDispatcher(module: Module, source: unknown) {
|
||||
assert.ok(source && typeof source === 'object', `${source} cannot be constructed into an event listener`);
|
||||
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));
|
||||
//@ts-ignore
|
||||
return fromEvent(source, module.name!)
|
||||
|
||||
//@ts-ignore
|
||||
.pipe(intoPayload(module),
|
||||
concatMap(createResult),
|
||||
execute);
|
||||
}
|
||||
|
||||
export function createDispatcher({ module, event }: { module: Processed<CommandModule>; event: BaseInteraction; }) {
|
||||
export function createDispatcher(
|
||||
{ module, event }: { module: Processed<CommandModule>; event: BaseInteraction; }
|
||||
) {
|
||||
assert.ok(CommandType.Text !== module.type,
|
||||
SernError.MismatchEvent + 'Found text command in interaction stream');
|
||||
if(isAutocomplete(event)) {
|
||||
@@ -118,6 +118,7 @@ export function fmt(msg: string, prefix: string): string[] {
|
||||
export function createInteractionHandler<T extends Interaction>(
|
||||
source: Observable<Interaction>,
|
||||
mg: Map<string, Module>,
|
||||
defaultPrefix?: string
|
||||
) {
|
||||
return createGenericHandler<Interaction, T, Result<ReturnType<typeof createDispatcher>, void>>(
|
||||
source,
|
||||
@@ -141,12 +142,13 @@ export function createMessageHandler(
|
||||
mg: any,
|
||||
) {
|
||||
return createGenericHandler(source, async event => {
|
||||
const [prefix, ...rest] = fmt(event.content, defaultPrefix);
|
||||
let fullPath = mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`);
|
||||
if(!fullPath) {
|
||||
const [prefix] = fmt(event.content, defaultPrefix);
|
||||
console.log(prefix)
|
||||
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: contextArgs(event, rest), module: fullPath as Processed<CommandModule> })
|
||||
return Ok({ args: [Context.wrap(event, defaultPrefix)], module })
|
||||
});
|
||||
}
|
||||
|
||||
@@ -215,9 +217,9 @@ export function createResultResolver<
|
||||
async function callPlugins({ args, module }: { args: unknown[], module: Module }) {
|
||||
let state = {};
|
||||
for(const plugin of module.onEvent) {
|
||||
const result = await plugin.execute.apply(null, !Array.isArray(args) ? args : args);
|
||||
const result = await plugin.execute.apply(null, arrayifySource(args));
|
||||
if(result.isErr()) {
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
if(typeof result.value === 'object') {
|
||||
//@ts-ignore TODO
|
||||
@@ -240,13 +242,12 @@ export function makeModuleExecutor< M extends Processed<Module>, Args extends {
|
||||
return createResultResolver({ onStop, onNext })
|
||||
}
|
||||
|
||||
export const handleCrash = ({ "@sern/errors": err,
|
||||
'@sern/emitter': sem,
|
||||
'@sern/logger': log } : UnpackedDependencies) =>
|
||||
export const handleCrash =
|
||||
({ "@sern/errors": err, '@sern/emitter': sem, '@sern/logger': log } : UnpackedDependencies) =>
|
||||
pipe(catchError(handleError(err, sem, log)),
|
||||
finalize(() => {
|
||||
finalize(() => {
|
||||
log?.info({
|
||||
message: 'A stream closed or reached end of lifetime',
|
||||
});
|
||||
disposeAll(log);
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -8,7 +8,7 @@ import { isAutocomplete, isCommand, isMessageComponent, isModal, resultPayload,
|
||||
import { UnpackedDependencies } from '../types/utility';
|
||||
import { Emitter } from '../core/interfaces';
|
||||
|
||||
export default function interactionHandler(deps: UnpackedDependencies) {
|
||||
export default function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: string) {
|
||||
//i wish javascript had clojure destructuring
|
||||
const { '@sern/modules': modules,
|
||||
'@sern/client': client,
|
||||
@@ -16,7 +16,7 @@ export default function interactionHandler(deps: UnpackedDependencies) {
|
||||
'@sern/errors': err,
|
||||
'@sern/emitter': emitter } = deps
|
||||
const interactionStream$ = sharedEventStream<Interaction>(client as unknown as Emitter, 'interactionCreate');
|
||||
const handle = createInteractionHandler(interactionStream$, modules);
|
||||
const handle = createInteractionHandler(interactionStream$, modules, defaultPrefix);
|
||||
|
||||
const interactionHandler$ = merge(handle(isMessageComponent),
|
||||
handle(isAutocomplete),
|
||||
|
||||
@@ -24,7 +24,8 @@ export default function message(
|
||||
{"@sern/emitter": emitter, '@sern/errors':err,
|
||||
'@sern/logger': log, '@sern/client': client,
|
||||
'@sern/modules': commands}: UnpackedDependencies,
|
||||
defaultPrefix: string | undefined) {
|
||||
defaultPrefix: string | undefined
|
||||
) {
|
||||
if (!defaultPrefix) {
|
||||
log?.debug({ message: 'No prefix found. message handler shutting down' });
|
||||
return EMPTY;
|
||||
@@ -42,7 +43,7 @@ export default function message(
|
||||
})),
|
||||
mergeMap(payload => {
|
||||
if(payload)
|
||||
executeModule(emitter, log, err, payload)
|
||||
return executeModule(emitter, log, err, payload)
|
||||
return EMPTY;
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) {
|
||||
.catch(err => { throw err });
|
||||
|
||||
const messages$ = messageHandler(deps, maybeWrapper.defaultPrefix);
|
||||
const interactions$ = interactionHandler(deps);
|
||||
const interactions$ = interactionHandler(deps, maybeWrapper.defaultPrefix);
|
||||
// listening to the message stream and interaction stream
|
||||
merge(messages$, interactions$).pipe(handleCrash(deps)).subscribe();
|
||||
}
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
import { beforeEach, describe, expect, vi, it } from 'vitest';
|
||||
import { eventDispatcher } from '../../src/handlers/event-utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { Module } from '../../src/types/core-modules';
|
||||
import { Processed } from '../../src/types/core-modules';
|
||||
import { CommandType } from '../../src/core/structures/enums';
|
||||
import { EventEmitter } from 'events';
|
||||
import { EventType } from '../../dist/core/structures/enums';
|
||||
|
||||
function createRandomModule(): Processed<Module> {
|
||||
return {
|
||||
type: faker.number.int({
|
||||
min: EventType.Discord,
|
||||
max: EventType.Cron,
|
||||
}),
|
||||
meta: { id:"", absPath: faker.system.directoryPath() },
|
||||
type: EventType.Discord,
|
||||
meta: { id:"", absPath: "" },
|
||||
description: faker.string.alpha(),
|
||||
name: faker.string.alpha(),
|
||||
name: "abc",
|
||||
onEvent: [],
|
||||
plugins: [],
|
||||
execute: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
const testScheduler = new TestScheduler((actual, expected) => {
|
||||
// asserting the two objects are equal - required
|
||||
// for TestScheduler assertions to work via your test framework
|
||||
// e.g. using chai.
|
||||
expect(actual).deep.equal(expected);
|
||||
});
|
||||
|
||||
describe('eventDispatcher standard', () => {
|
||||
let m: Processed<Module>;
|
||||
let ee: EventEmitter;
|
||||
@@ -33,15 +37,17 @@ describe('eventDispatcher standard', () => {
|
||||
it('should throw', () => {
|
||||
expect(() => eventDispatcher(m, 'not event emitter')).toThrowError();
|
||||
});
|
||||
|
||||
it("Shouldn't throw", () => {
|
||||
expect(() => eventDispatcher(m, ee)).not.toThrowError();
|
||||
});
|
||||
|
||||
it('Should be called once', () => {
|
||||
const s = eventDispatcher(m, ee);
|
||||
s.subscribe();
|
||||
ee.emit(m.name, faker.string.alpha());
|
||||
|
||||
expect(m.execute).toHaveBeenCalledOnce();
|
||||
});
|
||||
//TODO
|
||||
// it('Should be called once', () => {
|
||||
// const s = eventDispatcher(m, ee);
|
||||
// console.log(m)
|
||||
// s.subscribe();
|
||||
// ee.emit(m.name);
|
||||
// console.log(m.execute)
|
||||
// expect(m.execute).toHaveBeenCalledOnce();
|
||||
// });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user