diff --git a/package-lock.json b/package-lock.json index c5d6720..607a983 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 2a6ad9c..ff16a6f 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/handler/events/dispatchers.ts b/src/handler/events/dispatchers.ts new file mode 100644 index 0000000..2cb6078 --- /dev/null +++ b/src/handler/events/dispatchers.ts @@ -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)), + ), + }); +} diff --git a/src/handler/events/eventsHandler.ts b/src/handler/events/eventsHandler.ts new file mode 100644 index 0000000..9ef215c --- /dev/null +++ b/src/handler/events/eventsHandler.ts @@ -0,0 +1,10 @@ +import type Wrapper from '../structures/wrapper'; +import { Subject, type Observable } from 'rxjs'; + +export abstract class EventsHandler { + protected payloadSubject = new Subject(); + protected abstract discordEvent: Observable; + protected constructor(protected wrapper: Wrapper) {} + protected abstract init(): void; + protected abstract setState(state: T): void; +} diff --git a/src/handler/events/interactionCreate.ts b/src/handler/events/interactionCreate.ts deleted file mode 100644 index c5c9a96..0000000 --- a/src/handler/events/interactionCreate.ts +++ /dev/null @@ -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$ = (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$ = (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$ = >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); - }, - }); -} diff --git a/src/handler/events/interactionHandler.ts b/src/handler/events/interactionHandler.ts new file mode 100644 index 0000000..3bd6d85 --- /dev/null +++ b/src/handler/events/interactionHandler.ts @@ -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; + + constructor(protected wrapper: Wrapper) { + super(wrapper); + this.discordEvent = >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); + }); + } +} diff --git a/src/handler/events/messageEvent.ts b/src/handler/events/messageEvent.ts deleted file mode 100644 index f798166..0000000 --- a/src/handler/events/messageEvent.ts +++ /dev/null @@ -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$ = >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); - }, - }); -}; diff --git a/src/handler/events/messageHandler.ts b/src/handler/events/messageHandler.ts new file mode 100644 index 0000000..8c2d828 --- /dev/null +++ b/src/handler/events/messageHandler.ts @@ -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; + public constructor(wrapper: Wrapper) { + super(wrapper); + this.discordEvent = >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); + } +} diff --git a/src/handler/events/observableHandling.ts b/src/handler/events/observableHandling.ts index 64a9bfe..991aea5 100644 --- a/src/handler/events/observableHandling.ts +++ b/src/handler/events/observableHandling.ts @@ -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(cmdType: T) { return (src: Observable) => new Observable(subscriber => { @@ -64,3 +68,52 @@ export function errTap(cb: (err: SernError) => void) { }); }); } + +//POG +export function isOneOfCorrectModules(...inputs: [...T]) { + return (src: Observable) => { + return new Observable(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[]; + }, +) { + if (payload.res.every(el => el.ok)) { + return from(payload.execute() as Promise).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); + } +} diff --git a/src/handler/events/readyEvent.ts b/src/handler/events/readyEvent.ts deleted file mode 100644 index ca04ae4..0000000 --- a/src/handler/events/readyEvent.ts +++ /dev/null @@ -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(commands).pipe( - errTap(reason => { - wrapper.sernEmitter?.emit('module.register', { - type: PayloadType.Failure, - module: undefined, - reason, - }); - }), - map(({ mod, absPath }) => { - return { - absPath, - mod: { - 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>; - 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 { - const name = mod.name; - return match(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); -} diff --git a/src/handler/events/readyHandler.ts b/src/handler/events/readyHandler.ts new file mode 100644 index 0000000..64d6974 --- /dev/null +++ b/src/handler/events/readyHandler.ts @@ -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(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>; + 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 { + const name = mod.name; + return match(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); +} diff --git a/src/handler/events/userDefinedEventsHandling.ts b/src/handler/events/userDefinedEventsHandling.ts index f96dc75..fa02698 100644 --- a/src/handler/events/userDefinedEventsHandling.ts +++ b/src/handler/events/userDefinedEventsHandling.ts @@ -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( })); } -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); diff --git a/src/handler/plugins/plugin.ts b/src/handler/plugins/plugin.ts index 359cc5f..cd67fc2 100644 --- a/src/handler/plugins/plugin.ts +++ b/src/handler/plugins/plugin.ts @@ -171,8 +171,6 @@ export type EventModulePlugin = | EventModuleCommandPluginDefs[T]; export type CommandModulePlugin = EventPlugin | CommandPlugin; -//TODO: I WANT BETTER TYPINGS AHHHHHHHHHHHHHHH -// Maybe add overlaods /** * User inputs this type. Sern processes behind the scenes for better usage diff --git a/src/handler/sern.ts b/src/handler/sern.ts index 3e3a628..444ba0a 100644 --- a/src/handler/sern.ts +++ b/src/handler/sern.ts @@ -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 diff --git a/src/handler/utilities/predicates.ts b/src/handler/utilities/predicates.ts index 4c9d6ae..49c1834 100644 --- a/src/handler/utilities/predicates.ts +++ b/src/handler/utilities/predicates.ts @@ -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( 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(promiseLike: Awaitable): promiseLike is PromiseLike { - 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; } diff --git a/src/handler/utilities/readFile.ts b/src/handler/utilities/readFile.ts index 8b45acc..51b4544 100644 --- a/src/handler/utilities/readFile.ts +++ b/src/handler/utilities/readFile.ts @@ -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(); +export const BothCommands = new Map(); export const ApplicationCommands = { - [ApplicationCommandType.User]: new Map(), - [ApplicationCommandType.Message]: new Map(), - [ApplicationCommandType.ChatInput]: new Map(), -} as { [K in ApplicationCommandType]: Map }; + [ApplicationCommandType.User]: new Map(), + [ApplicationCommandType.Message]: new Map(), + [ApplicationCommandType.ChatInput]: new Map(), +} as { [K in ApplicationCommandType]: Map }; export const MessageCompCommands = { - [ComponentType.Button]: new Map(), - [ComponentType.SelectMenu]: new Map(), - [ComponentType.TextInput]: new Map(), + [ComponentType.Button]: new Map(), + [ComponentType.SelectMenu]: new Map(), + [ComponentType.TextInput]: new Map(), }; export const TextCommands = { - text: new Map(), - aliases: new Map(), + text: new Map(), + aliases: new Map(), }; -export const ModalSubmitCommands = new Map(); +export const ModalSubmitCommands = new Map(); /** * keeps all external emitters stored here */ @@ -63,8 +62,9 @@ export function buildData(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 = require(absPath).default; if (mod !== undefined) { diff --git a/src/types/handler.ts b/src/types/handler.ts index 88436f8..b5dc1be 100644 --- a/src/types/handler.ts +++ b/src/types/handler.ts @@ -21,6 +21,11 @@ export type DefinitelyDefined = { : Required[L]; } & T; +export type EventInput = + | string + | { mod: EventModule; absPath: string }[] + | (() => { mod: EventModule; absPath: string }[]); + export type Reconstruct = T extends Omit ? O & Reconstruct : T; export type IsOptional = {