mirror of
https://github.com/sern-handler/handler
synced 2026-06-26 09:42:15 +00:00
refactor(events): use of classes for scalability & maintainability (#83)
Co-authored-by: EvolutionX-10 <evolutionx9777@gmail.com>
This commit is contained in:
88
package-lock.json
generated
88
package-lock.json
generated
@@ -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",
|
||||
|
||||
10
package.json
10
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"
|
||||
},
|
||||
|
||||
110
src/handler/events/dispatchers.ts
Normal file
110
src/handler/events/dispatchers.ts
Normal 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)),
|
||||
),
|
||||
});
|
||||
}
|
||||
10
src/handler/events/eventsHandler.ts
Normal file
10
src/handler/events/eventsHandler.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
129
src/handler/events/interactionHandler.ts
Normal file
129
src/handler/events/interactionHandler.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
80
src/handler/events/messageHandler.ts
Normal file
80
src/handler/events/messageHandler.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
159
src/handler/events/readyHandler.ts
Normal file
159
src/handler/events/readyHandler.ts
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
Reference in New Issue
Block a user