feat: Separating events from command modules, leads separation of responsibility

This commit is contained in:
Jacob Nguyen
2022-06-24 21:33:04 -05:00
parent 27be769228
commit 29b0064329
6 changed files with 112 additions and 122 deletions

View File

@@ -1,12 +1,12 @@
import type { Message } from 'discord.js';
import { Observable, throwError } from 'rxjs';
import { SernError } from '../structures/errors';
import type { Module, ModuleDefs } from '../structures/module';
import type { Module, CommandModuleDefs } from '../structures/module';
import { correctModuleType } from '../utilities/predicates';
import type { Result } from 'ts-results';
export function filterCorrectModule<T extends keyof ModuleDefs>(cmdType: T) {
export function filterCorrectModule<T extends keyof CommandModuleDefs>(cmdType: T) {
return (src: Observable<Module | undefined>) =>
new Observable<ModuleDefs[T]>(subscriber => {
new Observable<CommandModuleDefs[T]>(subscriber => {
return src.subscribe({
next(mod) {
if (mod === undefined) {

View File

@@ -6,13 +6,13 @@ import type { Result } from 'ts-results';
import { Err, Ok } from 'ts-results';
import type { Awaitable } from 'discord.js';
import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { CommandModule, Module } from '../structures/module';
import type { CommandModule } from '../structures/module';
import { match } from 'ts-pattern';
import { SernError } from '../structures/errors';
import type { DefinedCommandModule, DefinedModule } from '../../types/handler';
import type { DefinedCommandModule } from '../../types/handler';
import { CommandType, PluginType } from '../structures/enums';
import { errTap } from './observableHandling';
import { processCommandPlugins$ } from './userDefinedEventsHandling';
import { processCommandPlugins } from './userDefinedEventsHandling';
export function onReady(wrapper: Wrapper) {
const { client, commands } = wrapper;
@@ -37,11 +37,8 @@ export function onReady(wrapper: Wrapper) {
);
const processPlugins$ = processCommandFiles$.pipe(
concatMap(mod => {
const cmdPluginRes = processCommandPlugins$(wrapper, mod);
if (cmdPluginRes.err) {
return cmdPluginRes.val;
}
return of({ mod, cmdPluginRes: cmdPluginRes.val });
const cmdPluginRes = processCommandPlugins(wrapper, mod);
return of({ mod, cmdPluginRes });
}),
);
@@ -84,9 +81,9 @@ export function onReady(wrapper: Wrapper) {
});
}
function registerModule(mod: DefinedModule): Result<void, void> {
function registerModule(mod: DefinedCommandModule): Result<void, void> {
const name = mod.name;
return match<Module>(mod)
return match<DefinedCommandModule>(mod)
.with({ type: CommandType.Text }, mod => {
mod.alias?.forEach(a => Files.TextCommands.aliases.set(a, mod));
Files.TextCommands.text.set(name, mod);

View File

@@ -1,76 +1,27 @@
import { CommandType } from '../structures/enums';
import { from, fromEvent, map, throwError } from 'rxjs';
import { SernError } from '../structures/errors';
import { from, fromEvent, map } from 'rxjs';
import * as Files from '../utilities/readFile';
import { buildData, ExternalEventEmitters } from '../utilities/readFile';
import { controller } from '../sern';
import type { DefinedEventModule, DefinedModule, SpreadParams } from '../../types/handler';
import type { DefinedCommandModule, DefinedEventModule, SpreadParams } from '../../types/handler';
import type { EventModule } from '../structures/module';
import type Wrapper from '../structures/wrapper';
import { basename } from 'path';
import { match, P } from 'ts-pattern';
import { isDiscordEvent, isSernEvent } from '../utilities/predicates';
import type { CommandPlugin } from '../plugins/plugin';
import { Err, Ok } from 'ts-results';
import { errTap } from './observableHandling';
/**
* Utility function to process command plugins for all Modules
* @param client
* @param sernEmitter
* @param mod
* @param absPath
*/
export function processCommandPlugins$<T extends DefinedModule>(
{ client, sernEmitter }: Wrapper,
mod: T,
) {
return match(mod as DefinedModule)
.with({ type: CommandType.External }, m =>
Ok(
m.plugins.map(plug => ({
...plug,
name: plug?.name ?? 'Unnamed Plugin',
description: plug?.description ?? '...',
execute: plug.execute(ExternalEventEmitters.get(m.emitter)!, m, controller),
})),
),
)
.with({ type: CommandType.Sern }, m =>
Ok(
m.plugins.map(plug => ({
...plug,
name: plug?.name ?? 'Unnamed Plugin',
description: plug?.description ?? '...',
execute: plug.execute(sernEmitter!, m, controller),
})),
),
)
.with(
{
type: P.not(CommandType.Autocomplete),
plugins: P.array({} as P.infer<CommandPlugin>),
},
m => {
return Ok(
m.plugins.map(plug => ({
...plug,
name: plug?.name ?? 'Unnamed Plugin',
description: plug?.description ?? '...',
execute: plug.execute(client, m, controller),
})),
);
},
)
.otherwise(() =>
Err(
throwError(
() =>
SernError.NonValidModuleType +
`. You cannot use command plugins and Autocomplete.`,
),
),
);
export function processCommandPlugins<T extends DefinedCommandModule>({ client }: Wrapper, mod: T) {
return mod.plugins.map(plug => ({
...plug,
name: plug?.name ?? 'Unnamed Plugin',
description: plug?.description ?? '...',
execute: plug.execute(client, mod, controller),
}));
}
export function processEvents(

View File

@@ -14,12 +14,18 @@
import type { Awaitable, Client } from 'discord.js';
import type { Err, Ok, Result } from 'ts-results';
import type { DefinitelyDefined, Module, Override } from '../..';
import { CommandType } from '../..';
import type { AutocompleteCommand, BaseModule, ModuleDefs } from '../structures/module';
import type { CommandType } from '../..';
import type {
BaseModule,
EventModule,
CommandModuleDefs,
CommandModule,
} from '../structures/module';
import { PluginType } from '../structures/enums';
import type { EventEmitter } from 'events';
import type { ExternalEventCommand, SernEventCommand } from '../structures/events';
import type SernEmitter from '../sernEmitter';
import type { AutocompleteInteraction } from 'discord.js';
export interface Controller {
next: () => Ok<void>;
@@ -33,14 +39,14 @@ type BasePlugin = Override<
}
>;
export type CommandPlugin<T extends keyof ModuleDefs = keyof ModuleDefs> = {
export type CommandPlugin<T extends keyof CommandModuleDefs = keyof CommandModuleDefs> = {
[K in T]: Override<
BasePlugin,
{
type: PluginType.Command;
execute: (
wrapper: Client,
module: DefinitelyDefined<ModuleDefs[T], 'name' | 'description'>,
module: DefinitelyDefined<CommandModuleDefs[T], 'name' | 'description'>,
controller: Controller,
) => Awaitable<Result<void, void>>;
}
@@ -71,13 +77,24 @@ export type SernEmitterPlugin = Override<
}
>;
export type EventPlugin<T extends keyof ModuleDefs = keyof ModuleDefs> = {
export type AutocompletePlugin = Override<
BaseModule,
{
type: PluginType.Event;
execute: (
autocmp: AutocompleteInteraction,
controlller: Controller,
) => Awaitable<void | unknown>;
}
>;
export type EventPlugin<T extends keyof CommandModuleDefs = keyof CommandModuleDefs> = {
[K in T]: Override<
BasePlugin,
{
type: PluginType.Event;
execute: (
event: Parameters<ModuleDefs[K]['execute']>,
event: Parameters<CommandModuleDefs[K]['execute']>,
controller: Controller,
) => Awaitable<Result<void, void>>;
}
@@ -92,7 +109,7 @@ export type EventPlugin<T extends keyof ModuleDefs = keyof ModuleDefs> = {
// }
export type ModuleNoPlugins = {
[T in keyof ModuleDefs]: Omit<ModuleDefs[T], 'plugins' | 'onEvent'>;
[T in keyof CommandModuleDefs]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent'>;
};
function isEventPlugin<T extends CommandType>(
@@ -107,31 +124,54 @@ function isCommandPlugin<T extends CommandType>(
}
//TODO: I WANT BETTER TYPINGS AHHHHHHHHHHHHHHH
// Maybe add overlaods
export function sernModule<T extends keyof ModuleDefs>(
export function sernModule<T extends CommandType.Slash = CommandType.Slash>(
plugin: (CommandPlugin<T> | EventPlugin<T>)[],
mod: ModuleNoPlugins[CommandType.Slash],
): Module;
export function sernModule<T extends CommandType.Text = CommandType.Text>(
plugin: (CommandPlugin<T> | EventPlugin<T>)[],
mod: ModuleNoPlugins[CommandType.Text],
): Module;
export function sernModule<T extends CommandType.Button = CommandType.Button>(
plugin: (CommandPlugin<T> | EventPlugin<T>)[],
mod: ModuleNoPlugins[CommandType.Button],
): Module;
export function sernModule<T extends CommandType.Both = CommandType.Both>(
plugin: (CommandPlugin<T> | EventPlugin<T>)[],
mod: ModuleNoPlugins[CommandType.Both],
): Module;
export function sernModule<T extends CommandType.MenuUser = CommandType.MenuUser>(
plugin: (CommandPlugin<T> | EventPlugin<T>)[],
mod: ModuleNoPlugins[CommandType.MenuMsg],
): Module;
export function sernModule<T extends CommandType.MenuSelect = CommandType.MenuSelect>(
plugin: (CommandPlugin<T> | EventPlugin<T>)[],
mod: ModuleNoPlugins[CommandType.MenuSelect],
): Module;
export function sernModule<T extends CommandType.Modal = CommandType.Modal>(
plugin: (CommandPlugin<T> | EventPlugin<T>)[],
mod: ModuleNoPlugins[CommandType.Modal],
): Module;
export function sernModule<T extends CommandType.MenuUser = CommandType.MenuUser>(
plugin: (CommandPlugin<T> | EventPlugin<T>)[],
mod: ModuleNoPlugins[CommandType.MenuUser],
): Module;
export function sernModule<T extends keyof CommandModuleDefs = keyof CommandModuleDefs>(
plugin: (CommandPlugin<T> | EventPlugin<T>)[],
mod: ModuleNoPlugins[T],
): Module {
): CommandModule {
const onEvent = plugin.filter(isEventPlugin);
const plugins = plugin.filter(isCommandPlugin);
if (mod.type === CommandType.Autocomplete) {
throw new Error(
'You cannot use this function declaration for Autocomplete Interactions! use the raw object for options or' +
'sernAutoComplete function',
);
} else
return {
onEvent,
plugins,
...mod,
} as Module;
}
export function sernAutocomplete(
onEvent: EventPlugin<CommandType.Autocomplete>[],
mod: Omit<AutocompleteCommand, 'type' | 'name' | 'description' | 'onEvent'>,
): Omit<AutocompleteCommand, 'type' | 'name' | 'description'> {
return {
onEvent,
plugins,
...mod,
};
} as CommandModule;
}
export function eventModule<T extends keyof EventModule>(): EventModule {
return {} as EventModule;
}

View File

@@ -18,9 +18,9 @@ import type {
UserContextMenuCommandInteraction,
} from 'discord.js';
import type { Args, Override, SlashOptions } from '../../types/handler';
import type { CommandPlugin, EventPlugin } from '../plugins/plugin';
import type { AutocompletePlugin, CommandPlugin, EventPlugin } from '../plugins/plugin';
import type Context from './context';
import { CommandType, PluginType } from './enums';
import { CommandType, EventType, PluginType } from './enums';
import type { DiscordEventCommand, ExternalEventCommand, SernEventCommand } from './events';
export interface BaseModule {
@@ -122,9 +122,10 @@ export type ModalSubmitCommand = Override<
export type AutocompleteCommand = Override<
BaseModule,
{
type: CommandType.Autocomplete;
name: string;
onEvent: EventPlugin<CommandType.Autocomplete>[];
name?: never;
description?: never;
type?: never;
onEvent: AutocompletePlugin[];
execute: (ctx: AutocompleteInteraction) => Awaitable<void | unknown>;
}
>;
@@ -137,14 +138,13 @@ export type CommandModule =
| ContextMenuMsg
| ButtonCommand
| SelectMenuCommand
| ModalSubmitCommand
| AutocompleteCommand;
| ModalSubmitCommand;
export type Module = CommandModule | EventModule;
//https://stackoverflow.com/questions/64092736/alternative-to-switch-statement-for-typescript-discriminated-union
// Explicit Module Definitions for mapping
export type ModuleDefs = {
export type CommandModuleDefs = {
[CommandType.Text]: TextCommand;
[CommandType.Slash]: SlashCommand;
[CommandType.Both]: BothCommand;
@@ -153,10 +153,12 @@ export type ModuleDefs = {
[CommandType.Button]: ButtonCommand;
[CommandType.MenuSelect]: SelectMenuCommand;
[CommandType.Modal]: ModalSubmitCommand;
[CommandType.Autocomplete]: AutocompleteCommand;
[CommandType.Sern]: SernEventCommand;
[CommandType.Discord]: DiscordEventCommand;
[CommandType.External]: ExternalEventCommand;
};
export type EventModuleDefs = {
[EventType.Sern]: SernEventCommand;
[EventType.Discord]: DiscordEventCommand;
[EventType.External]: ExternalEventCommand;
};
//TODO: support deeply nested Autocomplete
@@ -171,7 +173,7 @@ export type SernAutocompleteData = Override<
| ApplicationCommandOptionType.String
| ApplicationCommandOptionType.Number
| ApplicationCommandOptionType.Integer;
command: Omit<AutocompleteCommand, 'type' | 'name' | 'description'>;
command: AutocompleteCommand;
}
>;

View File

@@ -1,4 +1,4 @@
import type { EventModule, Module, ModuleDefs } from '../structures/module';
import type { CommandModuleDefs, EventModule, Module } from '../structures/module';
import type {
Awaitable,
ButtonInteraction,
@@ -9,23 +9,23 @@ import type {
SelectMenuInteraction,
UserContextMenuCommandInteraction,
} from 'discord.js';
import type {
DiscordEventCommand,
ExternalEventCommand,
SernEventCommand,
} from '../structures/events';
import { CommandType } from '../..';
import {
AutocompleteInteraction,
Interaction,
InteractionType,
ModalSubmitInteraction,
} from 'discord.js';
import type {
DiscordEventCommand,
ExternalEventCommand,
SernEventCommand,
} from '../structures/events';
import { EventType } from '../..';
export function correctModuleType<T extends keyof ModuleDefs>(
export function correctModuleType<T extends keyof CommandModuleDefs>(
plug: Module | undefined,
type: T,
): plug is ModuleDefs[T] {
): plug is CommandModuleDefs[T] {
// Another way to check if type is equivalent,
// It will check based on flag system instead
return plug !== undefined && (plug.type & type) !== 0;
@@ -78,7 +78,7 @@ export function isPromise<T>(promiseLike: Awaitable<T>): promiseLike is PromiseL
}
export function isDiscordEvent(el: EventModule): el is DiscordEventCommand {
return el.type === CommandType.Discord;
return el.type === EventType.Discord;
}
export function isSernEvent(el: EventModule): el is SernEventCommand {
return !isDiscordEvent(el);
@@ -89,5 +89,5 @@ export function isExternalEvent(el: EventModule): el is ExternalEventCommand {
}
export function isEventModule(module: Module): module is EventModule {
return [CommandType.Discord, CommandType.Sern, CommandType.External].includes(module.type);
return [EventType.Sern, EventType.Discord, EventType.External].includes(module.type);
}