refactor(events): use of classes for scalability & maintainability (#83)

Co-authored-by: EvolutionX-10 <evolutionx9777@gmail.com>
This commit is contained in:
Jacob Nguyen
2022-07-16 13:08:11 -05:00
committed by GitHub
parent 79be5096d3
commit 17eb816ec9
17 changed files with 628 additions and 565 deletions

88
package-lock.json generated
View File

@@ -9,8 +9,8 @@
"version": "1.1.0-beta",
"license": "MIT",
"dependencies": {
"discord.js": "^14.0.0-dev.1647259751.2297c2b",
"rxjs": "^7.5.5",
"discord.js": "^14.0.0-dev.1657757514-fe34f48",
"rxjs": "^7.5.6",
"ts-pattern": "^4.0.2",
"ts-results": "^3.3.0"
},
@@ -303,9 +303,9 @@
}
},
"node_modules/@discordjs/builders": {
"version": "0.16.0-dev.1657411897-f0b68d5",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.16.0-dev.1657411897-f0b68d5.tgz",
"integrity": "sha512-QUiAx+IVQBO3qES2E/O5xsuGtkFCVk3GkH//r3bbpHiONipJlGEk9FSmHSmBl3nWGy3gku1JgG4AgyVa5l/+OA==",
"version": "0.16.0-dev.1657757509-fe34f48",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.16.0-dev.1657757509-fe34f48.tgz",
"integrity": "sha512-pxaG7OHRqsnd8MPhfVYQUirrvVmMWxwjSL0j4adHz1w7gWUtgchxOVcOCLgWdFDCoN4bNwRjkstNcI+yLpNE7w==",
"dependencies": {
"@sapphire/shapeshift": "^3.4.1",
"discord-api-types": "^0.36.1",
@@ -318,17 +318,17 @@
}
},
"node_modules/@discordjs/collection": {
"version": "0.8.0-dev.1657411895-f0b68d5",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.8.0-dev.1657411895-f0b68d5.tgz",
"integrity": "sha512-CirWYerl4tgKJ+GWiD7Sg8JAw3QKm0bisGk5Mr8yn9GVZeq5R84yNJmUqN6rhg0UIgP8ERSrJeOqdJqDKVWxVw==",
"version": "0.8.0-dev.1657757511-fe34f48",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.8.0-dev.1657757511-fe34f48.tgz",
"integrity": "sha512-r4ixU8TXVMb3D9TvlVISrrwrV+uxJJBIKuZWI5AJr1M1rJdt41lFtD4ZsO3SuyE8pBo1eU/XSTc6lsTmNMhy1A==",
"engines": {
"node": ">=16.9.0"
}
},
"node_modules/@discordjs/rest": {
"version": "0.6.0-dev.1657411905-f0b68d5",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-0.6.0-dev.1657411905-f0b68d5.tgz",
"integrity": "sha512-3Mwo9mWy6wAfyzNiiXCTQ/zwOHvMSym9WfJHSoa1Yi+gJ7tLM0YPQJGQTHXZvr0WICH6wUWasbWBE0dMiz28Ig==",
"version": "0.6.0-dev.1657757537-fe34f48",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-0.6.0-dev.1657757537-fe34f48.tgz",
"integrity": "sha512-jJA5tedKJiVLmieAvBffBNM5409ZOPxFi3A+6KMfmxBYgI5N6lfDIcRgd+oosi1l0t37lyxozofwdivS4aHNGA==",
"dependencies": {
"@discordjs/collection": "^0.8.0-dev",
"@sapphire/async-queue": "^1.3.2",
@@ -1614,14 +1614,14 @@
}
},
"node_modules/discord-api-types": {
"version": "0.36.1",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.36.1.tgz",
"integrity": "sha512-PTDinUU574hXA9Ko9wrftL1iww1raNiRVKjuPIWQ5Li1g7vQPArpZWw9x01kh/IXLPdzSAJ6H8T0eAYzxzFzIg=="
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.36.2.tgz",
"integrity": "sha512-TunPAvzwneK/m5fr4hxH3bMsrtI22nr9yjfHyo5NBGMjpsAauGNiGCmwoFf0oO3jSd2mZiKUvZwCKDaB166u2Q=="
},
"node_modules/discord.js": {
"version": "14.0.0-dev.1657411900-f0b68d5",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.0.0-dev.1657411900-f0b68d5.tgz",
"integrity": "sha512-mPeIaxthGZEc4qKi6HzWnMIbHOh63qJHr37qFWMoDZpYOPht7Q1V2w3eGh7e30kCoh1BNrLGaeYqwRxpYENjsg==",
"version": "14.0.0-dev.1657757514-fe34f48",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.0.0-dev.1657757514-fe34f48.tgz",
"integrity": "sha512-warixUTyz+NIyoTQht21Seo1bC1UPSRw3/b/uUtPB/BGu4e1X1orwcEP1wepQuwyqLIf8yo0vRJo21vRvwILvg==",
"dependencies": {
"@discordjs/builders": "^0.16.0-dev",
"@discordjs/collection": "^0.8.0-dev",
@@ -4021,9 +4021,9 @@
}
},
"node_modules/rxjs": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz",
"integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==",
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz",
"integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==",
"dependencies": {
"tslib": "^2.1.0"
}
@@ -4530,9 +4530,9 @@
}
},
"node_modules/undici": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.6.1.tgz",
"integrity": "sha512-yYVqywdCbNb99f/w045wqmv++WExXDjY0FEvLSB7QUZZH6izxrVkF4dJn1aimcvN0+WAhv75Gg7v6VJoqmRtJQ==",
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.7.0.tgz",
"integrity": "sha512-ORgxwDkiPS+gK2VxE7iyVeR7JliVn5DqhZ4LgQqYLBXsuK+lwOEmnJ66dhvlpLM0tC3fC7eYF1Bti2frbw2eAA==",
"engines": {
"node": ">=12.18"
}
@@ -5035,9 +5035,9 @@
}
},
"@discordjs/builders": {
"version": "0.16.0-dev.1657411897-f0b68d5",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.16.0-dev.1657411897-f0b68d5.tgz",
"integrity": "sha512-QUiAx+IVQBO3qES2E/O5xsuGtkFCVk3GkH//r3bbpHiONipJlGEk9FSmHSmBl3nWGy3gku1JgG4AgyVa5l/+OA==",
"version": "0.16.0-dev.1657757509-fe34f48",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.16.0-dev.1657757509-fe34f48.tgz",
"integrity": "sha512-pxaG7OHRqsnd8MPhfVYQUirrvVmMWxwjSL0j4adHz1w7gWUtgchxOVcOCLgWdFDCoN4bNwRjkstNcI+yLpNE7w==",
"requires": {
"@sapphire/shapeshift": "^3.4.1",
"discord-api-types": "^0.36.1",
@@ -5047,14 +5047,14 @@
}
},
"@discordjs/collection": {
"version": "0.8.0-dev.1657411895-f0b68d5",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.8.0-dev.1657411895-f0b68d5.tgz",
"integrity": "sha512-CirWYerl4tgKJ+GWiD7Sg8JAw3QKm0bisGk5Mr8yn9GVZeq5R84yNJmUqN6rhg0UIgP8ERSrJeOqdJqDKVWxVw=="
"version": "0.8.0-dev.1657757511-fe34f48",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.8.0-dev.1657757511-fe34f48.tgz",
"integrity": "sha512-r4ixU8TXVMb3D9TvlVISrrwrV+uxJJBIKuZWI5AJr1M1rJdt41lFtD4ZsO3SuyE8pBo1eU/XSTc6lsTmNMhy1A=="
},
"@discordjs/rest": {
"version": "0.6.0-dev.1657411905-f0b68d5",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-0.6.0-dev.1657411905-f0b68d5.tgz",
"integrity": "sha512-3Mwo9mWy6wAfyzNiiXCTQ/zwOHvMSym9WfJHSoa1Yi+gJ7tLM0YPQJGQTHXZvr0WICH6wUWasbWBE0dMiz28Ig==",
"version": "0.6.0-dev.1657757537-fe34f48",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-0.6.0-dev.1657757537-fe34f48.tgz",
"integrity": "sha512-jJA5tedKJiVLmieAvBffBNM5409ZOPxFi3A+6KMfmxBYgI5N6lfDIcRgd+oosi1l0t37lyxozofwdivS4aHNGA==",
"requires": {
"@discordjs/collection": "^0.8.0-dev",
"@sapphire/async-queue": "^1.3.2",
@@ -6021,14 +6021,14 @@
}
},
"discord-api-types": {
"version": "0.36.1",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.36.1.tgz",
"integrity": "sha512-PTDinUU574hXA9Ko9wrftL1iww1raNiRVKjuPIWQ5Li1g7vQPArpZWw9x01kh/IXLPdzSAJ6H8T0eAYzxzFzIg=="
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.36.2.tgz",
"integrity": "sha512-TunPAvzwneK/m5fr4hxH3bMsrtI22nr9yjfHyo5NBGMjpsAauGNiGCmwoFf0oO3jSd2mZiKUvZwCKDaB166u2Q=="
},
"discord.js": {
"version": "14.0.0-dev.1657411900-f0b68d5",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.0.0-dev.1657411900-f0b68d5.tgz",
"integrity": "sha512-mPeIaxthGZEc4qKi6HzWnMIbHOh63qJHr37qFWMoDZpYOPht7Q1V2w3eGh7e30kCoh1BNrLGaeYqwRxpYENjsg==",
"version": "14.0.0-dev.1657757514-fe34f48",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.0.0-dev.1657757514-fe34f48.tgz",
"integrity": "sha512-warixUTyz+NIyoTQht21Seo1bC1UPSRw3/b/uUtPB/BGu4e1X1orwcEP1wepQuwyqLIf8yo0vRJo21vRvwILvg==",
"requires": {
"@discordjs/builders": "^0.16.0-dev",
"@discordjs/collection": "^0.8.0-dev",
@@ -7853,9 +7853,9 @@
}
},
"rxjs": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz",
"integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==",
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz",
"integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==",
"requires": {
"tslib": "^2.1.0"
}
@@ -8229,9 +8229,9 @@
"optional": true
},
"undici": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.6.1.tgz",
"integrity": "sha512-yYVqywdCbNb99f/w045wqmv++WExXDjY0FEvLSB7QUZZH6izxrVkF4dJn1aimcvN0+WAhv75Gg7v6VJoqmRtJQ=="
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.7.0.tgz",
"integrity": "sha512-ORgxwDkiPS+gK2VxE7iyVeR7JliVn5DqhZ4LgQqYLBXsuK+lwOEmnJ66dhvlpLM0tC3fC7eYF1Bti2frbw2eAA=="
},
"universalify": {
"version": "0.1.2",

View File

@@ -7,9 +7,7 @@
"compile": "tsc",
"watch": "tsc -w",
"lint": "eslint src/**/*.ts",
"format": "eslint src/**/*.ts --fix",
"release": "standard-version && git push --follow-tags",
"commit": "cz"
"format": "eslint src/**/*.ts --fix"
},
"keywords": [
"sern-handler",
@@ -23,8 +21,8 @@
"author": "SernDevs",
"license": "MIT",
"dependencies": {
"discord.js": "^14.0.0-dev.1647259751.2297c2b",
"rxjs": "^7.5.5",
"discord.js": "^14.0.0-dev.1657757514-fe34f48",
"rxjs": "^7.5.6",
"ts-pattern": "^4.0.2",
"ts-results": "^3.3.0"
},
@@ -35,7 +33,7 @@
"prettier": "2.7.1",
"typescript": "4.7.4"
},
"repository": {
"repository": {
"type": "git",
"url": "git+https://github.com/sern-handler/handler.git"
},

View File

@@ -0,0 +1,110 @@
import type {
BothCommand,
ButtonCommand,
ContextMenuMsg,
ContextMenuUser,
ModalSubmitCommand,
SelectMenuCommand,
SlashCommand,
} from '../structures/module';
import Context from '../structures/context';
import type { SlashOptions } from '../../types/handler';
import { asyncResolveArray } from '../utilities/asyncResolveArray';
import { controller } from '../sern';
import type {
ButtonInteraction,
ModalSubmitInteraction,
SelectMenuInteraction,
AutocompleteInteraction,
ChatInputCommandInteraction,
Interaction,
UserContextMenuCommandInteraction,
MessageContextMenuCommandInteraction,
} from 'discord.js';
import { isAutocomplete } from '../utilities/predicates';
import { SernError } from '../structures/errors';
export function applicationCommandDispatcher(interaction: Interaction) {
if (isAutocomplete(interaction)) {
return dispatchAutocomplete(interaction);
} else {
const ctx = Context.wrap(interaction as ChatInputCommandInteraction);
const args: ['slash', SlashOptions] = ['slash', ctx.interaction.options];
return (mod: BothCommand | SlashCommand) => ({
mod,
execute: () => mod.execute(ctx, args),
eventPluginRes: asyncResolveArray(
mod.onEvent.map(plugs => plugs.execute([ctx, args], controller)),
),
});
}
}
export function dispatchAutocomplete(interaction: AutocompleteInteraction) {
const choice = interaction.options.getFocused(true);
return (mod: BothCommand | SlashCommand) => {
const selectedOption = mod.options?.find(o => o.autocomplete && o.name === choice.name);
if (selectedOption !== undefined && selectedOption.autocomplete) {
return {
mod,
execute: () => selectedOption.command.execute(interaction),
eventPluginRes: asyncResolveArray(
selectedOption.command.onEvent.map(e => e.execute(interaction, controller)),
),
};
}
throw Error(
SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`,
);
};
}
export function modalCommandDispatcher(interaction: ModalSubmitInteraction) {
return (mod: ModalSubmitCommand) => ({
mod,
execute: () => mod.execute(interaction),
eventPluginRes: asyncResolveArray(
mod.onEvent.map(plugs => plugs.execute([interaction], controller)),
),
});
}
export function buttonCommandDispatcher(interaction: ButtonInteraction) {
return (mod: ButtonCommand) => ({
mod,
execute: () => mod.execute(interaction),
eventPluginRes: asyncResolveArray(
mod.onEvent.map(plugs => plugs.execute([interaction], controller)),
),
});
}
export function selectMenuCommandDispatcher(interaction: SelectMenuInteraction) {
return (mod: SelectMenuCommand) => ({
mod,
execute: () => mod.execute(interaction),
eventPluginRes: asyncResolveArray(
mod.onEvent.map(plugs => plugs.execute([interaction], controller)),
),
});
}
export function ctxMenuUserDispatcher(interaction: UserContextMenuCommandInteraction) {
return (mod: ContextMenuUser) => ({
mod,
execute: () => mod.execute(interaction),
eventPluginRes: asyncResolveArray(
mod.onEvent.map(plugs => plugs.execute([interaction], controller)),
),
});
}
export function ctxMenuMsgDispatcher(interaction: MessageContextMenuCommandInteraction) {
return (mod: ContextMenuMsg) => ({
mod,
execute: () => mod.execute(interaction),
eventPluginRes: asyncResolveArray(
mod.onEvent.map(plugs => plugs.execute([interaction], controller)),
),
});
}

View File

@@ -0,0 +1,10 @@
import type Wrapper from '../structures/wrapper';
import { Subject, type Observable } from 'rxjs';
export abstract class EventsHandler<T> {
protected payloadSubject = new Subject<T>();
protected abstract discordEvent: Observable<unknown>;
protected constructor(protected wrapper: Wrapper) {}
protected abstract init(): void;
protected abstract setState(state: T): void;
}

View File

@@ -1,233 +0,0 @@
import type {
CommandInteraction,
Interaction,
MessageComponentInteraction,
ModalSubmitInteraction,
SelectMenuInteraction,
} from 'discord.js';
import { concatMap, fromEvent, map, Observable, of, throwError } from 'rxjs';
import type Wrapper from '../structures/wrapper';
import * as Files from '../utilities/readFile';
import { match } from 'ts-pattern';
import { SernError } from '../structures/errors';
import Context from '../structures/context';
import { controller } from '../sern';
import type { Module } from '../structures/module';
import { PayloadType } from '../structures/enums';
import {
isApplicationCommand,
isAutocomplete,
isButton,
isChatInputCommand,
isMessageComponent,
isMessageCtxMenuCmd,
isModalSubmit,
isSelectMenu,
isUserContextMenuCmd,
} from '../utilities/predicates';
import { filterCorrectModule } from './observableHandling';
import { CommandType } from '../structures/enums';
import type { AutocompleteInteraction } from 'discord.js';
import { asyncResolveArray } from '../utilities/asyncResolveArray';
function applicationCommandHandler(mod: Module | undefined, interaction: CommandInteraction) {
const mod$ = <T extends CommandType>(cmdTy: T) => of(mod).pipe(filterCorrectModule(cmdTy));
return (
match(interaction)
.when(isChatInputCommand, i => {
const ctx = Context.wrap(i);
return mod$(CommandType.Slash).pipe(
concatMap(m => {
return of(
m.onEvent.map(e => e.execute([ctx, ['slash', i.options]], controller)),
).pipe(
map(res => ({
mod,
res,
execute() {
return m.execute(ctx, ['slash', i.options]);
},
})),
);
}),
);
})
//Todo: refactor so that we dont have to have two separate branches. They're near identical!!
//Only thing that differs is type of interaction
.when(isMessageCtxMenuCmd, ctx => {
return mod$(CommandType.MenuMsg).pipe(
concatMap(m => {
return of(m.onEvent.map(e => e.execute([ctx], controller))).pipe(
map(res => ({
mod,
res,
execute() {
return m.execute(ctx);
},
})),
);
}),
);
})
.when(isUserContextMenuCmd, ctx => {
return mod$(CommandType.MenuUser).pipe(
concatMap(m => {
return of(m.onEvent.map(e => e.execute([ctx], controller))).pipe(
map(res => ({
mod,
res,
execute() {
return m.execute(ctx);
},
})),
);
}),
);
})
.run()
);
}
function messageComponentInteractionHandler(
mod: Module | undefined,
interaction: MessageComponentInteraction,
) {
const mod$ = <T extends CommandType>(ty: T) => of(mod).pipe(filterCorrectModule(ty));
//Todo: refactor so that we dont have to have two separate branches. They're near identical!!
//Only thing that differs is type of interaction
return match(interaction)
.when(isButton, ctx => {
return mod$(CommandType.Button).pipe(
concatMap(m => {
return of(m.onEvent.map(e => e.execute([ctx], controller))).pipe(
map(res => ({
mod,
res,
execute() {
return m.execute(ctx);
},
})),
);
}),
);
})
.when(isSelectMenu, (ctx: SelectMenuInteraction) => {
return mod$(CommandType.MenuSelect).pipe(
concatMap(m => {
return of(m.onEvent.map(e => e.execute([ctx], controller))).pipe(
map(res => ({
mod,
res,
execute() {
return m.execute(ctx);
},
})),
);
}),
);
})
.otherwise(() => throwError(() => SernError.NotSupportedInteraction));
}
function modalHandler(modul: Module | undefined, ctx: ModalSubmitInteraction) {
return of(modul).pipe(
filterCorrectModule(CommandType.Modal),
concatMap(mod => {
return of(mod.onEvent.map(e => e.execute([ctx], controller))).pipe(
map(res => ({
mod,
res,
execute() {
return mod.execute(ctx);
},
})),
);
}),
);
}
function autoCmpHandler(mod: Module | undefined, interaction: AutocompleteInteraction) {
return of(mod).pipe(
filterCorrectModule(CommandType.Slash),
concatMap(mod => {
const choice = interaction.options.getFocused(true);
const selectedOption = mod.options?.find(o => o.autocomplete && o.name === choice.name);
if (selectedOption !== undefined && selectedOption.autocomplete) {
return of(
selectedOption.command.onEvent.map(e => e.execute(interaction, controller)),
).pipe(
map(res => ({
mod,
res,
execute() {
return selectedOption.command.execute(interaction);
},
})),
);
}
return throwError(
() =>
SernError.NotSupportedInteraction +
` There is probably no autocomplete tag for this option`,
);
}),
);
}
export function onInteractionCreate(wrapper: Wrapper) {
const { client } = wrapper;
const interactionEvent$ = <Observable<Interaction>>fromEvent(client, 'interactionCreate');
interactionEvent$
.pipe(
/*processing plugins*/
concatMap(interaction => {
if (isApplicationCommand(interaction)) {
const modul =
Files.ApplicationCommands[interaction.commandType].get(
interaction.commandName,
) ?? Files.BothCommands.get(interaction.commandName);
return applicationCommandHandler(modul, interaction);
}
if (isMessageComponent(interaction)) {
const modul = Files.MessageCompCommands[interaction.componentType].get(
interaction.customId,
);
return messageComponentInteractionHandler(modul, interaction);
}
if (isModalSubmit(interaction)) {
const modul = Files.ModalSubmitCommands.get(interaction.customId);
return modalHandler(modul, interaction);
}
if (isAutocomplete(interaction)) {
const modul =
Files.ApplicationCommands['1'].get(interaction.commandName) ??
Files.BothCommands.get(interaction.commandName);
return autoCmpHandler(modul, interaction);
}
return throwError(() => SernError.NotSupportedInteraction);
}),
)
.subscribe({
async next({ mod, res: eventPluginRes, execute }) {
const ePlugArr = await asyncResolveArray(eventPluginRes);
if (ePlugArr.every(e => e.ok)) {
await execute();
wrapper.sernEmitter?.emit('module.activate', {
type: PayloadType.Success,
module: mod!,
});
} else {
wrapper.sernEmitter?.emit('module.activate', {
type: PayloadType.Failure,
module: mod!,
reason: SernError.PluginFailure,
});
}
},
error(err) {
wrapper.sernEmitter?.emit('error', err);
},
});
}

View File

@@ -0,0 +1,129 @@
import type { Interaction } from 'discord.js';
import { concatMap, from, fromEvent, map, Observable } from 'rxjs';
import type Wrapper from '../structures/wrapper';
import { EventsHandler } from './eventsHandler';
import {
isApplicationCommand,
isAutocomplete,
isMessageComponent,
isModalSubmit,
} from '../utilities/predicates';
import * as Files from '../utilities/readFile';
import type { CommandModule } from '../structures/module';
import { SernError } from '../structures/errors';
import { CommandType } from '../structures/enums';
import { match, P } from 'ts-pattern';
import {
applicationCommandDispatcher,
buttonCommandDispatcher,
ctxMenuMsgDispatcher,
ctxMenuUserDispatcher,
modalCommandDispatcher,
selectMenuCommandDispatcher,
} from './dispatchers';
import type {
ButtonInteraction,
ModalSubmitInteraction,
SelectMenuInteraction,
UserContextMenuCommandInteraction,
MessageContextMenuCommandInteraction,
} from 'discord.js';
import { executeModule } from './observableHandling';
export default class InteractionHandler extends EventsHandler<{
event: Interaction;
mod: CommandModule;
}> {
protected override discordEvent: Observable<Interaction>;
constructor(protected wrapper: Wrapper) {
super(wrapper);
this.discordEvent = <Observable<Interaction>>fromEvent(wrapper.client, 'interactionCreate');
this.init();
this.payloadSubject
.pipe(
map(this.processModules),
concatMap(({ mod, execute, eventPluginRes }) => {
//resolve all the Results from event plugins
return from(eventPluginRes).pipe(map(res => ({ mod, res, execute })));
}),
concatMap(payload => executeModule(wrapper, payload)),
)
.subscribe({
error: err => {
wrapper.sernEmitter?.emit('error', err);
},
});
}
override init() {
this.discordEvent.subscribe({
next: interaction => {
if (isMessageComponent(interaction)) {
const mod = Files.MessageCompCommands[interaction.componentType].get(
interaction.customId,
);
this.setState({ event: interaction, mod });
} else if (isApplicationCommand(interaction) || isAutocomplete(interaction)) {
const mod =
Files.ApplicationCommands[interaction.commandType].get(
interaction.commandName,
) ?? Files.BothCommands.get(interaction.commandName);
this.setState({ event: interaction, mod });
} else if (isModalSubmit(interaction)) {
/**
* maybe move modal submits into message component object maps?
*/
const mod = Files.ModalSubmitCommands.get(interaction.customId);
this.setState({ event: interaction, mod });
} else {
throw Error('This interaction is not supported yet');
}
},
error: e => {
this.wrapper.sernEmitter?.emit('error', e);
},
});
}
protected setState(state: { event: Interaction; mod: CommandModule | undefined }): void {
if (state.mod === undefined) {
this.payloadSubject.error(SernError.UndefinedModule);
} else {
//if statement above checks already, safe cast
this.payloadSubject.next(state as { event: Interaction; mod: CommandModule });
}
}
protected processModules(payload: { event: Interaction; mod: CommandModule }) {
return match(payload.mod)
.with(
{ type: P.union(CommandType.Slash, CommandType.Both) },
applicationCommandDispatcher(payload.event),
)
.with(
{ type: CommandType.Modal },
modalCommandDispatcher(payload.event as ModalSubmitInteraction),
)
.with(
{ type: CommandType.Button },
buttonCommandDispatcher(payload.event as ButtonInteraction),
)
.with(
{ type: CommandType.MenuSelect },
selectMenuCommandDispatcher(payload.event as SelectMenuInteraction),
)
.with(
{ type: CommandType.MenuUser },
ctxMenuUserDispatcher(payload.event as UserContextMenuCommandInteraction),
)
.with(
{ type: CommandType.MenuMsg },
ctxMenuMsgDispatcher(payload.event as MessageContextMenuCommandInteraction),
)
.otherwise(() => {
throw Error(SernError.MismatchModule);
});
}
}

View File

@@ -1,74 +0,0 @@
import type { Message } from 'discord.js';
import { concatMap, from, fromEvent, map, Observable, of } from 'rxjs';
import { controller } from '../sern';
import Context from '../structures/context';
import type Wrapper from '../structures/wrapper';
import { fmt } from '../utilities/messageHelpers';
import * as Files from '../utilities/readFile';
import { filterCorrectModule, ignoreNonBot } from './observableHandling';
import { CommandType, PayloadType } from '../structures/enums';
import { SernError } from '../structures/errors';
import { asyncResolveArray } from '../utilities/asyncResolveArray';
export const onMessageCreate = (wrapper: Wrapper) => {
const { client, defaultPrefix } = wrapper;
if (defaultPrefix === undefined) return;
const messageEvent$ = <Observable<Message>>fromEvent(client, 'messageCreate');
const processMessage$ = messageEvent$.pipe(
ignoreNonBot(defaultPrefix),
map(message => {
const [prefix, ...rest] = fmt(message, defaultPrefix);
return {
ctx: Context.wrap(message),
args: <['text', string[]]>['text', rest],
mod:
Files.TextCommands.text.get(prefix) ??
Files.BothCommands.get(prefix) ??
Files.TextCommands.aliases.get(prefix),
};
}),
);
const ensureModuleType$ = processMessage$.pipe(
concatMap(payload =>
of(payload.mod).pipe(
filterCorrectModule(CommandType.Text),
map(mod => ({ ...payload, mod })),
),
),
);
const processEventPlugins$ = ensureModuleType$.pipe(
concatMap(({ ctx, args, mod }) => {
const res = asyncResolveArray(
mod.onEvent.map(ePlug => {
return ePlug.execute([ctx, args], controller);
}),
);
return from(res).pipe(map(res => ({ mod, ctx, args, res })));
}),
);
processEventPlugins$.subscribe({
next({ mod, ctx, args, res }) {
if (res.every(pl => pl.ok)) {
Promise.resolve(mod.execute(ctx, args)).then(() => {
wrapper.sernEmitter?.emit('module.activate', {
type: PayloadType.Success,
module: mod!,
});
});
} else {
wrapper.sernEmitter?.emit('module.activate', {
type: PayloadType.Failure,
module: mod!,
reason: SernError.PluginFailure,
});
}
},
error(e) {
wrapper.sernEmitter?.emit('error', e);
},
});
};

View File

@@ -0,0 +1,80 @@
import { EventsHandler } from './eventsHandler';
import { concatMap, from, fromEvent, map, Observable, of, switchMap } from 'rxjs';
import type Wrapper from '../structures/wrapper';
import type { Message } from 'discord.js';
import { executeModule, ignoreNonBot, isOneOfCorrectModules } from './observableHandling';
import { fmt } from '../utilities/messageHelpers';
import Context from '../structures/context';
import * as Files from '../utilities/readFile';
import type { TextCommand } from '../structures/module';
import { CommandType } from '../structures/enums';
import { asyncResolveArray } from '../utilities/asyncResolveArray';
import { controller } from '../sern';
export default class MessageHandler extends EventsHandler<{
ctx: Context;
args: ['text', string[]];
mod: TextCommand;
}> {
protected discordEvent: Observable<Message>;
public constructor(wrapper: Wrapper) {
super(wrapper);
this.discordEvent = <Observable<Message>>fromEvent(wrapper.client, 'messageCreate');
this.init();
this.payloadSubject
.pipe(
switchMap(({ mod, ctx, args }) => {
const res = asyncResolveArray(
mod.onEvent.map(ePlug => {
return ePlug.execute([ctx, args], controller);
}),
);
const execute = () => {
return mod.execute(ctx, args);
};
//resolves the promise and re-emits it back into source
return from(res).pipe(map(res => ({ mod, execute, res })));
}),
concatMap(payload => executeModule(wrapper, payload)),
)
.subscribe({
error: err => {
wrapper.sernEmitter?.emit('error', err);
},
});
}
protected init(): void {
if (this.wrapper.defaultPrefix === undefined) return; //for now, just ignore if prefix doesn't exist
const { defaultPrefix } = this.wrapper;
this.discordEvent
.pipe(
ignoreNonBot(this.wrapper.defaultPrefix),
map(message => {
const [prefix, ...rest] = fmt(message, defaultPrefix);
return {
ctx: Context.wrap(message),
args: <['text', string[]]>['text', rest],
mod:
Files.TextCommands.text.get(prefix) ??
Files.BothCommands.get(prefix) ??
Files.TextCommands.aliases.get(prefix),
};
}),
concatMap(element => {
return of(element.mod).pipe(
isOneOfCorrectModules(CommandType.Text),
map(mod => ({ ...element, mod })),
);
}),
)
.subscribe({
next: value => this.setState(value),
error: err => this.wrapper.sernEmitter?.emit('error', err),
});
}
protected setState(state: { ctx: Context; args: ['text', string[]]; mod: TextCommand }) {
this.payloadSubject.next(state);
}
}

View File

@@ -1,9 +1,13 @@
import type { Message } from 'discord.js';
import { Observable, throwError } from 'rxjs';
import { from, Observable, of, tap, throwError } from 'rxjs';
import { SernError } from '../structures/errors';
import type { Module, CommandModuleDefs } from '../structures/module';
import type { Module, CommandModuleDefs, CommandModule } from '../structures/module';
import { correctModuleType } from '../utilities/predicates';
import type { Result } from 'ts-results';
import type { CommandType } from '../structures/enums';
import type Wrapper from '../structures/wrapper';
import { PayloadType } from '../structures/enums';
export function filterCorrectModule<T extends keyof CommandModuleDefs>(cmdType: T) {
return (src: Observable<Module | undefined>) =>
new Observable<CommandModuleDefs[T]>(subscriber => {
@@ -64,3 +68,52 @@ export function errTap<T extends Module>(cb: (err: SernError) => void) {
});
});
}
//POG
export function isOneOfCorrectModules<T extends readonly CommandType[]>(...inputs: [...T]) {
return (src: Observable<CommandModule | undefined>) => {
return new Observable<CommandModuleDefs[T[number]]>(subscriber => {
return src.subscribe({
next(mod) {
if (mod === undefined) {
return throwError(() => SernError.UndefinedModule);
}
if (inputs.some(type => (mod.type & type) !== 0)) {
subscriber.next(mod as CommandModuleDefs[T[number]]);
} else {
return throwError(() => SernError.MismatchModule);
}
},
error: e => subscriber.error(e),
complete: () => subscriber.complete(),
});
});
};
}
export function executeModule(
wrapper: Wrapper,
payload: {
mod: CommandModule;
execute: () => unknown;
res: Result<void, void>[];
},
) {
if (payload.res.every(el => el.ok)) {
return from(payload.execute() as Promise<unknown>).pipe(
tap(() => {
wrapper.sernEmitter?.emit('module.activate', {
type: PayloadType.Success,
module: payload.mod,
});
}),
);
} else {
wrapper.sernEmitter?.emit('module.activate', {
type: PayloadType.Failure,
module: payload.mod,
reason: SernError.PluginFailure,
});
return of(null);
}
}

View File

@@ -1,128 +0,0 @@
import { concat, concatMap, from, fromEvent, map, Observable, of, skip, take } from 'rxjs';
import { basename } from 'path';
import * as Files from '../utilities/readFile';
import type Wrapper from '../structures/wrapper';
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 } from '../structures/module';
import { match } from 'ts-pattern';
import { SernError } from '../structures/errors';
import type { DefinedCommandModule } from '../../types/handler';
import { CommandType, PluginType, PayloadType } from '../structures/enums';
import { errTap } from './observableHandling';
import { processCommandPlugins } from './userDefinedEventsHandling';
export function onReady(wrapper: Wrapper) {
const { client, commands } = wrapper;
const ready$ = fromEvent(client, 'ready').pipe(take(1), skip(1));
// Using sernModule function already checks if module is not EventModule
const processCommandFiles$ = Files.buildData<CommandModule>(commands).pipe(
errTap(reason => {
wrapper.sernEmitter?.emit('module.register', {
type: PayloadType.Failure,
module: undefined,
reason,
});
}),
map(({ mod, absPath }) => {
return {
absPath,
mod: <DefinedCommandModule>{
name: mod?.name ?? Files.fmtFileName(basename(absPath)),
description: mod?.description ?? '...',
...mod,
},
};
}),
);
const processPlugins$ = processCommandFiles$.pipe(
concatMap(payload => {
const cmdPluginRes = processCommandPlugins(wrapper, payload);
return of({ mod: payload.mod, cmdPluginRes });
}),
);
(
concat(ready$, processPlugins$) as Observable<{
mod: DefinedCommandModule;
cmdPluginRes: {
execute: Awaitable<Result<void, void>>;
type: PluginType.Command;
name: string;
description: string;
}[];
}>
)
.pipe(
concatMap(pl => {
return from(
//refactor, this allocates too many objects
Promise.all(
pl.cmdPluginRes.map(async e => ({ ...e, execute: await e.execute })),
),
).pipe(map(res => ({ ...pl, cmdPluginsRes: res })));
}),
)
.subscribe(({ mod, cmdPluginsRes }) => {
const loadedPluginsCorrectly = cmdPluginsRes.every(({ execute }) => execute.ok);
if (loadedPluginsCorrectly) {
const res = registerModule(mod);
if (res.err) {
throw Error(SernError.NonValidModuleType);
}
wrapper.sernEmitter?.emit('module.register', {
type: PayloadType.Success,
module: mod,
});
} else {
wrapper.sernEmitter?.emit('module.register', {
type: PayloadType.Failure,
module: mod,
reason: SernError.PluginFailure,
});
}
});
}
function registerModule(mod: DefinedCommandModule): Result<void, void> {
const name = mod.name;
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);
return Ok.EMPTY;
})
.with({ type: CommandType.Slash }, mod => {
Files.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.Both }, mod => {
Files.BothCommands.set(name, mod);
mod.alias?.forEach(a => Files.TextCommands.aliases.set(a, mod));
return Ok.EMPTY;
})
.with({ type: CommandType.MenuUser }, mod => {
Files.ApplicationCommands[ApplicationCommandType.User].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.MenuMsg }, mod => {
Files.ApplicationCommands[ApplicationCommandType.Message].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.Button }, mod => {
Files.ApplicationCommands[ComponentType.Button].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.MenuSelect }, mod => {
Files.MessageCompCommands[ComponentType.SelectMenu].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.Modal }, mod => {
Files.ModalSubmitCommands.set(name, mod);
return Ok.EMPTY;
})
.otherwise(() => Err.EMPTY);
}

View File

@@ -0,0 +1,159 @@
import { EventsHandler } from './eventsHandler';
import type Wrapper from '../structures/wrapper';
import { concatMap, fromEvent, Observable, map, take, of, from, toArray, switchMap } from 'rxjs';
import type { CommandModule } from '../structures/module';
import * as Files from '../utilities/readFile';
import { errTap } from './observableHandling';
import type { DefinedCommandModule } from '../../types/handler';
import { basename } from 'path';
import { CommandType, PayloadType, PluginType } from '../structures/enums';
import { processCommandPlugins } from './userDefinedEventsHandling';
import type { Awaitable } from 'discord.js';
import { SernError } from '../structures/errors';
import { match } from 'ts-pattern';
import { Err, Ok, type Result } from 'ts-results';
import { ApplicationCommandType, ComponentType } from 'discord.js';
export default class ReadyHandler extends EventsHandler<{
mod: DefinedCommandModule;
absPath: string;
}> {
protected discordEvent!: Observable<{ mod: CommandModule; absPath: string }>;
constructor(wrapper: Wrapper) {
super(wrapper);
const ready$ = fromEvent(this.wrapper.client, 'ready').pipe(take(1));
this.discordEvent = ready$.pipe(
concatMap(() =>
Files.buildData<CommandModule>(this.wrapper.commands).pipe(
errTap(reason =>
wrapper.sernEmitter?.emit('module.register', {
type: PayloadType.Failure,
module: undefined,
reason,
}),
),
),
),
);
this.init();
this.payloadSubject
.pipe(
concatMap(payload => this.processPlugins(payload)),
concatMap(payload => this.resolvePlugins(payload)),
)
.subscribe(payload => {
const allPluginsSuccessful = payload.pluginRes.every(({ execute }) => execute.ok);
if (allPluginsSuccessful) {
const res = registerModule(payload.mod);
if (res.err) {
throw Error(SernError.NonValidModuleType);
}
wrapper.sernEmitter?.emit('module.register', {
type: PayloadType.Success,
module: payload.mod,
});
} else {
wrapper.sernEmitter?.emit('module.register', {
type: PayloadType.Failure,
module: payload.mod,
reason: SernError.PluginFailure,
});
}
});
}
private static intoDefinedModule({ absPath, mod }: { absPath: string; mod: CommandModule }): {
absPath: string;
mod: DefinedCommandModule;
} {
return {
absPath,
mod: {
name: mod?.name ?? Files.fmtFileName(basename(absPath)),
description: mod?.description ?? '...',
...mod,
},
};
}
private resolvePlugins({
mod,
cmdPluginRes,
}: {
mod: DefinedCommandModule;
cmdPluginRes: {
name: string;
description: string;
execute: Awaitable<Result<void, void>>;
type: PluginType.Command;
}[];
}) {
if (mod.plugins.length === 0) {
return of({ mod, pluginRes: [] });
}
// modules with no event plugins are ignored in the previous
return from(cmdPluginRes).pipe(
switchMap(pl =>
from(pl.execute).pipe(
map(execute => ({ ...pl, execute })),
toArray(),
),
),
map(pluginRes => ({ mod, pluginRes })),
);
}
private processPlugins(payload: { mod: DefinedCommandModule; absPath: string }) {
const cmdPluginRes = processCommandPlugins(this.wrapper, payload);
return of({ mod: payload.mod, cmdPluginRes });
}
protected init() {
this.discordEvent.pipe(map(ReadyHandler.intoDefinedModule)).subscribe({
next: value => this.setState(value),
complete: () => this.payloadSubject.unsubscribe(),
});
}
protected setState(state: { absPath: string; mod: DefinedCommandModule }): void {
this.payloadSubject.next(state);
}
}
function registerModule(mod: DefinedCommandModule): Result<void, void> {
const name = mod.name;
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);
return Ok.EMPTY;
})
.with({ type: CommandType.Slash }, mod => {
Files.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.Both }, mod => {
Files.BothCommands.set(name, mod);
mod.alias?.forEach(a => Files.TextCommands.aliases.set(a, mod));
return Ok.EMPTY;
})
.with({ type: CommandType.MenuUser }, mod => {
Files.ApplicationCommands[ApplicationCommandType.User].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.MenuMsg }, mod => {
Files.ApplicationCommands[ApplicationCommandType.Message].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.Button }, mod => {
Files.MessageCompCommands[ComponentType.Button].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.MenuSelect }, mod => {
Files.MessageCompCommands[ComponentType.SelectMenu].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.Modal }, mod => {
Files.ModalSubmitCommands.set(name, mod);
return Ok.EMPTY;
})
.otherwise(() => Err.EMPTY);
}

View File

@@ -2,7 +2,12 @@ import { from, fromEvent, map } from 'rxjs';
import * as Files from '../utilities/readFile';
import { buildData, ExternalEventEmitters } from '../utilities/readFile';
import { controller } from '../sern';
import type { DefinedCommandModule, DefinedEventModule, SpreadParams } from '../../types/handler';
import type {
DefinedCommandModule,
DefinedEventModule,
EventInput,
SpreadParams,
} from '../../types/handler';
import type { EventModule } from '../structures/module';
import { PayloadType } from '../structures/enums';
import type Wrapper from '../structures/wrapper';
@@ -28,13 +33,7 @@ export function processCommandPlugins<T extends DefinedCommandModule>(
}));
}
export function processEvents(
wrapper: Wrapper,
events:
| string
| { mod: EventModule; absPath: string }[]
| (() => { mod: EventModule; absPath: string }[]),
) {
export function processEvents(wrapper: Wrapper, events: EventInput) {
const eventStream$ = eventObservable$(wrapper, events);
const normalize$ = eventStream$.pipe(
map(({ mod, absPath }) => {
@@ -59,13 +58,7 @@ export function processEvents(
});
}
function eventObservable$(
{ sernEmitter }: Wrapper,
events:
| string
| { mod: EventModule; absPath: string }[]
| (() => { mod: EventModule; absPath: string }[]),
) {
function eventObservable$({ sernEmitter }: Wrapper, events: EventInput) {
return match(events)
.when(Array.isArray, (arr: { mod: EventModule; absPath: string }[]) => {
return from(arr);

View File

@@ -171,8 +171,6 @@ export type EventModulePlugin<T extends EventType> =
| EventModuleCommandPluginDefs[T];
export type CommandModulePlugin<T extends CommandType> = EventPlugin<T> | CommandPlugin<T>;
//TODO: I WANT BETTER TYPINGS AHHHHHHHHHHHHHHH
// Maybe add overlaods
/**
* User inputs this type. Sern processes behind the scenes for better usage

View File

@@ -1,7 +1,4 @@
import type Wrapper from './structures/wrapper';
import { onReady } from './events/readyEvent';
import { onMessageCreate } from './events/messageEvent';
import { onInteractionCreate } from './events/interactionCreate';
import { Err, Ok } from 'ts-results';
import { ExternalEventEmitters } from './utilities/readFile';
import type { EventEmitter } from 'events';
@@ -17,6 +14,9 @@ import type {
InputEventModule,
} from './plugins/plugin';
import { SernError } from './structures/errors';
import InteractionHandler from './events/interactionHandler';
import ReadyHandler from './events/readyHandler';
import MessageHandler from './events/messageHandler';
/**
*
@@ -28,15 +28,15 @@ export function init(wrapper: Wrapper) {
if (events !== undefined) {
processEvents(wrapper, events);
}
onReady(wrapper);
onMessageCreate(wrapper);
onInteractionCreate(wrapper);
new ReadyHandler(wrapper);
new MessageHandler(wrapper);
new InteractionHandler(wrapper);
}
/**
*
* @param emitter Any external event emitter.
* The object will be stored in a map, and then fetched by the name of the instance's class provided.
* The object will be stored in a map, and then fetched by the name of the instance's class.
* As there are infinite possibilities to adding external event emitters,
* Most types aren't provided and are as narrow as possibly can.
* @example

View File

@@ -1,19 +1,11 @@
import type { CommandModuleDefs, EventModule, Module } from '../structures/module';
import type {
Awaitable,
ButtonInteraction,
ChatInputCommandInteraction,
CommandInteraction,
MessageComponentInteraction,
MessageContextMenuCommandInteraction,
SelectMenuInteraction,
UserContextMenuCommandInteraction,
} from 'discord.js';
import {
AutocompleteInteraction,
Interaction,
InteractionType,
ModalSubmitInteraction,
type CommandInteraction,
type MessageComponentInteraction,
} from 'discord.js';
import type {
DiscordEventCommand,
@@ -31,30 +23,6 @@ export function correctModuleType<T extends keyof CommandModuleDefs>(
return plug !== undefined && (plug.type & type) !== 0;
}
export function isChatInputCommand(i: CommandInteraction): i is ChatInputCommandInteraction {
return i.isChatInputCommand();
}
export function isButton(i: MessageComponentInteraction): i is ButtonInteraction {
return i.isButton();
}
export function isSelectMenu(i: MessageComponentInteraction): i is SelectMenuInteraction {
return i.isSelectMenu();
}
export function isMessageCtxMenuCmd(
i: CommandInteraction,
): i is MessageContextMenuCommandInteraction {
return i.isMessageContextMenuCommand();
}
export function isUserContextMenuCmd(
i: CommandInteraction,
): i is UserContextMenuCommandInteraction {
return i.isUserContextMenuCommand();
}
export function isApplicationCommand(interaction: Interaction): interaction is CommandInteraction {
return interaction.type === InteractionType.ApplicationCommand;
}
@@ -72,11 +40,6 @@ export function isMessageComponent(
return interaction.type === InteractionType.MessageComponent;
}
export function isPromise<T>(promiseLike: Awaitable<T>): promiseLike is PromiseLike<T> {
const keys = new Set(Object.keys(promiseLike));
return keys.has('then') && keys.has('catch');
}
export function isDiscordEvent(el: EventModule): el is DiscordEventCommand {
return el.type === EventType.Discord;
}

View File

@@ -2,30 +2,29 @@ import { ApplicationCommandType, ComponentType } from 'discord.js';
import { readdirSync, statSync } from 'fs';
import { join } from 'path';
import { from, Observable } from 'rxjs';
import type { Module } from '../structures/module';
import type { CommandModule } from '../structures/module';
import { SernError } from '../structures/errors';
import type { Result } from 'ts-results';
import { Err, Ok } from 'ts-results';
import { Err, Ok, type Result } from 'ts-results';
import type { EventEmitter } from 'events';
//Maybe move this? this probably doesnt belong in utlities/
export const BothCommands = new Map<string, Module>();
export const BothCommands = new Map<string, CommandModule>();
export const ApplicationCommands = {
[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> };
[ApplicationCommandType.User]: new Map<string, CommandModule>(),
[ApplicationCommandType.Message]: new Map<string, CommandModule>(),
[ApplicationCommandType.ChatInput]: new Map<string, CommandModule>(),
} as { [K in ApplicationCommandType]: Map<string, CommandModule> };
export const MessageCompCommands = {
[ComponentType.Button]: new Map<string, Module>(),
[ComponentType.SelectMenu]: new Map<string, Module>(),
[ComponentType.TextInput]: new Map<string, Module>(),
[ComponentType.Button]: new Map<string, CommandModule>(),
[ComponentType.SelectMenu]: new Map<string, CommandModule>(),
[ComponentType.TextInput]: new Map<string, CommandModule>(),
};
export const TextCommands = {
text: new Map<string, Module>(),
aliases: new Map<string, Module>(),
text: new Map<string, CommandModule>(),
aliases: new Map<string, CommandModule>(),
};
export const ModalSubmitCommands = new Map<string, Module>();
export const ModalSubmitCommands = new Map<string, CommandModule>();
/**
* keeps all external emitters stored here
*/
@@ -63,8 +62,9 @@ export function buildData<T>(commandDir: string): Observable<
SernError
>
> {
const commands = getCommands(commandDir);
return from(
getCommands(commandDir).map(absPath => {
commands.map(absPath => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mod = <T | undefined>require(absPath).default;
if (mod !== undefined) {

View File

@@ -21,6 +21,11 @@ export type DefinitelyDefined<T, K extends keyof T = keyof T> = {
: Required<T>[L];
} & T;
export type EventInput =
| string
| { mod: EventModule; absPath: string }[]
| (() => { mod: EventModule; absPath: string }[]);
export type Reconstruct<T> = T extends Omit<infer O, never> ? O & Reconstruct<O> : T;
export type IsOptional<T> = {