diff --git a/scripts/generate.js b/scripts/generate.js deleted file mode 100644 index 43ac1651..00000000 --- a/scripts/generate.js +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env node -const fs = require('fs/promises'); - -const buildPluginContent = (pluginName) => - `import { Plugin } from '../../common/plugin'; -import { IContainer, IMessage, ChannelType } from '../../common/types'; - -export class ${pluginName} extends Plugin { - public name: string = '${pluginName}'; - public description: string = 'Some sort of a description.'; - public usage: string = '${pluginName}'; - public permission: ChannelType = ChannelType.Public; - - constructor(public container: IContainer) { - super(); - } - - public async execute(message: IMessage, args?: string[]) { - // Your logic here. - } -} -`; - -const buildPluginFriendlyName = (name) => - name - .split(' ') - .filter(Boolean) - .map((token) => `${token.slice(0, 1).toUpperCase()}${token.slice(1)}`) - .join('') - .concat('Plugin'); - -const buildPluginLoader = (pluginLoader, pluginName, fileName) => { - const tokens = pluginLoader.split('\n'); - const desiredIdx = tokens.findIndex((r) => r.length === 0); - tokens.splice( - desiredIdx, - 0, - `import { ${pluginName} } from '../app/plugins/${fileName}.plugin';` - ); - return tokens.join('\n').replace( - `};`, - - ` ${pluginName.toLowerCase().slice(0, -6)}: ${pluginName}, -};` - ); -}; - -const createPlugin = async (name) => { - const sanitizedName = name.toLowerCase().replace(/ /g, '_'); - const cwd = process.cwd(); - const pluginDirectoryPath = `${cwd}/src/app/plugins`; - const newPluginPath = `${pluginDirectoryPath}/${sanitizedName}.plugin.ts`; - const newPluginName = buildPluginFriendlyName(name); - const newPluginContent = buildPluginContent(newPluginName); - - await fs.writeFile(newPluginPath, newPluginContent); - - const pluginLoaderPath = `${cwd}/src/bootstrap/plugin.loader.ts`; - const pluginLoaderContent = await fs.readFile(pluginLoaderPath, 'utf-8'); - const newPluginLoaderContent = buildPluginLoader( - pluginLoaderContent, - newPluginName, - sanitizedName - ); - - await fs.writeFile(pluginLoaderPath, newPluginLoaderContent); -}; - -const action = process.argv[3]; -const name = process.argv.slice(4).join(' '); - -if (action !== 'plugin') { - console.error('Invalid action name.'); - return -1; -} - -if (!name) { - console.error('No name specified.'); - return -1; -} - -createPlugin(name) - .then(() => console.log('✅ Added Plugin')) - .catch((err) => console.error(`❌ Something went wrong: ${err}`)); diff --git a/src/__tests__/plugin.test.ts b/src/__tests__/plugin.test.ts deleted file mode 100644 index 71791ead..00000000 --- a/src/__tests__/plugin.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { vi, beforeAll, test, describe, expect } from 'vitest'; -import { getContainerMock, getMessageMock, getTextChannelMock } from '../__mocks__'; - -vi.mock('discord.js', async (importOriginal) => { - const module: typeof import('discord.js') = await importOriginal(); - - const Client = module.Client; - - Client.prototype.login = vi.fn(); - Client.prototype.guilds = { - cache: { - // @ts-expect-error overloaded - first() { - return { - name: 'guild', - id: '123456789', - }; - }, - }, - }; - - return { - ...module, - Client, - }; -}); - -vi.mock('../services/message.service', () => { - const MessageService = vi.fn(); - - MessageService.prototype._getBotChannel = () => getTextChannelMock(); - MessageService.prototype.getChannel = () => getTextChannelMock(); - - return { MessageService }; -}); - -import { CommandHandler } from '../app/handlers/command.handler'; -import { Plugin } from '../common/plugin'; -import { IContainer, ChannelType, IMessage, Voidable } from '../common/types'; - -const container = getContainerMock(); -class MockPlugin extends Plugin { - public container: IContainer = container; - public commandName: string = 'mock'; - public name: string = 'mock'; - public description: string = 'A mock Plugin'; - public usage: string = 'mock'; - public permission: ChannelType = ChannelType.Public; - - public execute(message: IMessage): Voidable { - message.reply('hello'); - } -} - -const plugin = new MockPlugin(); -const mockMessage = getMessageMock(); - -mockMessage.content = '!mock'; -const commandHandler = new CommandHandler(container); - -beforeAll(() => { - container.pluginService.register(plugin); -}); - -describe('Plugin Architecture', () => { - test('Command to fetch proper plugin', () => { - expect(commandHandler.build(mockMessage.content)).toBeTruthy(); - }); - - test('Expect plugin to be executed.', async () => { - const spy = vi.spyOn(plugin, 'execute'); - await plugin.execute(mockMessage); - expect(spy).toBeCalled(); - }); - - test('Plugin with same name to fail', () => { - expect(() => container.pluginService.register(plugin)).toThrow(); - }); -}); diff --git a/src/app/handlers/command.handler.ts b/src/app/handlers/command.handler.ts index def3485b..a1b4e919 100644 --- a/src/app/handlers/command.handler.ts +++ b/src/app/handlers/command.handler.ts @@ -1,15 +1,10 @@ -import * as types from '../../common/types'; import Constants from '../../common/constants'; -import levenshtein from 'js-levenshtein'; -import { MessageEmbed, MessageReaction, User } from 'discord.js'; -import ms from 'ms'; import { Handler } from '../../common/handler'; import { commands } from '../../common/slash'; +import * as types from '../../common/types'; export class CommandHandler extends Handler { public name: string = 'Command'; - private _CHECK_EMOTE = '✅'; - private _CANCEL_EMOTE = '❎'; constructor(public container: types.IContainer) { super(); @@ -17,8 +12,6 @@ export class CommandHandler extends Handler { public async execute(message: types.IMessage): Promise { const command = this.build(message.content); - const plugins = this.container.pluginService.plugins; - const aliases = this.container.pluginService.aliases; // checks to see if the user is actually talking to the bot if (!command) { @@ -30,17 +23,8 @@ export class CommandHandler extends Handler { return; } - const plugin = plugins[aliases[command.name]]; - const isDM = !message.guild; - - if (plugin) { - await this._attemptRunPlugin(message, plugin, command, isDM); - return; - } - // Check if a slash command of the same name exists, if so redirect to it const slashCommand = commands.get(command.name); - if (slashCommand) { const cachedCommand = this.container.guildService .get() @@ -51,71 +35,6 @@ export class CommandHandler extends Handler { ); return; } - - await this._tryFuzzySearch(message, command, isDM); - } - - private async _tryFuzzySearch(message: types.IMessage, command: types.ICommand, isDM: boolean) { - const { plugins, aliases } = this.container.pluginService; - const allNames = Array.from(Object.keys(this.container.pluginService.aliases)); - - const validCommandsInChannel = allNames.filter((name) => { - const plugin = plugins[aliases[name]]; - return plugin.hasPermission(message) === true; - }); - - const [mostLikelyCommand] = validCommandsInChannel.sort( - (a: string, b: string) => levenshtein(command.name, a) - levenshtein(command.name, b) - ); - - const embed = new MessageEmbed(); - embed.setTitle('Command not found'); - embed.setDescription( - 'Did you mean `!' + - `${mostLikelyCommand}${command.args.length ? ' ' : ''}${command.args.join(' ')}\`?\n` + - 'React with ✅ to run this command.\n' + - 'React with ❎ to close this offering.' - ); - - const msg = await message.channel.send({ embeds: [embed] }); - await msg.react(this._CHECK_EMOTE); - await msg.react(this._CANCEL_EMOTE); - - const collector = msg.createReactionCollector( - { - filter: (reaction: MessageReaction, user: User) => - [this._CHECK_EMOTE, this._CANCEL_EMOTE].includes(reaction.emoji.name!) && - user.id !== msg.author.id, // Only run if its not the bot putting reacts - time: ms('10m'), - } // Listen for 10 Minutes - ); - - // Delete message after collector is finished - collector.on('end', () => { - if (msg.deletable) { - msg.delete().catch(() => {}); - } - }); - - collector.on('collect', async (reaction: MessageReaction) => { - const lastUserToReact = reaction.users.cache.last(); - - // If the person reacting wasn't the original sender - if (lastUserToReact !== message.author) { - // Delete the reaction - await reaction.users.remove(lastUserToReact); - return; - } - - if (reaction.emoji.name === this._CANCEL_EMOTE) { - await msg.delete().catch(); - return; - } - - const mostLikelyPlugin = plugins[aliases[mostLikelyCommand]]; - await this._attemptRunPlugin(message, mostLikelyPlugin, command, isDM); - await msg.delete().catch(); - }); } build(content: string): types.Maybe { @@ -128,52 +47,4 @@ export class CommandHandler extends Handler { const args = messageArr.slice(1).filter(Boolean); return { name, args }; } - - private async _attemptRunPlugin( - message: types.IMessage, - plugin: types.IPlugin, - command: types.ICommand, - isDM: boolean - ) { - if ((isDM && !plugin.usableInDM) || (!isDM && !plugin.usableInGuild)) { - return; - } - - const permissionResponse = plugin.hasPermission(message); - if (!isDM && permissionResponse !== true) { - message.reply(permissionResponse); - return; - } - - if (!plugin.validate(message, command.args)) { - await message.reply(`Invalid arguments! Try: \`${Constants.Prefix}${plugin.usage}\``); - return; - } - - if (!plugin.isActive) { - await message.reply('This plugin has been deactivated.'); - return; - } - - const pEvent: types.IPluginEvent = { - status: 'starting', - pluginName: plugin.name, - args: command.args, - user: message.author.tag, - }; - - try { - this.container.loggerService.info(JSON.stringify(pEvent)); - await plugin.execute(message, command.args); - - pEvent.status = 'fulfillCommand'; - this.container.loggerService.info(JSON.stringify(pEvent)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - pEvent.status = 'error'; - pEvent.error = e.message as string; - pEvent.stack = e.stack as string; - this.container.loggerService.error(JSON.stringify(pEvent)); - } - } }