mirror of
https://github.com/sern-handler/handler
synced 2026-06-06 01:16:55 +00:00
firstcommit
This commit is contained in:
2658
package-lock.json
generated
Normal file
2658
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,7 @@
|
||||
"callsites": "^3.1.0",
|
||||
"cron": "^3.1.7",
|
||||
"deepmerge": "^4.3.1",
|
||||
"rxjs": "^7.8.0"
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.0.1",
|
||||
|
||||
@@ -11,7 +11,22 @@ import type {
|
||||
import { ApplicationCommandOptionType, InteractionType } from 'discord.js';
|
||||
import { PluginType } from './structures/enums';
|
||||
import assert from 'assert';
|
||||
import type { Payload } from '../types/utility';
|
||||
import type { Payload, UnpackedDependencies } from '../types/utility';
|
||||
|
||||
export const createSDT = (module: Module, deps: UnpackedDependencies, params: string|undefined) => {
|
||||
return {
|
||||
state: {},
|
||||
deps,
|
||||
params,
|
||||
type: module.type,
|
||||
module: {
|
||||
name: module.name,
|
||||
description: module.description,
|
||||
locals: module.locals,
|
||||
meta: module.meta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the first character(s) _[depending on prefix length]_ of the message
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { Interaction, Message, BaseInteraction } from 'discord.js';
|
||||
// @ts-nocheck
|
||||
|
||||
import type { Message, BaseInteraction } from 'discord.js';
|
||||
import util from 'node:util';
|
||||
import {
|
||||
EMPTY, type Observable, concatMap, filter,
|
||||
@@ -8,7 +10,7 @@ import {
|
||||
import * as Id from '../core/id'
|
||||
import type { Emitter, ErrorHandling, Logging } from '../core/interfaces';
|
||||
import { SernError } from '../core/structures/enums'
|
||||
import { EMPTY_ERR, Err, Ok, Result, wrapAsync } from '../core/structures/result';
|
||||
import { Err, Ok, Result, wrapAsync } from '../core/structures/result';
|
||||
import type { UnpackedDependencies } from '../types/utility';
|
||||
import type { CommandModule, Module, Processed } from '../types/core-modules';
|
||||
import * as assert from 'node:assert';
|
||||
@@ -37,8 +39,6 @@ const arrayify= <T>(src: T) =>
|
||||
interface ExecutePayload {
|
||||
module: Module;
|
||||
args: unknown[];
|
||||
deps: Dependencies;
|
||||
params?: string;
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
@@ -121,38 +121,6 @@ function createGenericHandler<Source, Narrowed extends Source, Output>(
|
||||
concatMap(makeModule)); // create a payload, preparing to execute
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Creates an RxJS observable that filters and maps incoming interactions to their respective modules.
|
||||
* @param i An RxJS observable of interactions.
|
||||
* @param mg The module manager instance used to retrieve the module path for each interaction.
|
||||
* @returns A handler to create a RxJS observable of dispatchers that take incoming interactions and execute their corresponding modules.
|
||||
*/
|
||||
export function createInteractionHandler<T extends Interaction>(
|
||||
source: Observable<Interaction>,
|
||||
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 modules = possibleIds
|
||||
.map(({ id, params }) => ({ module: mg.get(id), params }))
|
||||
.filter(({ module }) => module !== undefined);
|
||||
if(modules.length == 0) {
|
||||
return EMPTY_ERR;
|
||||
}
|
||||
const [{module, params}] = modules;
|
||||
return Ok(createDispatcher({
|
||||
module: module as Processed<CommandModule>,
|
||||
event, defaultPrefix, deps, params
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
export function createMessageHandler(
|
||||
source: Observable<Message>,
|
||||
defaultPrefix: string,
|
||||
@@ -238,20 +206,10 @@ export async function callInitPlugins(_module: Module, deps: Dependencies, emit?
|
||||
return module
|
||||
}
|
||||
|
||||
export async function callPlugins({ args, module, deps, params }: ExecutePayload) {
|
||||
export async function callPlugins({ args, module }: ExecutePayload) {
|
||||
let state = {};
|
||||
for(const plugin of module.onEvent??[]) {
|
||||
const executionContext = {
|
||||
state,
|
||||
deps,
|
||||
params,
|
||||
type: module.type,
|
||||
module: { name: module.name,
|
||||
description: module.description,
|
||||
locals: module.locals,
|
||||
meta: module.meta }
|
||||
};
|
||||
const result = await plugin.execute(...args, executionContext);
|
||||
const result = await plugin.execute(...args);
|
||||
if(!result.ok) {
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,61 @@
|
||||
import type { Interaction } from 'discord.js';
|
||||
import { mergeMap, merge, concatMap, EMPTY } from 'rxjs';
|
||||
import { createInteractionHandler, executeModule, intoTask, sharedEventStream, filterTap, handleCrash } from './event-utils';
|
||||
import type { Module } from '../types/core-modules'
|
||||
import { callPlugins } from './event-utils';
|
||||
import { SernError } from '../core/structures/enums'
|
||||
import { isAutocomplete, isCommand, isMessageComponent, isModal, resultPayload } from '../core/functions'
|
||||
import { createSDT, isAutocomplete, isCommand, isMessageComponent, isModal, treeSearch } from '../core/functions'
|
||||
import { UnpackedDependencies } from '../types/utility';
|
||||
import * as Id from '../core/id'
|
||||
import { Context } from '../core/structures/context';
|
||||
|
||||
export default function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: string) {
|
||||
|
||||
|
||||
export function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: string) {
|
||||
//i wish javascript had clojure destructuring
|
||||
const { '@sern/client': client,
|
||||
'@sern/modules': moduleManager,
|
||||
'@sern/emitter': emitter } = deps
|
||||
const interactionStream$ = sharedEventStream<Interaction>(client, 'interactionCreate');
|
||||
const handle = createInteractionHandler(interactionStream$, deps, defaultPrefix);
|
||||
|
||||
const interactionHandler$ = merge(handle(isMessageComponent),
|
||||
handle(isAutocomplete),
|
||||
handle(isCommand),
|
||||
handle(isModal));
|
||||
return interactionHandler$
|
||||
.pipe(filterTap(e => emitter.emit('warning', resultPayload('warning', undefined, e))),
|
||||
concatMap(intoTask(module => {
|
||||
emitter.emit('module.activate', resultPayload('failure', module, SernError.PluginFailure))
|
||||
})),
|
||||
mergeMap(payload => {
|
||||
if(payload)
|
||||
return executeModule(emitter, payload)
|
||||
return EMPTY;
|
||||
}),
|
||||
handleCrash(deps, "interaction handling"));
|
||||
client.on('interactionCreate', async (event) => {
|
||||
|
||||
//returns array of possible ids
|
||||
const possibleIds = Id.reconstruct(event);
|
||||
|
||||
let modules = possibleIds
|
||||
.map(({ id, params }) => ({ module: moduleManager.get(id)!, params }))
|
||||
.filter(({ module }) => module !== undefined);
|
||||
|
||||
if(modules.length == 0) {
|
||||
return;
|
||||
}
|
||||
const { module, params } = modules.at(0)!;
|
||||
let payload;
|
||||
if(isAutocomplete(event)) {
|
||||
//@ts-ignore stfu
|
||||
const option = treeSearch(event, module.options);
|
||||
//@ts-ignore stfu
|
||||
const { command } = option;
|
||||
payload= { module: command as Module, //autocomplete is not a true "module" warning cast!
|
||||
args: [event, createSDT(command, deps, params)] };
|
||||
} else if(isCommand(event)) {
|
||||
payload={ module,
|
||||
args: [Context.wrap(event, defaultPrefix), createSDT(module, deps, params)] };
|
||||
} else if (isModal(event) || isMessageComponent(event)) {
|
||||
payload={ module, args: [event, createSDT(module, deps, params)] }
|
||||
} else {
|
||||
throw Error("Invalid event")
|
||||
}
|
||||
const result = await callPlugins(payload)
|
||||
if(!result.ok) {
|
||||
throw Error(result.error ?? SernError.PluginFailure)
|
||||
}
|
||||
if(payload.args.length != 2) {
|
||||
throw Error ('assdfasd')
|
||||
}
|
||||
//@ts-ignore assigning final state from plugin
|
||||
payload.args[1].state = result.value
|
||||
|
||||
|
||||
// will be blocking if long task + await
|
||||
// todo, add to task queue
|
||||
module.execute(...payload.args)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { EMPTY, mergeMap, concatMap } from 'rxjs';
|
||||
import type { Message } from 'discord.js';
|
||||
import { createMessageHandler, executeModule, intoTask, sharedEventStream, filterTap, handleCrash} from './event-utils';
|
||||
import { callPlugins} from './event-utils';
|
||||
import { SernError } from '../core/structures/enums'
|
||||
import { resultPayload } from '../core/functions'
|
||||
import { createSDT, fmt } from '../core/functions'
|
||||
import { UnpackedDependencies } from '../types/utility';
|
||||
import type { Emitter } from '../core/interfaces';
|
||||
|
||||
import type { Module } from '../types/core-modules';
|
||||
import { Context } from '../core/structures/context';
|
||||
/**
|
||||
* Ignores messages from any person / bot except itself
|
||||
* @param prefix
|
||||
*/
|
||||
function isNonBot(prefix: string) {
|
||||
return (msg: Message): msg is Message => !msg.author.bot && hasPrefix(prefix, msg.content);
|
||||
function isBotOrNoPrefix(msg: Message, prefix: string) {
|
||||
return msg.author.bot || !hasPrefix(prefix, msg.content);
|
||||
}
|
||||
|
||||
function hasPrefix(prefix: string, content: string) {
|
||||
@@ -19,32 +18,37 @@ function hasPrefix(prefix: string, content: string) {
|
||||
return prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0;
|
||||
}
|
||||
|
||||
export default
|
||||
function (deps: UnpackedDependencies, defaultPrefix?: string) {
|
||||
export function messageHandler (deps: UnpackedDependencies, defaultPrefix?: string) {
|
||||
const {"@sern/emitter": emitter,
|
||||
'@sern/logger': log,
|
||||
'@sern/modules': mg,
|
||||
'@sern/client': client} = deps
|
||||
|
||||
if (!defaultPrefix) {
|
||||
log?.debug({ message: 'No prefix found. message handler shutting down' });
|
||||
return EMPTY;
|
||||
return;
|
||||
}
|
||||
const messageStream$ = sharedEventStream<Message>(client as unknown as Emitter, 'messageCreate');
|
||||
const handle = createMessageHandler(messageStream$, defaultPrefix, deps);
|
||||
client.on('messageCreate', async message => {
|
||||
if(isBotOrNoPrefix(message, defaultPrefix)) {
|
||||
return
|
||||
}
|
||||
const [prefix] = fmt(message.content, defaultPrefix);
|
||||
let module = mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`) as Module;
|
||||
if(!module) {
|
||||
throw Error('Possibly undefined behavior: could not find a static id to resolve')
|
||||
}
|
||||
const payload = { module, args: [Context.wrap(message, defaultPrefix), createSDT(module, deps, undefined)] }
|
||||
const result = await callPlugins(payload)
|
||||
if (!result.ok) {
|
||||
// const result = resultPayload('failure', module, SernError.PluginFailure);
|
||||
// emitter.emit('module.activate', result);
|
||||
throw Error(result.error ?? SernError.PluginFailure)
|
||||
}
|
||||
//@ts-ignore
|
||||
payload.args[1].state = result.value
|
||||
//todo, add to task queue
|
||||
module.execute(...payload.args)
|
||||
|
||||
const msgCommands$ = handle(isNonBot(defaultPrefix));
|
||||
|
||||
return msgCommands$.pipe(
|
||||
filterTap(e => emitter.emit('warning', resultPayload('warning', undefined, e))),
|
||||
concatMap(intoTask(module => {
|
||||
const result = resultPayload('failure', module, SernError.PluginFailure);
|
||||
emitter.emit('module.activate', result);
|
||||
})),
|
||||
mergeMap(payload => {
|
||||
if(payload)
|
||||
return executeModule(emitter, payload)
|
||||
return EMPTY;
|
||||
}),
|
||||
handleCrash(deps, "message handling")
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -1,40 +1,98 @@
|
||||
import { concatMap, from, interval, of, map, startWith, fromEvent, take, mergeScan } from "rxjs"
|
||||
import { concatMap, map } from "rxjs"
|
||||
import { Presence } from "../core/presences";
|
||||
import { Services } from "../core/ioc";
|
||||
import assert from "node:assert";
|
||||
import * as Files from "../core/module-loading";
|
||||
type SetPresence = (conf: Presence.Result) => Promise<unknown>
|
||||
|
||||
const parseConfig = async (conf: Promise<Presence.Result>) => {
|
||||
return conf.then(s => {
|
||||
if('repeat' in s) {
|
||||
const { onRepeat, repeat } = s;
|
||||
assert(repeat !== undefined, "repeat option is undefined");
|
||||
assert(onRepeat !== undefined, "onRepeat callback is undefined, but repeat exists");
|
||||
const src$ = typeof repeat === 'number'
|
||||
? interval(repeat)
|
||||
: fromEvent(...repeat);
|
||||
return src$.pipe(mergeScan(async (args) => onRepeat(args), s),
|
||||
startWith(s));
|
||||
const parseConfig = async (conf: Promise<Presence.Result>, setPresence: SetPresence) => {
|
||||
const result = await conf;
|
||||
|
||||
if ('repeat' in result) {
|
||||
const { onRepeat, repeat } = result;
|
||||
|
||||
// Validate configuration
|
||||
if (repeat === undefined) {
|
||||
throw new Error("repeat option is undefined");
|
||||
}
|
||||
return of(s).pipe(take(1));
|
||||
})
|
||||
if (onRepeat === undefined) {
|
||||
throw new Error("onRepeat callback is undefined, but repeat exists");
|
||||
}
|
||||
|
||||
// Initial state
|
||||
let currentState = result;
|
||||
const processState = async (state: typeof currentState) => {
|
||||
try {
|
||||
const result = onRepeat(state);
|
||||
// If it's a promise, await it, otherwise use the value directly
|
||||
return result instanceof Promise ? await result : result;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return state; // Return previous state on error
|
||||
}
|
||||
};
|
||||
// Handle numeric interval
|
||||
if (typeof repeat === 'number') {
|
||||
// Return a promise that never resolves (or resolves on cleanup)
|
||||
return new Promise((resolve) => {
|
||||
// Immediately return initial state
|
||||
processState(currentState);
|
||||
|
||||
// Set up interval
|
||||
let isProcessing = false;
|
||||
const intervalId = setInterval(() => {
|
||||
// Skip if previous operation is still running
|
||||
if (isProcessing) return;
|
||||
isProcessing = true;
|
||||
|
||||
processState(currentState)
|
||||
.then(newState => {
|
||||
console.log(newState)
|
||||
currentState = newState;
|
||||
return setPresence(currentState)
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
isProcessing = false;
|
||||
});
|
||||
}, repeat);
|
||||
|
||||
// Optional: Return cleanup function
|
||||
return () => clearInterval(intervalId);
|
||||
});
|
||||
}
|
||||
// Handle event-based repeat
|
||||
else {
|
||||
return new Promise((resolve) => {
|
||||
const [target, eventName] = repeat;
|
||||
|
||||
// Immediately return initial state
|
||||
onRepeat(currentState);
|
||||
|
||||
// Set up event listener
|
||||
const handler = async () => {
|
||||
currentState = await onRepeat(currentState);
|
||||
};
|
||||
|
||||
target.addListener(eventName, handler);
|
||||
|
||||
// Optional: Return cleanup function
|
||||
return () => target.removeListener(eventName, handler);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// No repeat configuration, just return the result
|
||||
return result;
|
||||
};
|
||||
|
||||
export const presenceHandler = (path: string, setPresence: SetPresence) => {
|
||||
const presence = Files
|
||||
.importModule<Presence.Config<(keyof Dependencies)[]>>(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 = async (path: string, setPresence: SetPresence) => {
|
||||
const presence = await
|
||||
Files.importModule<Presence.Config<(keyof Dependencies)[]>>(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);
|
||||
})
|
||||
return parseConfig(presence(), setPresence);
|
||||
|
||||
}
|
||||
|
||||
14
src/sern.ts
14
src/sern.ts
@@ -3,11 +3,10 @@ import { useContainerRaw } from '@sern/ioc/global';
|
||||
|
||||
import callsites from 'callsites';
|
||||
import * as Files from './core/module-loading';
|
||||
import { merge } from 'rxjs';
|
||||
import eventsHandler from './handlers/user-defined-events';
|
||||
import ready from './handlers/ready';
|
||||
import messageHandler from './handlers/message';
|
||||
import interactionHandler from './handlers/interaction';
|
||||
import { interactionHandler } from './handlers/interaction';
|
||||
import { messageHandler } from './handlers/message'
|
||||
import { presenceHandler } from './handlers/presence';
|
||||
import { UnpackedDependencies } from './types/utility';
|
||||
import type { Presence} from './core/presences';
|
||||
@@ -56,7 +55,7 @@ export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) {
|
||||
const setPresence = async (p: Presence.Result) => {
|
||||
return deps['@sern/client'].user?.setPresence(p);
|
||||
}
|
||||
presenceHandler(presencePath.path, setPresence).subscribe();
|
||||
presenceHandler(presencePath.path, setPresence);
|
||||
}
|
||||
if(maybeWrapper.tasks) {
|
||||
registerTasks(maybeWrapper.tasks, deps);
|
||||
@@ -64,8 +63,9 @@ export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) {
|
||||
})
|
||||
.catch(err => { throw err });
|
||||
|
||||
const messages$ = messageHandler(deps, maybeWrapper.defaultPrefix);
|
||||
const interactions$ = interactionHandler(deps, maybeWrapper.defaultPrefix);
|
||||
//const messages$ = messageHandler(deps, maybeWrapper.defaultPrefix);
|
||||
interactionHandler(deps, maybeWrapper.defaultPrefix);
|
||||
messageHandler(deps, maybeWrapper.defaultPrefix)
|
||||
// listening to the message stream and interaction stream
|
||||
merge(messages$, interactions$).subscribe();
|
||||
//merge(messages$, interactions$).subscribe();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user