diff --git a/package-lock.json b/package-lock.json index 6e1d66c2e..a193a5f65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "modbot", - "version": "0.5.0", + "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "0.5.0", + "version": "1.0.0", "license": "MIT", "dependencies": { "@google-cloud/logging": "^9.1.1", diff --git a/package.json b/package.json index 03d642304..cf20a0fcc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modbot", - "version": "0.5.0", + "version": "1.0.0", "description": "Discord Bot for the Aternos Discord server", "main": "index.js", "scripts": { diff --git a/src/Command.js b/src/Command.js index 26a39c298..a8ce9a76e 100644 --- a/src/Command.js +++ b/src/Command.js @@ -163,9 +163,9 @@ class Command { .addFields( /** @type {any} */ { name: 'Usage', value: `\`${prefix}${cmd} ${this.usage}\``, inline: true}, /** @type {any} */ { name: 'Description', value: this.description, inline: true}, - /** @type {any} */ { name: 'Required Permissions', value: this.userPerms.length !== 0 ? `\`${this.userPerms.join('`, `')}\`` : 'none' } + /** @type {any} */ { name: 'Required Permissions', value: this.userPerms.length !== 0 ? `\`${this.userPerms.join('`, `')}\`` : 'none', inline: true } ) - .setColor(util.color.green) + .setColor(util.color.red) .setTimestamp(); if (this.comment) { embed.addFields( @@ -251,6 +251,15 @@ class Command { await message.reactions.removeAll(); } } + + /** + * get an overview of this command + * @return {string} + */ + static getOverview() { + return `**${this.names.join(', ')}**\n`+ + `${this.description}\n`; + } } module.exports = Command; diff --git a/src/commands/legacy/help.js b/src/commands/legacy/help.js deleted file mode 100644 index 217ae7463..000000000 --- a/src/commands/legacy/help.js +++ /dev/null @@ -1,105 +0,0 @@ -const util = require('../../util.js'); -const Discord = require('discord.js'); -const fs = require('fs').promises; -const GuildConfig = require('../../GuildConfig'); -const CommandHandler = require('../../features/message/CommandManager'); -const monitor = require('../../Monitor').getInstance(); - -const command = {}; - -command.description = 'List all commands or get a description for a single command'; - -command.usage = ''; - -command.names = ['help']; - -const commands = {}; -let commandList = ''; - -(async () => { - for (let file of await fs.readdir(`${__dirname}`)) { - let path = `${__dirname}/${file}`; - if (!file.endsWith('.js') || !(await fs.lstat(path)).isFile()) { - continue; - } - try { - let cmd = require(path); - for (let name of cmd.names) { - commands[name] = cmd; - } - commandList += `\`${cmd.names[0]}\`, `; - } catch (e) { - await monitor.error(`(help) Failed to load legacy command 'legacy/${file}'`, e); - console.error(`(help) Failed to load legacy command 'legacy/${file}'`, e); - } - } - commandList = commandList.substring(0, commandList.length - 2); -})(); - -const newCommands = CommandHandler.getCommands(); -for (const cmd in newCommands) { - commandList += `\`${cmd}\`, ` -} - -command.execute = async (message, args, database, bot) => { - let config = await GuildConfig.get(message.guild.id); - let embed = new Discord.MessageEmbed() - .setColor(util.color.green) - .setFooter(`Command executed by ${message.author.username}`) - .setTimestamp(); - if (!args.length || args[0] === 'list') { - embed - .setAuthor(`Help Menu | Prefix: ${config.prefix}`) - .addFields( - /** @type {any} */ { name: "Commands", value: commandList, inline: true} - ); - } - else { - const name = args[0]; - if (newCommands[name]) { - embed = await newCommands[name].getUsage(message, name, config); - } - else if (commands[name]) { - embed = await command.getUse(message, name); - } - else { - embed - .setAuthor(`Help | Prefix: ${config.prefix}`) - .setColor(util.color.red) - .setDescription(`${args[0]} is not a valid command`); - } - } - message.channel.send(embed); - -}; - -command.getUse = async (message, cmd) => { - let command = commands[cmd]; - let config = await GuildConfig.get(message.guild.id); - let embed = new Discord.MessageEmbed() - .setAuthor(`Help for ${cmd} | Prefix: ${config.prefix}`) - .setFooter(`Command executed by ${message.author.username}`) - .addFields( - /** @type {any} */ { name: "Usage", value: `\`${config.prefix}${cmd} ${command.usage}\``, inline: true}, - /** @type {any} */ { name: "Description", value: command.description, inline: true} - ) - .setColor(util.color.green) - .setTimestamp(); - if (command.comment) { - embed.addFields( - /** @type {any} */{ name: "Comment", value: `${command.comment}`, inline: false}); - } - if (command.names.length > 1) { - let aliases = ''; - for (let name of command.names) { - if (name !== cmd) { - aliases += `\`${name}\`, `; - } - } - embed.addFields( - /** @type {any} */{ name: "Aliases", value: aliases.substring(0,aliases.length - 2), inline: false}); - } - return embed; -}; - -module.exports = command; diff --git a/src/commands/external/ArticleCommand.js b/src/commands/utility/ArticleCommand.js similarity index 100% rename from src/commands/external/ArticleCommand.js rename to src/commands/utility/ArticleCommand.js diff --git a/src/commands/utility/HelpCommand.js b/src/commands/utility/HelpCommand.js new file mode 100644 index 000000000..4e29d2fda --- /dev/null +++ b/src/commands/utility/HelpCommand.js @@ -0,0 +1,55 @@ +const Command = require('../../Command'); +const {MessageEmbed} = require('discord.js'); +const util = require('../../util'); + +class HelpCommand extends Command { + + static description = 'View command information'; + + static usage = '[]'; + + static names = ['help']; + + async execute() { + + const commandManager = require('../../features/message/CommandManager'); + + const categories = commandManager.getCategories(); + const commands = commandManager.getCommands(); + + if (!this.args.length) { + const embed = new MessageEmbed() + .setColor(util.color.red) + .setFooter(`View a command or category using ${this.prefix}help `); + for (const [key, commands] of categories.entries()) { + if (commands.length === 0) continue; + embed.addField(util.toTitleCase(key), commands.map(c => c.names).flat().sort().join(', ')); + } + return this.message.channel.send(embed); + } + + const name = this.args.shift().toLowerCase(); + const category = categories.get(name); + const command = commands.get(name); + if (!category && !command) return this.sendUsage(); + + if (category) { + let description = ''; + + for (const command of category) { + description += command.getOverview() + '\n'; + } + + return this.message.channel.send(new MessageEmbed() + .setTitle(`ModBot ${util.toTitleCase(name)} Commands:`) + .setColor(util.color.red) + .setDescription(description) + .setFooter(`View a command using ${this.prefix}help `) + ); + } + + if (command) return this.message.channel.send(await command.getUsage(this.message, name, this.guildConfig)); + } +} + +module.exports = HelpCommand; diff --git a/src/features/message/CommandManager.js b/src/features/message/CommandManager.js index fd0f9e1ab..9e8e48c45 100644 --- a/src/features/message/CommandManager.js +++ b/src/features/message/CommandManager.js @@ -3,14 +3,21 @@ const defaultPrefix = require('../../../config.json').prefix; const Discord = require('discord.js'); const util = require('../../util'); const GuildConfig = require('../../GuildConfig'); +const {Collection} = Discord; const {APIErrors} = Discord.Constants; - const monitor = require('../../Monitor').getInstance(); class CommandManager { + /** - * loaded commands - * @type {Object} + * command categories + * @type {module:"discord.js".Collection[]>} + */ + static #categories = new Collection(); + + /** + * loaded commands (name => class) + * @type {module:"discord.js".Collection>} * @private */ static #commands = this._loadCommands(); @@ -21,10 +28,13 @@ class CommandManager { * @private */ static _loadCommands() { - const commands = {}; + const commands = new Collection(); for (const folder of fs.readdirSync(`${__dirname}/../../commands`)) { + + const category = []; + const dirPath = `${__dirname}/../../commands/${folder}`; - if (!fs.lstatSync(dirPath).isDirectory() || folder === 'legacy') continue; + if (!fs.lstatSync(dirPath).isDirectory()) continue; for (const file of fs.readdirSync(dirPath)) { const path = `${dirPath}/${file}`; if (!file.endsWith('.js') || !fs.lstatSync(path).isFile()) { @@ -32,24 +42,39 @@ class CommandManager { } try { const command = require(path); + category.push(command); for (const name of command.names) { - if (commands[name]) { + if (commands.has(name)) { console.error(`Two command registered the name '${name}':`); - console.error(`- ${commands[name].path}`); + console.error(`- ${commands.get(name).path}`); console.error(`- ${folder}/${file}`); } command.path = `${folder}/${file}`; - commands[name] = command; + commands.set(name, command); } } catch (e) { monitor.error(`Failed to load command '${folder}/${file}'`, e); console.error(`Failed to load command '${folder}/${file}'`, e); } } + + this.#categories.set(folder, category); } return commands; } + /** + * get command categories + * @return {module:"discord.js".Collection[]>} + */ + static getCategories() { + return this.#categories; + } + + /** + * get all commands (name => class) + * @return {module:"discord.js".Collection>} + */ static getCommands() { return this.#commands; } @@ -64,7 +89,7 @@ class CommandManager { */ static async event(options, message) { const {isCommand, name, prefix} = await this.getCommandName(message); - const Command = this.#commands[name]; + const Command = this.#commands.get(name); if (!isCommand || Command === undefined) return; try { @@ -129,7 +154,7 @@ class CommandManager { static async isCommand(message) { const {isCommand, name} = await this.getCommandName(message); if (!isCommand) return false; - return this.#commands[name] !== undefined; + return this.#commands.has(name); } } diff --git a/src/features/message/legacyCommands.js b/src/features/message/legacyCommands.js deleted file mode 100644 index 55da39b3b..000000000 --- a/src/features/message/legacyCommands.js +++ /dev/null @@ -1,83 +0,0 @@ -const fs = require('fs').promises; -const { prefix } = require('../../../config.json'); -const Discord = require('discord.js'); -const util = require('../../util'); -const GuildConfig = require('../../GuildConfig'); -const monitor = require('../../Monitor').getInstance(); -const {APIErrors} = Discord.Constants; - -/** - * loaded commands - * @type {*[]} - */ -const commands = []; - -(async () => { - for (let file of await fs.readdir(`${__dirname}/../../commands/legacy`)) { - let path = `${__dirname}/../../commands/legacy/${file}`; - if (!file.endsWith('.js') || !(await fs.lstat(path)).isFile()) { - continue; - } - try { - commands.push(require(path)); - } catch (e) { - await monitor.error(`Failed to load legacy command 'legacy/${file}'`, e); - console.error(`Failed to load legacy command 'legacy/${file}'`, e); - } - } -})(); - -/** - * - * @param {Object} options - * @param {Database} options.database - * @param {module:"discord.js".Client} options.bot - * @param {module:"discord.js".Message} message - * @return {Promise} - */ -exports.event = async(options, message) => { - let foundCommand = await exports.getCommand(message); - if (foundCommand === null) return; - const [command,args] = foundCommand; - - try { - await Promise.resolve(command.execute(message, args, options.database, options.bot)); - } catch (e) { - try { - if (e.code === APIErrors.MISSING_PERMISSIONS) { - await message.channel.send('I am missing permissions to execute that command!'); - } - else { - await message.channel.send('An error occurred while executing that command!'); - } - } - catch (e2) { - if (e2.code === APIErrors.MISSING_PERMISSIONS) { - return; - } - } - await monitor.error(`Failed to execute command ${command.names[0]}`, e); - console.error(`An error occurred while executing command ${command.names[0]}:`,e); - } -}; - -/** - * get the command in this message - * @param {module:"discord.js".Message} message - * @return {Promise<[Object,String[]]|null>} - */ -exports.getCommand = async (message) => { - if (!message.guild || message.author.bot) return null; - let guild = await GuildConfig.get(/** @type {module:"discord.js".Snowflake} */ message.guild.id); - let usedPrefix = util.startsWithMultiple(message.content.toLowerCase(),guild.prefix.toLowerCase(), prefix.toLowerCase()); - const args = util.split(message.content.substring(usedPrefix.length),' '); - if (!usedPrefix) return null; - - let cmd = args.shift().toLowerCase(); - for (let command of commands) { - if (command.names.includes(cmd)) { - return [command,args]; - } - } - return null; -}; diff --git a/src/features/message/respond.js b/src/features/message/respond.js index f8f40a59a..5530c219b 100644 --- a/src/features/message/respond.js +++ b/src/features/message/respond.js @@ -1,19 +1,18 @@ const AutoResponse = require('../../AutoResponse'); -const legacyCommands = require('./legacyCommands'); -const newCommands = require('./CommandManager'); +const CommandManager = require('./CommandManager'); exports.event = async (options, message) => { - if (!message.guild || message.author.bot || await legacyCommands.getCommand(message) !== null || await newCommands.isCommand(message)) return; - const triggered = []; + if (!message.guild || message.author.bot || await CommandManager.isCommand(message)) return; + const triggered = []; - const responses = await AutoResponse.get(message.channel.id, message.guild.id); - for (let [,response] of responses) { - if (response.matches(message)) { - triggered.push(response.response); + const responses = await AutoResponse.get(message.channel.id, message.guild.id); + for (let [,response] of responses) { + if (response.matches(message)) { + triggered.push(response.response); + } } - } - if (triggered.length) { - await message.channel.send(triggered[Math.floor(Math.random() * triggered.length)]); - } + if (triggered.length) { + await message.channel.send(triggered[Math.floor(Math.random() * triggered.length)]); + } }; diff --git a/src/util.js b/src/util.js index f3bda1433..18a9f2463 100644 --- a/src/util.js +++ b/src/util.js @@ -448,17 +448,6 @@ util.split = (str, ...splitAt) => { return parts; }; -/** - * Get an Embed showing the usage of a command - * @param {module:"discord.js".Message} message - * @param {String} command the name of the command - * @return {module:"discord.js".MessageEmbed} - */ -util.usage = async(message, command) => { - const help = require('./commands/legacy/help.js'); - return await help.getUse(message, command); -}; - /** * Fetch messages (even more than 100) from a channel * @async