refactor: Remove PluggedModule, change Module signature to support event plugins

This commit is contained in:
Jacob Nguyen
2022-05-15 15:58:54 -05:00
parent 61fe8534b5
commit b6bf08f673
9 changed files with 106 additions and 112 deletions

View File

@@ -8,37 +8,39 @@ import type {
import { concatMap, fromEvent, Observable, of, throwError } from 'rxjs';
import type Wrapper from '../structures/wrapper';
import * as Files from '../utilities/readFile';
import { isEventPlugin } from './readyEvent';
import { match, P } from 'ts-pattern';
import { SernError } from '../structures/errors';
import Context from '../structures/context';
import type { Result } from 'ts-results';
import type { PluggedModule } from '../structures/modules/module';
import { CommandType, controller } from '../sern';
import type { Args } from '../../types/handler';
import type { MessageComponentInteraction } from 'discord.js';
import { ComponentType } from 'discord.js';
import type { UnionToTuple } from '../utilities/resolveParameters';
import type { Module } from '../structures/modules/commands/module';
import type { EventPlugin } from '../plugins/plugin';
function isChatInputCommand(i : CommandInteraction) : i is ChatInputCommandInteraction {
return i.isChatInputCommand();
}
function applicationCommandHandler(plugged: PluggedModule| undefined, interaction: CommandInteraction) {
if (plugged === undefined) {
function applicationCommandHandler(mod: Module | undefined, interaction: CommandInteraction) {
if (mod === undefined) {
return throwError(() => SernError.UndefinedModule);
}
const eventPlugins = plugged.plugins.filter(isEventPlugin);
const eventPlugins = mod.onEvent;
return match(interaction)
.when(isChatInputCommand, i => {
const ctx = Context.wrap(i);
const res = eventPlugins.map(e => {
return e.execute(
const res = eventPlugins.map(e => {
return (<EventPlugin<CommandType.Both>>e).execute(
[ctx, <Args>['slash', i.options]]
, controller);
}) as Awaited<Result<void, void>>[];
//Possible unsafe cast
// could result in the promises not being resolved
return of({ type : plugged.mod.type, res, plugged, ctx });
return of({ type : mod.type, res, plugged: mod, ctx });
},
)
.when(
@@ -50,20 +52,20 @@ function applicationCommandHandler(plugged: PluggedModule| undefined, interactio
[ctx] as UnionToTuple<CommandType.MenuMsg | CommandType.MenuUser>
, controller);
}) as Awaited<Result<void, void>>[];
return of({ type : plugged.mod.type, res, plugged, ctx });
return of({ type : mod.type, res, plugged: mod, ctx });
},
)
.run();
}
function messageComponentInteractionHandler(
plugged: PluggedModule | undefined,
mod: Module | undefined,
interaction: MessageComponentInteraction,
) {
if (plugged === undefined) {
if (mod === undefined) {
return throwError(() => SernError.UndefinedModule);
}
const eventPlugins = plugged.plugins.filter(isEventPlugin);
const eventPlugins = mod.onEvent;
return match(interaction)
.with({
componentType : P.union(ComponentType.Button, ComponentType.SelectMenu)
@@ -71,7 +73,7 @@ function messageComponentInteractionHandler(
const res = eventPlugins.map(e => {
return e.execute([ctx] as UnionToTuple<CommandType.Button | CommandType.MenuSelect>, controller);
}) as Awaited<Result<void, void>>[];
return of({ type : plugged.mod.type, res, plugged, ctx });
return of({ type : mod.type, res, plugged: mod, ctx });
})
.otherwise(() => throwError( () => SernError.NotSupportedInteraction) );
}

View File

@@ -8,7 +8,6 @@ import type Wrapper from '../structures/wrapper';
import { fmt } from '../utilities/messageHelpers';
import * as Files from '../utilities/readFile';
import { filterCorrectModule, ignoreNonBot } from './observableHandling';
import { isEventPlugin } from './readyEvent';
export const onMessageCreate = (wrapper: Wrapper) => {
const { client, defaultPrefix } = wrapper;
@@ -40,25 +39,24 @@ export const onMessageCreate = (wrapper: Wrapper) => {
);
const processEventPlugins$ = ensureModuleType$.pipe(
concatMap(({ ctx, args, mod: plugged }) => {
const eventPlugins = plugged.plugins.filter(isEventPlugin);
concatMap(({ ctx, args, mod }) => {
const res = Promise.all(
eventPlugins.map(ePlug => {
if ((ePlug.modType & plugged.mod.type) === 0) {
mod.onEvent.map(ePlug => {
if ((ePlug.modType & mod.type) === 0) {
return Err.EMPTY;
}
return ePlug.execute([ctx, args], controller);
}),
);
return from(res).pipe(map(res => ({ plugged, ctx, args, res })));
return from(res).pipe(map(res => ({ mod, ctx, args, res })));
}),
);
processEventPlugins$.subscribe(({ plugged, ctx, args, res }) => {
processEventPlugins$.subscribe(({ mod, ctx, args, res }) => {
if (res.every(pl => pl.ok)) {
Promise.resolve(plugged.mod.execute(ctx, args)).then(() => console.log(plugged));
Promise.resolve(mod.execute(ctx, args)).then(() => console.log(mod));
} else {
console.log(plugged, 'failed');
console.log(mod, 'failed');
}
});
};

View File

@@ -3,25 +3,24 @@ import { Observable, throwError } from 'rxjs';
import type { ModuleDefs } from '../structures/modules/commands/moduleHandler';
import { SernError } from '../structures/errors';
import { isNotFromBot } from '../utilities/messageHelpers';
import type { PluggedModule } from '../structures/modules/module';
import type { SernPlugin } from '../plugins/plugin';
import type { Module } from '../structures/modules/commands/module';
export function correctModuleType<T extends keyof ModuleDefs>(
plug: PluggedModule | undefined,
plug: Module | undefined,
type: T,
): plug is { mod: ModuleDefs[T]; plugins: SernPlugin[] } {
return plug !== undefined && plug.mod.type === type;
): plug is ModuleDefs[T] {
return plug !== undefined && plug.type === type;
}
export function filterCorrectModule<T extends keyof ModuleDefs>(cmdType: T) {
return (src: Observable<PluggedModule | undefined>) =>
new Observable<{ mod: ModuleDefs[T]; plugins: SernPlugin[] }>(subscriber => {
return (src: Observable<Module | undefined>) =>
new Observable<ModuleDefs[T]>(subscriber => {
return src.subscribe({
next(plug) {
if (correctModuleType(plug, cmdType)) {
subscriber.next({ mod: plug.mod, plugins: plug.plugins });
next(mod) {
if (correctModuleType(mod, cmdType)) {
subscriber.next(mod);
} else {
if (plug === undefined) {
if (mod === undefined) {
return throwError(() => SernError.UndefinedModule);
}
return throwError(() => SernError.MismatchModule);

View File

@@ -9,28 +9,26 @@ import type {
ModuleType,
} from '../structures/modules/commands/moduleHandler';
import { CommandType } from '../sern';
import { CommandPlugin, EventPlugin, PluginType, SernPlugin } from '../plugins/plugin';
import { partition } from '../utilities/partition';
import type { PluginType } from '../plugins/plugin';
import { Err, Ok, Result } from 'ts-results';
import type { PluggedModule } from '../structures/modules/module';
import type { Awaitable } from 'discord.js';
import type { Module } from '../structures/modules/commands/module';
export const onReady = (wrapper: Wrapper) => {
const { client, commands } = wrapper;
const ready$ = fromEvent(client, 'ready').pipe(take(1), skip(1));
const processCommandFiles$ = Files.buildData(commands).pipe(
map(({ plugged, absPath }) => {
const name = plugged.mod?.name ?? Files.fmtFileName(basename(absPath));
if (plugged.mod?.name === undefined) {
return { mod: { name, ...plugged.mod }, plugins: plugged.plugins };
map(({ mod, absPath }) => {
const name = mod?.name ?? Files.fmtFileName(basename(absPath));
if (mod?.name === undefined) {
return { name, ...mod } ;
}
return plugged;
return mod;
}),
);
const processPlugins$ = processCommandFiles$.pipe(
concatMap(({ mod, plugins: allPlugins }) => {
const [cmdPlugins, eventPlugins] = partition(isCmdPlugin, allPlugins);
const cmdPluginsRes = cmdPlugins.map(plug => {
concatMap(( mod ) => {
const cmdPluginsRes = mod.plugins.map(plug => {
return {
...plug,
name: plug?.name ?? 'Unnamed Plugin',
@@ -40,13 +38,13 @@ export const onReady = (wrapper: Wrapper) => {
}),
};
});
return of({ plugged: <PluggedModule>{ mod, plugins: eventPlugins }, cmdPluginsRes });
return of({ mod , cmdPluginsRes });
}),
);
(
concat(ready$, processPlugins$) as Observable<{
plugged: PluggedModule;
mod: Module;
cmdPluginsRes: {
execute: Awaitable<Result<void, void>>;
type: PluginType.Command;
@@ -62,11 +60,10 @@ export const onReady = (wrapper: Wrapper) => {
),
),
)
.subscribe(({ plugged, cmdPluginsRes }) => {
.subscribe(({ mod, cmdPluginsRes }) => {
const loadedPluginsCorrectly = cmdPluginsRes.every(res => res.execute.ok);
const { mod, plugins } = plugged;
if (loadedPluginsCorrectly) {
registerModule(mod.name!, mod, plugins);
registerModule(mod.name!, mod);
} else {
console.log(`Failed to load command ${mod.name!}`);
console.log(mod);
@@ -76,40 +73,32 @@ export const onReady = (wrapper: Wrapper) => {
function handler(name: string): ModuleHandlers {
return {
[CommandType.Text]: (mod, plugins) => {
mod.alias.forEach(a => Files.TextCommandStore.aliases.set(a, { mod, plugins }));
Files.TextCommandStore.text.set(name, { mod, plugins });
[CommandType.Text]: (mod) => {
mod.alias.forEach(a => Files.TextCommandStore.aliases.set(a, mod));
Files.TextCommandStore.text.set(name, mod);
},
[CommandType.Slash]: (mod, plugins) => {
Files.ApplicationCommandStore[1].set(name, { mod, plugins });
[CommandType.Slash]: (mod) => {
Files.ApplicationCommandStore[1].set(name, mod);
},
[CommandType.Both]: (mod, plugins) => {
Files.BothCommand.set(name, { mod, plugins });
mod.alias.forEach(a => Files.TextCommandStore.aliases.set(a, { mod, plugins }));
[CommandType.Both]: (mod) => {
Files.BothCommand.set(name, mod);
mod.alias.forEach(a => Files.TextCommandStore.aliases.set(a, mod));
},
[CommandType.MenuUser]: (mod, plugins) => {
Files.ApplicationCommandStore[2].set(name, { mod, plugins });
[CommandType.MenuUser]: (mod) => {
Files.ApplicationCommandStore[2].set(name, mod);
},
[CommandType.MenuMsg]: (mod, plugins) => {
Files.ApplicationCommandStore[3].set(name, { mod, plugins });
[CommandType.MenuMsg]: (mod) => {
Files.ApplicationCommandStore[3].set(name, mod);
},
[CommandType.Button]: (mod, plugins) => {
Files.MessageCompCommandStore[2].set(name, { mod, plugins });
[CommandType.Button]: (mod) => {
Files.MessageCompCommandStore[2].set(name, mod);
},
[CommandType.MenuSelect]: (mod, plugins) => {
Files.MessageCompCommandStore[2].set(name, { mod, plugins });
[CommandType.MenuSelect]: (mod) => {
Files.MessageCompCommandStore[2].set(name, mod);
},
};
}
function isCmdPlugin(p: SernPlugin): p is CommandPlugin {
return (p.type & PluginType.Command) !== 0;
}
export function isEventPlugin<T extends CommandType>(p: SernPlugin): p is EventPlugin {
return (p.type & PluginType.Event) !== 0;
}
function registerModule<T extends ModuleType>(name: string, mod: ModuleStates[T], plugins: SernPlugin[]) {
return (<HandlerCallback<CommandType>>handler(name)[mod.type])(mod, plugins);
function registerModule<T extends ModuleType>(name: string, mod: ModuleStates[T]) {
return (<HandlerCallback<CommandType>>handler(name)[mod.type])(mod);
}

View File

@@ -7,8 +7,8 @@
// The goal of plugins is to organize commands and
// provide extensions to repetitive patterns
// examples include refreshing modules,
// categorizing commands, cooldowns, permissions, etc
// Plugins are reminisce of middleware in express.
// categorizing commands, cooldowns, permissions, etc.
// Plugins are reminiscent of middleware in express.
//
import type { Awaitable, Client } from 'discord.js';
@@ -16,7 +16,7 @@ import type { Err, Ok, Result } from 'ts-results';
import type { Module, Override, Wrapper } from '../..';
import type { CommandType } from '../sern';
import type { ModuleDefs } from '../structures/modules/commands/moduleHandler';
import type { BaseModule, PluggedModule } from '../structures/modules/module';
import type { BaseModule } from '../structures/modules/module';
export enum PluginType {
Command = 0b01,
@@ -45,7 +45,7 @@ export type CommandPlugin = {
//TODO: rn adding the modType check a little hackish. Find better way to determine the
// module type of the event plugin
export type EventPlugin<T extends CommandType = CommandType> = {
export type EventPlugin<T extends CommandType> = {
type: PluginType.Event;
modType: T;
} & Override<
@@ -54,16 +54,13 @@ export type EventPlugin<T extends CommandType = CommandType> = {
execute: (event: Parameters<ModuleDefs[T]['execute']>, controller: Controller) => Awaitable<Result<void, void>>;
}
>;
export function plugins(...plug: CommandPlugin[]) : CommandPlugin[];
export function plugins<T extends CommandType>(...plug: EventPlugin<T>[]): EventPlugin<T>[];
export type SernPlugin = CommandPlugin | EventPlugin;
export function plugins(...plug: SernPlugin[]) {
export function plugins<T extends CommandType>(...plug : CommandPlugin[] | EventPlugin<T>[]) {
return plug;
}
export function sernModule(plugins: SernPlugin[] , mod: Module): PluggedModule {
return {
mod,
plugins,
};
export function sernModule(mod: Module): Module {
return mod;
}

View File

@@ -9,37 +9,53 @@ import type { Override } from '../../../../types/handler';
import type { CommandType } from '../../../sern';
import type { BaseModule } from '../module';
import type { UserContextMenuCommandInteraction } from 'discord.js';
import type { CommandPlugin, EventPlugin } from '../../../plugins/plugin';
//possible refactoring to interfaces and not types
//possible refactoring types into interfaces and not types
export type TextCommand = {
type: CommandType.Text;
onEvent : EventPlugin<CommandType.Text>[];
plugins : CommandPlugin[];
alias: string[] | [];
} & BaseModule;
export type SlashCommand = {
type: CommandType.Slash;
onEvent : EventPlugin<CommandType.Slash>[];
plugins : CommandPlugin[];
options: ApplicationCommandOptionData[] | [];
} & BaseModule;
export type BothCommand = {
type: CommandType.Both;
onEvent : EventPlugin<CommandType.Both>[]
plugins : CommandPlugin[]
alias: string[] | [];
options: ApplicationCommandOptionData[] | [];
} & BaseModule;
export type ContextMenuUser = {
type: CommandType.MenuUser;
onEvent : EventPlugin<CommandType.MenuUser>[];
plugins : CommandPlugin[];
} & Override<BaseModule, { execute: (ctx: UserContextMenuCommandInteraction) => Awaitable<void> }>;
export type ContextMenuMsg = {
type: CommandType.MenuMsg;
onEvent : EventPlugin<CommandType.MenuUser>[];
plugins : CommandPlugin[];
} & Override<BaseModule, { execute: (ctx: MessageContextMenuCommandInteraction) => Awaitable<void> }>;
export type ButtonCommand = {
type: CommandType.Button;
onEvent : EventPlugin<CommandType.Button>[];
plugins : CommandPlugin[];
} & Override<BaseModule, { execute: (ctx: ButtonInteraction) => Awaitable<void> }>;
export type SelectMenuCommand = {
type: CommandType.MenuSelect;
onEvent : EventPlugin<CommandType.MenuSelect>[];
plugins :CommandPlugin[];
} & Override<BaseModule, { execute: (ctx: SelectMenuInteraction) => Awaitable<void> }>;
export type Module =

View File

@@ -1,4 +1,3 @@
import type { SernPlugin } from '../../../plugins/plugin';
import { CommandType } from '../../../sern';
import type {
BothCommand,
@@ -28,6 +27,6 @@ export type ModuleStates = {
[K in ModuleType]: { type: K } & ModuleDefs[K];
};
// A handler callback that is called on each ModuleDef
export type HandlerCallback<K extends ModuleType> = (mod: ModuleStates[K], plugins: SernPlugin[]) => unknown;
export type HandlerCallback<K extends ModuleType> = (mod: ModuleStates[K]) => unknown;
//An object that acts as the mapped object to handler
export type ModuleHandlers = { [K in ModuleType]: HandlerCallback<K> };

View File

@@ -1,6 +1,5 @@
import type { Awaitable } from 'discord.js';
import type { Args, Module } from '../../..';
import type { SernPlugin } from '../../plugins/plugin';
import type { Args } from '../../..';
import type Context from '../context';
export interface BaseModule {
@@ -8,8 +7,3 @@ export interface BaseModule {
description: string;
execute: (ctx: Context, args: Args) => Awaitable<void>;
}
export interface PluggedModule {
mod: Module;
plugins: SernPlugin[];
}

View File

@@ -2,23 +2,23 @@ import { ApplicationCommandType, ComponentType } from 'discord.js';
import { readdirSync, statSync } from 'fs';
import { join } from 'path';
import { from, Observable } from 'rxjs';
import type { PluggedModule } from '../structures/modules/module';
import type { Module } from '../structures/modules/commands/module';
export const BothCommand = new Map<string, PluggedModule>();
export const BothCommand = new Map<string, Module>();
export const ApplicationCommandStore = {
[ApplicationCommandType.User]: new Map<string, PluggedModule>(),
[ApplicationCommandType.Message]: new Map<string, PluggedModule>(),
[ApplicationCommandType.ChatInput]: new Map<string, PluggedModule>(),
} as { [K in ApplicationCommandType]: Map<string, PluggedModule> };
[ApplicationCommandType.User]: new Map<string, Module>(),
[ApplicationCommandType.Message]: new Map<string, Module>(),
[ApplicationCommandType.ChatInput]: new Map<string, Module>(),
} as { [K in ApplicationCommandType]: Map<string, Module> };
export const MessageCompCommandStore = {
[ComponentType.Button]: new Map<string, PluggedModule>(),
[ComponentType.SelectMenu]: new Map<string, PluggedModule>(),
[ComponentType.TextInput] : new Map<string, PluggedModule>()
[ComponentType.Button]: new Map<string, Module>(),
[ComponentType.SelectMenu]: new Map<string, Module>(),
[ComponentType.TextInput] : new Map<string, Module>()
};
export const TextCommandStore = {
text: new Map<string, PluggedModule>(),
aliases: new Map<string, PluggedModule>(),
text: new Map<string, Module>(),
aliases: new Map<string, Module>(),
};
// Courtesy @Townsy45
@@ -40,19 +40,19 @@ export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
/**
*
* @returns {Observable<{ mod: PluggedModule; absPath: string; }[]>} data from command files
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
* @param commandDir
*/
export function buildData(commandDir: string): Observable<{
plugged: PluggedModule;
mod: Module;
absPath: string;
}> {
return from(
getCommands(commandDir).map(absPath => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const plugged = <PluggedModule>require(absPath).module;
return { plugged, absPath };
const mod = <Module>require(absPath).module;
return { mod, absPath };
}),
);
}