style: pretty

This commit is contained in:
Jacob Nguyen
2023-05-06 02:04:00 -05:00
parent 8e9b3bbe04
commit 43181bf916
28 changed files with 345 additions and 338 deletions

View File

@@ -1,27 +1,26 @@
import {
import {
ChatInputCommandInteraction,
Client,
InteractionReplyOptions,
Message,
MessageReplyOptions,
Snowflake,
User
} from "discord.js";
import { CoreContext } from "../core/structures/context";
import { Result , Ok , Err } from 'ts-results-es';
import { ReplyOptions } from "../types/handler";
User,
} from 'discord.js';
import { CoreContext } from '../core/structures/context';
import { Result, Ok, Err } from 'ts-results-es';
import { ReplyOptions } from '../types/handler';
/**
* @since 1.0.0
* Provides values shared between
* Message and ChatInputCommandInteraction
*/
export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
get options() {
return this.interaction.options
get options() {
return this.interaction.options;
}
protected constructor(protected ctx: Result<Message, ChatInputCommandInteraction>) {
super(ctx)
super(ctx);
}
public get id(): Snowflake {
@@ -64,7 +63,7 @@ export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
public get inGuild(): boolean {
return this.ctx.val.inGuild();
}
public async reply(content: ReplyOptions) {
return safeUnwrap(
this.ctx
@@ -83,9 +82,6 @@ export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
}
}
function safeUnwrap<T>(res: Result<T, T>) {
return res.val;
}

View File

@@ -1,11 +1,11 @@
import { ClientEvents } from "discord.js";
import { CommandType, EventType, PluginType } from "./core/structures";
import { AnyEventPlugin, Plugin } from "./types/plugin";
import { CommandModule, EventModule, InputCommand, InputEvent } from "./types/module";
import { partition } from "./core/functions";
import { filename, filePath } from "./core/module-loading";
import { Awaitable } from "./types/handler";
export const sernMeta = Symbol('@sern/meta')
import { ClientEvents } from 'discord.js';
import { CommandType, EventType, PluginType } from './core/structures';
import { AnyEventPlugin, Plugin } from './types/plugin';
import { CommandModule, EventModule, InputCommand, InputEvent } from './types/module';
import { partition } from './core/functions';
import { filename, filePath } from './core/module-loading';
import { Awaitable } from './types/handler';
export const sernMeta = Symbol('@sern/meta');
const appBitField = 0b000000011111;
/*
* Generates a number based on CommandType.
@@ -13,11 +13,9 @@ const appBitField = 0b000000011111;
* TextCommands are 0 as they aren't either or.
*/
function apiType(t: CommandType) {
if(t === CommandType.Both || t === CommandType.Modal) return 1;
if (t === CommandType.Both || t === CommandType.Modal) return 1;
const log = Math.log2(t);
return (appBitField & t) !== 0
? log
: log-2;
return (appBitField & t) !== 0 ? log : log - 2;
}
/*
@@ -26,8 +24,8 @@ function apiType(t: CommandType) {
* Then, another number generated by apiType function is appended
*/
function uniqueId(t: CommandType) {
const am = ((appBitField & t) !== 0) ? 'A' : 'C';
return am+apiType(t);
const am = (appBitField & t) !== 0 ? 'A' : 'C';
return am + apiType(t);
}
/**
@@ -39,18 +37,18 @@ export function commandModule(mod: InputCommand): CommandModule {
mod.plugins ?? [],
el => (el as Plugin).type === PluginType.Control,
);
const fullPath = filePath()
const name = mod.name ?? filename(fullPath)
const fullPath = filePath();
const name = mod.name ?? filename(fullPath);
return {
...mod,
description: mod.description ?? "...",
description: mod.description ?? '...',
name,
onEvent,
plugins,
[sernMeta]: {
id: `${name}__${uniqueId(mod.type)}`,
fullPath,
}
},
} as CommandModule;
}
/**
@@ -65,12 +63,12 @@ export function eventModule(mod: InputEvent): EventModule {
);
const fullPath = filePath();
return {
name: mod.name ?? filename(fullPath),
name: mod.name ?? filename(fullPath),
onEvent,
plugins,
[sernMeta]: {
id: 'no-id',
fullPath
fullPath,
},
...mod,
} as EventModule;
@@ -89,7 +87,6 @@ export function discordEvent<T extends keyof ClientEvents>(mod: {
}) {
return eventModule({
type: EventType.Discord,
...mod
...mod,
});
}

View File

@@ -7,7 +7,7 @@ import { importModule } from '../module-loading';
export interface ModuleManager {
get(id: string): string | undefined;
set(id: string, path: string): void;
getPublishableCommands() : Promise<CommandModule[]>
getPublishableCommands(): Promise<CommandModule[]>;
}
/**
* @since 2.0.0
@@ -18,16 +18,16 @@ export class DefaultModuleManager implements ModuleManager {
return this.moduleStore.get(id);
}
set(id: string, path: string): void {
this.moduleStore.set(id, path)
this.moduleStore.set(id, path);
}
//not tested
getPublishableCommands(): Promise<CommandModule[]> {
const entries = this.moduleStore.entries();
const publishable = 0b000000110;
return Promise.all(
Array.from(entries)
.filter(([id,]) => (Number.parseInt(id.at(-1)!) & publishable) !== 0)
.map(([, path]) => importModule<CommandModule>(path)))
const entries = this.moduleStore.entries();
const publishable = 0b000000110;
return Promise.all(
Array.from(entries)
.filter(([id]) => (Number.parseInt(id.at(-1)!) & publishable) !== 0)
.map(([, path]) => importModule<CommandModule>(path)),
);
}
}

View File

@@ -27,7 +27,7 @@ export function single<T>(cb: () => T) {
* use transient if you want a new dependency every time your container getter is called
* @param cb
*/
export function transient<T>(cb: (() => () => T) ) {
export function transient<T>(cb: () => () => T) {
return cb;
}
/**
@@ -36,9 +36,7 @@ export function transient<T>(cb: (() => () => T) ) {
* Finally, update the containerSubject with the new container state
* @param conf
*/
export function composeRoot<T extends AnyDependencies>(
conf: DependencyConfiguration<T>
) {
export function composeRoot<T extends AnyDependencies>(conf: DependencyConfiguration<T>) {
//This should have no client or logger yet.
const currentContainer = containerSubject.getValue();
const excludeLogger = conf.exclude?.has('@sern/logger');
@@ -93,29 +91,29 @@ function defaultContainer() {
>;
}
const requiredDependencyKeys = [
'@sern/emitter',
'@sern/errors',
'@sern/logger',
] as const;
const requiredDependencyKeys = ['@sern/emitter', '@sern/errors', '@sern/logger'] as const;
/**
* A way for sern to grab only the necessary dependencies.
* A way for sern to grab only the necessary dependencies.
* Returns a function which allows for the user to call for more dependencies.
*/
export function makeFetcher<Dep extends AnyDependencies>(containerConfig : Wrapper['containerConfig']) {
return <const Keys extends (keyof Dep)[]>(otherKeys: [...Keys]) =>
containerConfig.get(...requiredDependencyKeys, ...otherKeys as (keyof AnyDependencies)[]) as MapDeps<
Dep,
[...typeof requiredDependencyKeys, ...Keys]
>;
export function makeFetcher<Dep extends AnyDependencies>(
containerConfig: Wrapper['containerConfig'],
) {
return <const Keys extends (keyof Dep)[]>(otherKeys: [...Keys]) =>
containerConfig.get(
...requiredDependencyKeys,
...(otherKeys as (keyof AnyDependencies)[]),
) as MapDeps<Dep, [...typeof requiredDependencyKeys, ...Keys]>;
}
/**
* @since 2.0.0
* @param conf a configuration for creating your project dependencies
*/
export function makeDependencies<const T extends AnyDependencies>(conf: DependencyConfiguration<T>) {
export function makeDependencies<const T extends AnyDependencies>(
conf: DependencyConfiguration<T>,
) {
//Until there are more optional dependencies, just check if the logger exists
composeRoot(conf);
return useContainer<T>();

View File

@@ -2,10 +2,9 @@ import { Err, Ok } from 'ts-results-es';
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
import type { SernAutocompleteData, SernOptionsData } from '../types/module';
//function wrappers for empty ok / err
export const ok = /* @__PURE__*/ () => Ok.EMPTY;
export const err =/* @__PURE__*/ () => Err.EMPTY;
export const err = /* @__PURE__*/ () => Err.EMPTY;
export function partition<T, V>(arr: (T & V)[], condition: (e: T & V) => boolean): [T[], V[]] {
const t: T[] = [];
@@ -20,7 +19,6 @@ export function partition<T, V>(arr: (T & V)[], condition: (e: T & V) => boolean
return [t, v];
}
/**
* Uses an iterative DFS to check if an autocomplete node exists
* @param iAutocomplete

View File

@@ -1,4 +1,4 @@
export * from './contracts';
export * from './plugins';
export * from './structures';
export { single, transient, useContainerRaw, makeDependencies } from './dependencies'
export { single, transient, useContainerRaw, makeDependencies } from './dependencies';

View File

@@ -2,38 +2,35 @@ import { SernError } from './structures/errors';
import { type Result, Err, Ok } from 'ts-results-es';
import { Processed } from '../types/core';
import { Module } from '../types/module';
import * as assert from 'node:assert'
import util from 'node:util'
import * as assert from 'node:assert';
import util from 'node:util';
import { type Observable, from, mergeMap, ObservableInput } from 'rxjs';
import { readdir, stat } from 'fs/promises';
import { basename, join, resolve } from 'path';
export type ModuleResult<T> = Promise<Result<Processed<T>, SernError>>
export type Loader<T> = (absPath: string) => ModuleResult<T>
export type ModuleResult<T> = Promise<Result<Processed<T>, SernError>>;
export type Loader<T> = (absPath: string) => ModuleResult<T>;
export async function importModule<T>(absPath: string) {
/// #if MODE === 'esm'
return (await import(absPath)).default as T
return import(absPath).then(i => i.default as T);
/// #elif MODE === 'cjs'
return require(absPath).default as T; // eslint-disable-line
/// #endif
}
export async function defaultModuleLoader<T extends Module>(
absPath: string,
): ModuleResult<T> {
export async function defaultModuleLoader<T extends Module>(absPath: string): ModuleResult<T> {
// prettier-ignore
const module = await importModule<T>(absPath);
if (module === undefined) {
return Err(SernError.UndefinedModule);
return Err(SernError.UndefinedModule);
}
checkIsProcessed(module)
checkIsProcessed(module);
return Ok(module);
}
function checkIsProcessed<T extends Module>(m: T): asserts m is Processed<T> {
assert.ok(m.name !== undefined, `name is not defined for ${util.format(m)}`)
assert.ok(m.name !== undefined, `name is not defined for ${util.format(m)}`);
}
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
/**
* a directory string is converted into a stream of modules.
@@ -42,7 +39,7 @@ export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
* @param commandDir
*/
export function buildModuleStream<T extends Module>(
input: ObservableInput<string>
input: ObservableInput<string>,
): Observable<Result<Processed<T>, SernError>> {
return from(input).pipe(mergeMap(defaultModuleLoader<T>));
}
@@ -52,28 +49,28 @@ export function getCommands(dir: string) {
}
export function filename(path: string) {
return fmtFileName(basename(path))
return fmtFileName(basename(path));
}
async function* readPath(dir: string): AsyncGenerator<string> {
try {
const files = await readdir(dir);
for (const file of files) {
const fullPath = join(dir, file);
const fileStats = await stat(fullPath);
if (fileStats.isDirectory()) {
yield* readPath(fullPath);
} else {
/// #if MODE === 'esm'
yield 'file:///'+fullPath;
/// #elif MODE === 'cjs'
yield fullPath;
/// #endif
}
try {
const files = await readdir(dir);
for (const file of files) {
const fullPath = join(dir, file);
const fileStats = await stat(fullPath);
if (fileStats.isDirectory()) {
yield* readPath(fullPath);
} else {
/// #if MODE === 'esm'
yield 'file:///' + fullPath;
/// #elif MODE === 'cjs'
yield fullPath;
/// #endif
}
}
} catch (err) {
throw err;
}
} catch (err) {
throw err;
}
}
//https://stackoverflow.com/questions/16697791/nodejs-get-filename-of-caller-function
@@ -86,7 +83,8 @@ export function filePath() {
Error.prepareStackTrace = undefined;
const path = stack[2].getFileName();
if(path === null) {
throw Error("Could not get the name of commandModule.")
if (path === null) {
throw Error('Could not get the name of commandModule.');
}
return path; }
return path;
}

View File

@@ -3,7 +3,20 @@
* Each function should be modular and testable, not bound to discord / sern
* and independent of each other
*/
import { concatMap, defaultIfEmpty, EMPTY, every, fromEvent, map, Observable, of, OperatorFunction, pipe, share, switchMap } from 'rxjs';
import {
concatMap,
defaultIfEmpty,
EMPTY,
every,
fromEvent,
map,
Observable,
of,
OperatorFunction,
pipe,
share,
switchMap,
} from 'rxjs';
import type { PluginResult, VoidResult } from '../types/plugin';
import { Result } from 'ts-results-es';
import { Awaitable } from '../types/handler';
@@ -16,16 +29,18 @@ export function filterMapTo<V>(item: () => V): OperatorFunction<boolean, V> {
return concatMap(shouldKeep => (shouldKeep ? of(item()) : EMPTY));
}
export function filterMap<In, Out>(cb: (i: In) => Awaitable<Result<Out, unknown>>): OperatorFunction<In, Out> {
export function filterMap<In, Out>(
cb: (i: In) => Awaitable<Result<Out, unknown>>,
): OperatorFunction<In, Out> {
return pipe(
switchMap(async input => cb(input)),
concatMap(s => {
if(s.ok) {
return of(s.val)
if (s.ok) {
return of(s.val);
}
return EMPTY;
})
)
}),
);
}
/**
@@ -48,7 +63,6 @@ export function callPlugin(args: unknown): OperatorFunction<
export const arrayifySource = map(src => (Array.isArray(src) ? (src as unknown[]) : [src]));
/**
* If the current value in Result stream is an error, calls callback.
* This also extracts the Ok value from Result
@@ -75,7 +89,5 @@ export const everyPluginOk: OperatorFunction<VoidResult, boolean> = pipe(
);
export const sharedObservable = <T>(e: EventEmitter, eventName: string) => {
return (fromEvent(e, eventName) as Observable<T>).pipe(share())
return (fromEvent(e, eventName) as Observable<T>).pipe(share());
};

View File

@@ -9,7 +9,7 @@ export function makePlugin<V extends unknown[]>(
): Plugin<V> {
return {
type,
execute
execute,
} as Plugin<V>;
}
/**

View File

@@ -1,12 +1,23 @@
import { AnySelectMenuInteraction, AutocompleteInteraction, ButtonInteraction, ChatInputCommandInteraction, MessageContextMenuCommandInteraction, ModalSubmitInteraction, UserContextMenuCommandInteraction } from "discord.js";
import { InteractionType } from "discord.js";
import {
AnySelectMenuInteraction,
AutocompleteInteraction,
ButtonInteraction,
ChatInputCommandInteraction,
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
UserContextMenuCommandInteraction,
} from 'discord.js';
import { InteractionType } from 'discord.js';
interface InteractionTypable {
type: InteractionType
type: InteractionType;
}
//discord.js pls fix ur typings or i will >:(
type AnyMessageComponentInteraction = AnySelectMenuInteraction | ButtonInteraction;
type AnyCommandInteraction = ChatInputCommandInteraction | MessageContextMenuCommandInteraction | UserContextMenuCommandInteraction;
type AnyCommandInteraction =
| ChatInputCommandInteraction
| MessageContextMenuCommandInteraction
| UserContextMenuCommandInteraction;
export function isMessageComponent(i: InteractionTypable): i is AnyMessageComponentInteraction {
return i.type === InteractionType.MessageComponent;
}

View File

@@ -1,19 +1,18 @@
import { Result as Either } from 'ts-results-es';
import { SernError } from './errors';
import * as assert from 'node:assert'
import * as assert from 'node:assert';
/**
* @since 3.0.0
*/
* @since 3.0.0
*/
export abstract class CoreContext<M, I> {
protected constructor(protected ctx: Either<M, I>) {
assert.ok(typeof ctx.val === 'object' && ctx.val != null)
assert.ok(typeof ctx.val === 'object' && ctx.val != null);
}
get message(): M {
return this.ctx.expect(SernError.MismatchEvent);
}
get interaction(): I {
}
get interaction(): I {
return this.ctx.expectErr(SernError.MismatchEvent);
}
@@ -25,11 +24,11 @@ export abstract class CoreContext<M, I> {
return !this.isMessage();
}
//todo: add agnostic options resolver for Context
abstract get options() : unknown
abstract get id() : string
abstract get options(): unknown;
static wrap(_: unknown): unknown { throw Error("You need to override this method; cannot wrap an abstract class") }
abstract get id(): string;
static wrap(_: unknown): unknown {
throw Error('You need to override this method; cannot wrap an abstract class');
}
}

View File

@@ -15,12 +15,12 @@
* ```
*/
export enum CommandType {
Text = 1 << 0,
Slash = 1 << 1,
Both = 3,
Text = 1 << 0,
Slash = 1 << 1,
Both = 3,
CtxUser = 1 << 2,
CtxMsg = 1 << 3,
Button = 1 << 4,
CtxMsg = 1 << 3,
Button = 1 << 4,
StringSelect = 1 << 5,
Modal = 1 << 6,
ChannelSelect = 1 << 7,

View File

@@ -1,3 +1,3 @@
export * from './enums';
export * from './context'
export * from './sernEmitter'
export * from './context';
export * from './sernEmitter';

View File

@@ -84,4 +84,3 @@ export class SernEmitter extends EventEmitter {
);
}
}

View File

@@ -12,13 +12,18 @@ export function contextArgs(
/*
* @overload
*/
export function contextArgs(wrappable: ChatInputCommandInteraction): () => [Context, ['slash', SlashOptions]];
export function contextArgs(
wrappable: ChatInputCommandInteraction,
): () => [Context, ['slash', SlashOptions]];
/**
* function overloads to create an arguments list for Context
* @param wrap
* @param messageArgs
*/
export function contextArgs(wrappable: Message | ChatInputCommandInteraction, messageArgs?: string[]) {
export function contextArgs(
wrappable: Message | ChatInputCommandInteraction,
messageArgs?: string[],
) {
const ctx = Context.wrap(wrappable);
const args = ctx.isMessage() ? ['text', messageArgs!] : ['slash', ctx.options];
return () => [ctx, args] as [Context, Args];
@@ -27,4 +32,3 @@ export function contextArgs(wrappable: Message | ChatInputCommandInteraction, me
export function interactionArg<T>(interaction: T) {
return () => [interaction] as [T];
}

View File

@@ -1,34 +1,34 @@
import { BaseInteraction, ChatInputCommandInteraction, Interaction, InteractionType, Message } from "discord.js";
import { Observable, filter, map } from "rxjs";
import { CommandType, ModuleManager } from "../../core";
import { SernError } from '../../core/structures/errors'
import {
BaseInteraction,
ChatInputCommandInteraction,
Interaction,
InteractionType,
Message,
} from 'discord.js';
import { Observable, filter, map } from 'rxjs';
import { CommandType, ModuleManager } from '../../core';
import { SernError } from '../../core/structures/errors';
import { filterMap } from '../../core/operators';
import { defaultModuleLoader } from "../../core/module-loading";
import { Processed } from "../../types/core";
import { BothCommand, CommandModule, Module } from "../../types/module";
import { contextArgs, dispatchAutocomplete, dispatchCommand, interactionArg } from "./dispatchers";
import { isAutocomplete } from "../../core/predicates";
import { ObservableInput, pipe, switchMap} from "rxjs";
import { SernEmitter } from "../../core";
import { defaultModuleLoader } from '../../core/module-loading';
import { Processed } from '../../types/core';
import { BothCommand, CommandModule, Module } from '../../types/module';
import { contextArgs, dispatchAutocomplete, dispatchCommand, interactionArg } from './dispatchers';
import { isAutocomplete } from '../../core/predicates';
import { ObservableInput, pipe, switchMap } from 'rxjs';
import { SernEmitter } from '../../core';
import { errTap } from '../../core/operators';
import * as Files from '../../core/module-loading';
import { sernMeta } from "../../commands";
import { AnyModule } from "../../types/module";
import { Err, Result } from "ts-results-es";
import { Awaitable } from "../../types/handler";
import { fmt } from "./messages";
import { sernMeta } from '../../commands';
import { AnyModule } from '../../types/module';
import { Err, Result } from 'ts-results-es';
import { Awaitable } from '../../types/handler';
import { fmt } from './messages';
function createGenericHandler<Source, Narrowed extends Source, Output>(
source: Observable<Source>,
makeModule: (event: Narrowed) => Awaitable<Result<Output, unknown>>
makeModule: (event: Narrowed) => Awaitable<Result<Output, unknown>>,
) {
return (pred: (i: Source) => i is Narrowed) =>
source.pipe(
filter(pred),
filterMap(makeModule)
)
return (pred: (i: Source) => i is Narrowed) => source.pipe(filter(pred), filterMap(makeModule));
}
/**
*
@@ -43,37 +43,33 @@ export function createInteractionHandler<T extends Interaction>(
) {
return createGenericHandler<Interaction, T, ReturnType<typeof createDispatcher>>(
source,
( event ) => {
const fullPath = mg.get(createId(event as unknown as Interaction))
if(!fullPath) return Err(SernError.UndefinedModule + " No full path found in module store");
return defaultModuleLoader<CommandModule>(fullPath)
.then(res =>
res.map(module => createDispatcher({ module, event }))
)
}
)
event => {
const fullPath = mg.get(createId(event as unknown as Interaction));
if (!fullPath)
return Err(SernError.UndefinedModule + ' No full path found in module store');
return defaultModuleLoader<CommandModule>(fullPath).then(res =>
res.map(module => createDispatcher({ module, event })),
);
},
);
}
export function createMessageHandler(
source: Observable<Message>,
defaultPrefix: string,
mg: ModuleManager
mg: ModuleManager,
) {
return createGenericHandler(
source,
( event ) => {
const [prefix, ...rest] = fmt(event.content, defaultPrefix);
const fullPath = mg.get(`${prefix}__A0`);
if (fullPath === undefined) {
return Err(SernError.UndefinedModule + " No full path found in module store");
}
return defaultModuleLoader<CommandModule>(fullPath).then(
result => {
const args = contextArgs(event, rest);
return result.map(module => dispatchCommand(module, args))
})
return createGenericHandler(source, event => {
const [prefix, ...rest] = fmt(event.content, defaultPrefix);
const fullPath = mg.get(`${prefix}__A0`);
if (fullPath === undefined) {
return Err(SernError.UndefinedModule + ' No full path found in module store');
}
)
return defaultModuleLoader<CommandModule>(fullPath).then(result => {
const args = contextArgs(event, rest);
return result.map(module => dispatchCommand(module, args));
});
});
}
/**
* Creates a unique ID for a given interaction object.
@@ -81,21 +77,27 @@ export function createMessageHandler(
* @returns A unique string ID based on the type and properties of the interaction object.
*/
function createId<T extends Interaction>(event: T) {
let id: string;
switch(event.type) {
case InteractionType.MessageComponent: {
let id: string;
switch (event.type) {
case InteractionType.MessageComponent:
{
id = `${event.customId}__C${event.componentType}`;
} break;
case InteractionType.ApplicationCommand:
case InteractionType.ApplicationCommandAutocomplete: {
}
break;
case InteractionType.ApplicationCommand:
case InteractionType.ApplicationCommandAutocomplete:
{
id = `${event.commandName}__A${event.commandType}`;
console.log(id)
} break;
case InteractionType.ModalSubmit: {
console.log(id);
}
break;
case InteractionType.ModalSubmit:
{
id = `${event.customId}__C1`;
} break;
}
return id;
}
break;
}
return id;
}
function createDispatcher({
@@ -107,7 +109,7 @@ function createDispatcher({
}) {
switch (module.type) {
case CommandType.Text:
throw Error(SernError.MismatchEvent+ " Found a text module in interaction stream.");
throw Error(SernError.MismatchEvent + ' Found a text module in interaction stream.');
case CommandType.Slash:
case CommandType.Both: {
if (isAutocomplete(event)) {
@@ -117,7 +119,7 @@ function createDispatcher({
* too different from regular command modules
*/
return dispatchAutocomplete(module as Processed<BothCommand>, event);
}
}
return dispatchCommand(module, contextArgs(event as ChatInputCommandInteraction));
}
default:
@@ -125,16 +127,15 @@ function createDispatcher({
}
}
export function buildModules<T extends AnyModule>(
input: ObservableInput<string>, sernEmitter: SernEmitter
input: ObservableInput<string>,
sernEmitter: SernEmitter,
) {
return pipe(
switchMap(() => Files.buildModuleStream<T>(input)),
errTap(error => {
sernEmitter.emit('module.register', SernEmitter.failure(undefined, error));
}),
map(module => ({ module, absPath: module[sernMeta].fullPath }))
map(module => ({ module, absPath: module[sernMeta].fullPath })),
);
}

View File

@@ -1,37 +1,30 @@
import { BaseInteraction, Interaction } from 'discord.js';
import {
catchError,
concatMap,
finalize,
merge,
} from 'rxjs';
import { catchError, concatMap, finalize, merge } from 'rxjs';
import { SernError } from '../../core/structures/errors';
import { executeModule, makeModuleExecutor } from './observableHandling';
import { ErrorHandling, handleError } from '../../core/contracts/errorHandling';
import { SernEmitter, WebsocketStrategy } from '../../core';
import { sharedObservable } from '../../core/operators'
import { sharedObservable } from '../../core/operators';
import { useContainerRaw } from '../../core/dependencies';
import type { Logging, ModuleManager } from '../../core/contracts';
import type { EventEmitter } from 'node:events';
import { isAutocomplete, isCommand, isMessageComponent, isModal } from '../../core/predicates';
import { createInteractionHandler } from './generic';
export function makeInteractionCreate([s, err, log, modules, client]: [
SernEmitter,
ErrorHandling,
Logging | undefined,
ModuleManager,
EventEmitter
]
) {
EventEmitter,
]) {
const interactionStream$ = sharedObservable<Interaction>(client, 'interactionCreate');
const handle = createInteractionHandler<Interaction>(interactionStream$, modules);
const interactionHandler$ = merge(
handle(isMessageComponent),
handle(isAutocomplete),
handle(isCommand),
handle(isModal)
handle(isModal),
);
return interactionHandler$
.pipe(

View File

@@ -37,29 +37,26 @@ export function makeMessageCreate(
ModuleManager,
EventEmitter,
],
defaultPrefix: string | undefined
defaultPrefix: string | undefined,
) {
if(!defaultPrefix) {
log?.debug({ message: 'No prefix found. message handler shut down' })
return EMPTY.subscribe()
if (!defaultPrefix) {
log?.debug({ message: 'No prefix found. message handler shut down' });
return EMPTY.subscribe();
}
const messageStream$ = sharedObservable<Message>(client, 'messageCreate');
const handler = createMessageHandler(messageStream$, defaultPrefix, modules);
const messageHandler = handler(
ignoreNonBot(defaultPrefix) as (m: Message) => m is Message
)
return messageHandler
.pipe(
makeModuleExecutor(module => {
s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
concatMap(payload => executeModule(s, payload)),
catchError(handleError(err, log)),
finalize(() => {
log?.info({ message: 'messageCreate stream closed or reached end of lifetime' });
useContainerRaw()
?.disposeAll()
.then(() => log?.info({ message: 'Cleaning container and crashing' }));
}),
)
const messageHandler = handler(ignoreNonBot(defaultPrefix) as (m: Message) => m is Message);
return messageHandler.pipe(
makeModuleExecutor(module => {
s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
concatMap(payload => executeModule(s, payload)),
catchError(handleError(err, log)),
finalize(() => {
log?.info({ message: 'messageCreate stream closed or reached end of lifetime' });
useContainerRaw()
?.disposeAll()
.then(() => log?.info({ message: 'Cleaning container and crashing' }));
}),
);
}

View File

@@ -6,20 +6,18 @@ import { callPlugin, everyPluginOk, filterMapTo } from '../../core/operators';
import type { ImportPayload, Processed } from '../../types/core';
import type { ControlPlugin, VoidResult } from '../../types/plugin';
import { Awaitable } from '../../types/handler';
import { Message } from 'discord.js'
import { Message } from 'discord.js';
function hasPrefix(prefix: string, content: string) {
const prefixInContent = content.slice(0, prefix.length);
return prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0;
}
/**
* Ignores messages from any person / bot except itself
* @param prefix
*/
export function ignoreNonBot(prefix: string) {
return ({ author, content }: Message) =>
!author.bot && hasPrefix(prefix, content);
return ({ author, content }: Message) => !author.bot && hasPrefix(prefix, content);
}
/**
@@ -76,7 +74,7 @@ export function createResultResolver<
const task$ = config.createStream(args);
return task$.pipe(
tap(result => {
result.err && config.onStop?.(args.module);
result.err && config.onStop?.(args.module);
}),
everyPluginOk,
filterMapTo(() => config.onNext(args)),

View File

@@ -1,7 +1,7 @@
import { ObservableInput, fromEvent, of, take } from 'rxjs';
import { callInitPlugins } from './observableHandling';
import { CommandType } from '../../core/structures';
import { SernError } from '../../core/structures/errors'
import { SernError } from '../../core/structures/errors';
import { Result } from 'ts-results-es';
import type { ModuleManager } from '../../core/contracts';
import { SernEmitter, PlatformStrategy, DispatchType } from '../../core';
@@ -12,7 +12,9 @@ import * as assert from 'node:assert';
import { buildModules } from './generic';
export function startReadyEvent(
[sEmitter, errorHandler, , moduleManager, client]: ServerlessDependencyList | WebsocketDependencyList,
[sEmitter, errorHandler, , moduleManager, client]:
| ServerlessDependencyList
| WebsocketDependencyList,
input: ObservableInput<string>,
) {
const ready$ = fromEvent(client!, 'interactionCreate').pipe(take(1));
@@ -40,19 +42,15 @@ export function startReadyEvent(
});
}
function registerModule<T extends Processed<Module>>(
manager: ModuleManager,
module: T,
): Result<void, void> {
const { id, fullPath } = module[sernMeta];
if(module.type === CommandType.Both || module.type === CommandType.Text) {
assert.ok('alias' in module)
assert.ok(Array.isArray(module.alias))
module.alias?.forEach(a => manager.set(`${a}__A0` , fullPath))
const { id, fullPath } = module[sernMeta];
if (module.type === CommandType.Both || module.type === CommandType.Text) {
assert.ok('alias' in module);
assert.ok(Array.isArray(module.alias));
module.alias?.forEach(a => manager.set(`${a}__A0`, fullPath));
}
return Result.wrap(() => manager.set(id, fullPath))
return Result.wrap(() => manager.set(id, fullPath));
}

View File

@@ -5,7 +5,7 @@ import type { CommandModule, EventModule } from '../../types/module';
import type { EventEmitter } from 'node:events';
import { SernEmitter } from '../../core';
import type { ErrorHandling, Logging } from '../../core/contracts';
import { EventType } from '../../core/structures'
import { EventType } from '../../core/structures';
import { SernError } from '../../core/structures/errors';
import { eventDispatcher } from './dispatchers';
import { handleError } from '../../core/contracts/errorHandling';
@@ -32,29 +32,31 @@ export function makeEventsHandler(
);
}
};
of(null).pipe(
buildModules(eventsPath, s),
callInitPlugins({
onStop: module =>
s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),
onNext: ({ module }) => {
s.emit('module.register', SernEmitter.success(module));
return module;
},
}),
map(intoDispatcher),
/**
* Where all events are turned on
*/
mergeAll(),
catchError(handleError(err, log)),
finalize(() => {
log?.info({ message: 'an event module reached end of lifetime' });
useContainerRaw()
?.disposeAll()
.then(() => {
log?.info({ message: 'Cleaning container and crashing' });
});
}),
).subscribe();
of(null)
.pipe(
buildModules(eventsPath, s),
callInitPlugins({
onStop: module =>
s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),
onNext: ({ module }) => {
s.emit('module.register', SernEmitter.success(module));
return module;
},
}),
map(intoDispatcher),
/**
* Where all events are turned on
*/
mergeAll(),
catchError(handleError(err, log)),
finalize(() => {
log?.info({ message: 'an event module reached end of lifetime' });
useContainerRaw()
?.disposeAll()
.then(() => {
log?.info({ message: 'Cleaning container and crashing' });
});
}),
)
.subscribe();
}

View File

@@ -27,7 +27,9 @@ export function init(wrapper: Wrapper) {
const dependencies = dependenciesAnd(['@sern/modules', '@sern/client']);
if (wrapper.events !== undefined) {
makeEventsHandler(
dependenciesAnd(['@sern/client']), wrapper.events, wrapper.containerConfig
dependenciesAnd(['@sern/client']),
wrapper.events,
wrapper.containerConfig,
);
}
startReadyEvent(dependencies, getCommands(wrapper.commands));
@@ -35,16 +37,15 @@ export function init(wrapper: Wrapper) {
makeInteractionCreate(dependencies);
const endTime = performance.now();
dependencies[2]?.info({ message: `sern : ${(endTime - startTime).toFixed(2)} ms` });
}
/**
* @deprecated - Please import the function directly:
* ```ts
* import { makeDependencies } from '@sern/handler'
*
* ```
*/
export { makeDependencies }
* @deprecated - Please import the function directly:
* ```ts
* import { makeDependencies } from '@sern/handler'
*
* ```
*/
export { makeDependencies };
/**
* @since 1.0.0
* The object passed into every plugin to control a command's behavior
@@ -53,4 +54,3 @@ export const controller = {
next: ok,
stop: err,
};

View File

@@ -3,6 +3,6 @@ export * from './types/handler';
export * from './types/module';
export * from './types/plugin';
export * from './core';
export { controller } from './handler/sern'
export { commandModule, eventModule } from './commands'
export { Context } from './classic/context'
export { controller } from './handler/sern';
export { commandModule, eventModule } from './commands';
export { Context } from './classic/context';

View File

@@ -1,10 +1,21 @@
import { type EventEmitter } from "node:events";
import { ErrorHandling, Logging, ModuleManager, SernEmitter } from "../core";
import { Container, UnpackFunction } from "iti";
import { type EventEmitter } from 'node:events';
import { ErrorHandling, Logging, ModuleManager, SernEmitter } from '../core';
import { Container, UnpackFunction } from 'iti';
export type ModuleStore = Map<string,string>
export type ServerlessDependencyList = [ SernEmitter,ErrorHandling, Logging | undefined, ModuleManager];
export type WebsocketDependencyList = [SernEmitter,ErrorHandling, Logging | undefined, ModuleManager, EventEmitter];
export type ModuleStore = Map<string, string>;
export type ServerlessDependencyList = [
SernEmitter,
ErrorHandling,
Logging | undefined,
ModuleManager,
];
export type WebsocketDependencyList = [
SernEmitter,
ErrorHandling,
Logging | undefined,
ModuleManager,
EventEmitter,
];
/**
* After modules are transformed, name and description are given default values if none
* are provided to Module. This type represents that transformation
@@ -22,20 +33,18 @@ export interface CoreDependencies {
'@sern/errors': Singleton<ErrorHandling>;
}
/**
* To support older versions. Type alias for WebsocketDependencies
* @deprecated
*/
export type Dependencies = WebsocketDependencies
* To support older versions. Type alias for WebsocketDependencies
* @deprecated
*/
export type Dependencies = WebsocketDependencies;
export interface ServerlessDependencies extends CoreDependencies {
'@sern/client': never
'@sern/client': never;
}
export interface WebsocketDependencies extends CoreDependencies {
'@sern/client': Singleton<EventEmitter>;
}
export type AnyDependencies =
| ServerlessDependencies
| WebsocketDependencies;
export type AnyDependencies = ServerlessDependencies | WebsocketDependencies;
//prettier-ignore
export type MapDeps<Deps extends AnyDependencies, T extends readonly unknown[]> = T extends [
@@ -58,8 +67,8 @@ export interface DependencyConfiguration<T extends AnyDependencies> {
export interface ImportPayload<T> {
module: T;
absPath: string
};
absPath: string;
}
export interface Wrapper {
commands: string;
@@ -67,5 +76,5 @@ export interface Wrapper {
events?: string;
containerConfig: {
get: (...keys: (keyof WebsocketDependencies)[]) => unknown[];
}
};
}

View File

@@ -1,4 +1,8 @@
import type { InteractionReplyOptions, MessageReplyOptions, CommandInteractionOptionResolver } from 'discord.js';
import type {
InteractionReplyOptions,
MessageReplyOptions,
CommandInteractionOptionResolver,
} from 'discord.js';
import { Processed } from './core';
import { AnyModule, CommandModule, EventModule } from './module';
import { PayloadType } from '../core';
@@ -14,13 +18,11 @@ export type Args = ParseType<{ text: string[]; slash: SlashOptions }>;
export type SlashOptions = Omit<CommandInteractionOptionResolver, 'getMessage' | 'getFocused'>;
export type ReplyOptions =
| string
| Omit<InteractionReplyOptions, 'fetchReply'>
| MessageReplyOptions;
export type AnyDefinedModule = Processed<CommandModule | EventModule>;
export type Payload =
| { type: PayloadType.Success; module: AnyModule }
@@ -32,4 +34,4 @@ export interface SernEventsMapping {
'module.activate': [Payload];
error: [Payload];
warning: [Payload];
};
}

View File

@@ -10,7 +10,7 @@ import type {
ApplicationCommandSubGroupData,
BaseApplicationCommandOptionsData,
} from 'discord.js';
import {
import {
AutocompleteInteraction,
ButtonInteraction,
ChannelSelectMenuInteraction,
@@ -21,13 +21,12 @@ import {
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserContextMenuCommandInteraction,
UserSelectMenuInteraction
} from "discord.js";
import { InitArgs, } from "../core";
import { Args, Payload, SlashOptions } from "../types/handler";
import { Context } from "../classic/context";
import { Processed } from "../types/core";
UserSelectMenuInteraction,
} from 'discord.js';
import { InitArgs } from '../core';
import { Args, Payload, SlashOptions } from '../types/handler';
import { Context } from '../classic/context';
import { Processed } from '../types/core';
import { CommandType, PluginType } from '../core/structures/enums';
import type { Awaitable, SernEventsMapping } from './handler';
import type { InitPlugin, ControlPlugin } from './plugin';
@@ -36,8 +35,8 @@ import type { AnyCommandPlugin, AnyEventPlugin } from './plugin';
import { sernMeta } from '../commands';
interface CommandMeta {
fullPath: string;
id: string;
fullPath: string;
id: string;
}
export interface Module {
@@ -46,14 +45,14 @@ export interface Module {
onEvent: ControlPlugin[];
plugins: InitPlugin[];
description?: string;
[sernMeta] : CommandMeta
[sernMeta]: CommandMeta;
execute: (...args: any[]) => Awaitable<any>;
}
export interface CommandTypeModule extends Module {
type: CommandType
type: CommandType;
}
export interface EventTypeModule extends Module {
type: EventType
type: EventType;
}
export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping>
extends Module {
@@ -68,7 +67,6 @@ export interface ExternalEventCommand extends Module {
execute(...args: unknown[]): Awaitable<unknown>;
}
export interface ContextMenuUser extends Module {
type: CommandType.CtxUser;
execute: (ctx: UserContextMenuCommandInteraction) => Awaitable<unknown>;
@@ -195,7 +193,7 @@ export interface CommandArgsMatrix {
[PluginType.Control]: [/* library coupled */ ModalSubmitInteraction];
[PluginType.Init]: [InitArgs<Processed<ModalSubmitCommand>>];
};
};
}
export interface EventArgsMatrix {
[EventType.Discord]: {
@@ -210,7 +208,7 @@ export interface EventArgsMatrix {
[PluginType.Control]: unknown[];
[PluginType.Init]: [InitArgs<Processed<ExternalEventCommand>>];
};
};
}
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
export type CommandModule =
| TextCommand
@@ -243,13 +241,13 @@ export interface CommandModuleDefs {
[CommandType.MentionableSelect]: MentionableSelectCommand;
[CommandType.UserSelect]: UserSelectCommand;
[CommandType.Modal]: ModalSubmitCommand;
};
}
export interface EventModuleDefs {
[EventType.Sern]: SernEventCommand;
[EventType.Discord]: DiscordEventCommand;
[EventType.External]: ExternalEventCommand;
};
}
export interface SernAutocompleteData
extends Omit<BaseApplicationCommandOptionsData, 'autocomplete'> {

View File

@@ -40,4 +40,3 @@ export interface ControlPlugin<Args extends any[] = any[]> {
export type AnyCommandPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<CommandModule>>]>;
export type AnyEventPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<EventModule>>]>;

View File

@@ -19,10 +19,8 @@ export default defineConfig([
target: 'node16',
tsconfig: './tsconfig-esm.json',
outDir: './dist/esm',
splitting: false,
esbuildPlugins: [
ifdefPlugin({ variables: { MODE: 'esm' }, verbose: true }),
],
splitting: false,
esbuildPlugins: [ifdefPlugin({ variables: { MODE: 'esm' }, verbose: true })],
outExtension() {
return {
js: '.mjs',