From c2bdf3acc9c22d028c04ff542820fd8cdc48038d Mon Sep 17 00:00:00 2001 From: York Date: Sun, 12 Sep 2021 20:30:43 +0100 Subject: [PATCH 1/5] Slash Command Permissions. Due to the current limitations of the permission system for slash commands we essentially extend the functionality of our permission checking system to the slash commands. --- commands/deploy.js | 8 +++----- config.js.example | 20 ++++++++++++++++---- events/interactionCreate.js | 29 +++++++++++++++++++++++++++-- package-lock.json | 12 ++++++------ slash/leave.js | 9 ++++++--- slash/ping.js | 5 ++++- slash/stats.js | 5 ++++- yarn.lock | 6 +++--- 8 files changed, 69 insertions(+), 25 deletions(-) diff --git a/commands/deploy.js b/commands/deploy.js index 5a789658..a57e23f6 100644 --- a/commands/deploy.js +++ b/commands/deploy.js @@ -1,19 +1,17 @@ exports.run = async (client, message, args, level) => { // eslint-disable-line no-unused-vars -// Filter the slash commands to find guild only ones. - const guildCmds = client.container.slashcmds.filter(c => c.guildOnly).map(c => c.commandData); // Now we filter out global commands by inverting the filter. - const globalCmds = client.container.slashcmds.filter(c => !c.guildOnly).map(c => c.commandData); + const [globalCmds, guildCmds] = client.container.slashcmds.partition(c => !c.conf.guildOnly); // Give the user a notification the commands are deploying. await message.channel.send("Deploying commands!"); // We'll use set but please keep in mind that `set` is overkill for a singular command. // Set the guild commands like - await client.guilds.cache.get(message.guild.id)?.commands.set(guildCmds); + await client.guilds.cache.get(message.guild.id)?.commands.set(guildCmds.map(c => c.commandData)); // Then set the global commands like - await client.application?.commands.set(globalCmds).catch(e => console.log(e)); + await client.application?.commands.set(globalCmds.map(c => c.commandData)).catch(e => console.log(e)); // Reply to the user that the commands have been deployed. await message.channel.send("All commands deployed!"); diff --git a/config.js.example b/config.js.example index a8a743bc..0c5cbfa3 100644 --- a/config.js.example +++ b/config.js.example @@ -76,7 +76,10 @@ const config = { name: "Server Owner", // Simple check, if the guild owner id matches the message author's ID, then it will return true. // Otherwise it will return false. - check: (message) => message.channel.type === "text" ? (message.guild.ownerID === message.author.id ? true : false) : false + check: (message) => { + const serverOwner = message?.author ? message.author : message.user; + return message.channel.type === "GUILD_TEXT" ? (message.guild.ownerId === serverOwner.id ? true : false) : false; + } }, // Bot Support is a special in between level that has the equivalent of server owner access @@ -85,13 +88,19 @@ const config = { name: "Bot Support", // The check is by reading if an ID is part of this array. Yes, this means you need to // change this and reboot the bot to add a support user. Make it better yourself! - check: (message) => config.support.includes(message.author.id) + check: (message) => { + const botSupport = message?.author ? message.author : message.user; + return config.support.includes(botSupport.id); + } }, // Bot Admin has some limited access like rebooting the bot or reloading commands. { level: 9, name: "Bot Admin", - check: (message) => config.admins.includes(message.author.id) + check: (message) => { + const botAdmin = message?.author ? message.author : message.user; + return config.admins.includes(botAdmin.id); + } }, // This is the bot owner, this should be the highest permission level available. @@ -101,7 +110,10 @@ const config = { { level: 10, name: "Bot Owner", // Another simple check, compares the message author id to a list of owners found in the bot application. - check: (message) => message.author.id === process.env.OWNER + check: (message) => { + const owner = message?.author ? message.author : message.user; + return owner.id === process.env.OWNER; + } } ] }; diff --git a/events/interactionCreate.js b/events/interactionCreate.js index 9816b3ea..4ce39c3e 100644 --- a/events/interactionCreate.js +++ b/events/interactionCreate.js @@ -1,15 +1,40 @@ const logger = require("../modules/Logger.js"); +const { getSettings, permlevel } = require("../modules/functions.js"); +const config = require("../config.js"); + module.exports = async (client, interaction) => { // If it's not a command, stop. if (!interaction.isCommand()) return; + + // Grab the settings for this server from Enmap. + // If there is no guild, get default conf (DMs) + const settings = interaction.settings = getSettings(interaction.guild); + + // Get the user or member's permission level from the elevation + const level = permlevel(interaction); + // Grab the command data from the client.container.slashcmds Collection const cmd = client.container.slashcmds.get(interaction.commandName); + // If that command doesn't exist, silently exit and do nothing if (!cmd) return; - // Run the command + + // Since the permission system from Discord is rather limited in regarding to + // Slash Commands, we'll just utilise our permission checker. + if (level < client.container.levelCache[cmd.conf.permLevel]) { + if (settings.systemNotice === "true") { + return await interaction.reply(`You do not have permission to use this command. +Your permission level is ${level} (${config.permLevels.find(l => l.level === level).name}) +This command requires level ${client.container.levelCache[cmd.conf.permLevel]} (${cmd.conf.permLevel})`); + } else { + return; + } + } + + // If everything checks out, run the command try { await cmd.run(client, interaction); - logger.log(`${interaction.user.id} ran slash command ${interaction.commandName}`, "cmd"); + logger.log(`${config.permLevels.find(l => l.level === level).name} ${interaction.user.id} ran slash command ${interaction.commandName}`, "cmd"); } catch (e) { console.error(e); diff --git a/package-lock.json b/package-lock.json index d457f15a..3d4ace7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2922,9 +2922,9 @@ "dev": true }, "node_modules/tar": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.6.tgz", - "integrity": "sha512-oaWyu5dQbHaYcyZCTfyPpC+VmI62/OM2RTUYavTk1MDr1cwW5Boi3baeYQKiZbY2uSQJGr+iMOzb/JFxLrft+g==", + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -5456,9 +5456,9 @@ } }, "tar": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.6.tgz", - "integrity": "sha512-oaWyu5dQbHaYcyZCTfyPpC+VmI62/OM2RTUYavTk1MDr1cwW5Boi3baeYQKiZbY2uSQJGr+iMOzb/JFxLrft+g==", + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", diff --git a/slash/leave.js b/slash/leave.js index 51cac12e..c259259a 100644 --- a/slash/leave.js +++ b/slash/leave.js @@ -1,8 +1,8 @@ -const { Permissions } = require('discord.js'); +const { Permissions } = require("discord.js"); exports.run = async (client, interaction) => { // eslint-disable-line no-unused-vars await interaction.deferReply(); - if(!interaction.guild.me.permissions.has(Permissions.FLAGS.KICK_MEMBERS)) + if (!interaction.guild.me.permissions.has(Permissions.FLAGS.KICK_MEMBERS)) return await interaction.editReply("I do not have permission to kick members in this server."); await interaction.member.send("You requested to leave the server, if you change your mind you can rejoin at a later date."); await interaction.member.kick(`${interaction.member.displayName} wanted to leave.`); @@ -17,4 +17,7 @@ exports.commandData = { }; // Set this to false if you want it to be global. -exports.guildOnly = true; \ No newline at end of file +exports.conf = { + permLevel: "User", + guildOnly: true +}; \ No newline at end of file diff --git a/slash/ping.js b/slash/ping.js index f3c08162..ff63f614 100644 --- a/slash/ping.js +++ b/slash/ping.js @@ -12,4 +12,7 @@ exports.commandData = { }; // Set this to false if you want it to be global. -exports.guildOnly = false; \ No newline at end of file +exports.conf = { + permLevel: "Moderator", + guildOnly: true +}; \ No newline at end of file diff --git a/slash/stats.js b/slash/stats.js index 9f472512..22b14556 100644 --- a/slash/stats.js +++ b/slash/stats.js @@ -24,4 +24,7 @@ exports.commandData = { }; // Set this to false if you want it to be global. -exports.guildOnly = false; \ No newline at end of file +exports.conf = { + permLevel: "Administrator", + guildOnly: false +}; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index d1cf3338..0f72da29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1892,9 +1892,9 @@ "readable-stream" "^3.1.1" "tar@^6.1.0": - "integrity" "sha512-oaWyu5dQbHaYcyZCTfyPpC+VmI62/OM2RTUYavTk1MDr1cwW5Boi3baeYQKiZbY2uSQJGr+iMOzb/JFxLrft+g==" - "resolved" "https://registry.npmjs.org/tar/-/tar-6.1.6.tgz" - "version" "6.1.6" + "integrity" "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==" + "resolved" "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz" + "version" "6.1.11" dependencies: "chownr" "^2.0.0" "fs-minipass" "^2.0.0" From bdc5501830e150349f339b2fe4d7b1922c96d23c Mon Sep 17 00:00:00 2001 From: York Date: Sun, 12 Sep 2021 20:35:38 +0100 Subject: [PATCH 2/5] Fixed a mistake. Forgot to revert testing parameters. Updated the comments in the deploy command. --- commands/deploy.js | 3 ++- slash/leave.js | 3 ++- slash/ping.js | 7 ++++--- slash/stats.js | 5 +++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/commands/deploy.js b/commands/deploy.js index a57e23f6..e4d546b0 100644 --- a/commands/deploy.js +++ b/commands/deploy.js @@ -1,6 +1,7 @@ exports.run = async (client, message, args, level) => { // eslint-disable-line no-unused-vars - // Now we filter out global commands by inverting the filter. + // We'll partition the slash commands based on the guildOnly boolean. + // Separating them into the correct objects defined in the array below. const [globalCmds, guildCmds] = client.container.slashcmds.partition(c => !c.conf.guildOnly); // Give the user a notification the commands are deploying. diff --git a/slash/leave.js b/slash/leave.js index c259259a..0360f2eb 100644 --- a/slash/leave.js +++ b/slash/leave.js @@ -16,7 +16,8 @@ exports.commandData = { defaultPermission: true, }; -// Set this to false if you want it to be global. +// Set guildOnly to true if you want it to be available on guilds only. +// Otherwise false is global. exports.conf = { permLevel: "User", guildOnly: true diff --git a/slash/ping.js b/slash/ping.js index ff63f614..62ff1eda 100644 --- a/slash/ping.js +++ b/slash/ping.js @@ -11,8 +11,9 @@ exports.commandData = { defaultPermission: true, }; -// Set this to false if you want it to be global. +// Set guildOnly to true if you want it to be available on guilds only. +// Otherwise false is global. exports.conf = { - permLevel: "Moderator", - guildOnly: true + permLevel: "User", + guildOnly: false }; \ No newline at end of file diff --git a/slash/stats.js b/slash/stats.js index 22b14556..91481ed7 100644 --- a/slash/stats.js +++ b/slash/stats.js @@ -23,8 +23,9 @@ exports.commandData = { defaultPermission: true, }; -// Set this to false if you want it to be global. +// Set guildOnly to true if you want it to be available on guilds only. +// Otherwise false is global. exports.conf = { - permLevel: "Administrator", + permLevel: "User", guildOnly: false }; \ No newline at end of file From ec89bb629f92c865f7e8e9328f1deaae96ac8089 Mon Sep 17 00:00:00 2001 From: York Date: Mon, 13 Sep 2021 12:32:38 +0100 Subject: [PATCH 3/5] Fixed Help Fixed the help command, it will now respond if you supply an invalid command name, or alias. It will now display the help message if an alias is supplied. Updated the `messageCreate` event to output the error into the console. Updated the example config to take advantage of the Nullish coalescing operator --- commands/help.js | 6 +++--- config.js.example | 8 ++++---- events/messageCreate.js | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/commands/help.js b/commands/help.js index d847aa5e..41859ba8 100644 --- a/commands/help.js +++ b/commands/help.js @@ -46,11 +46,11 @@ exports.run = (client, message, args, level) => { } else { // Show individual command's help. let command = args[0]; - if (container.commands.has(command)) { - command = container.commands.get(command); + if (container.commands.has(command) || container.commands.has(container.aliases.get(command))) { + command = container.commands.get(command) ?? container.commands.get(container.aliases.get(command)); if (level < container.levelCache[command.conf.permLevel]) return; message.channel.send(codeBlock("asciidoc", `= ${command.help.name} = \n${command.help.description}\nusage:: ${command.help.usage}\naliases:: ${command.conf.aliases.join(", ")}`)); - } + } else return message.channel.send("No command with that name, or alias exists."); }}; exports.conf = { diff --git a/config.js.example b/config.js.example index 0c5cbfa3..46d0a89d 100644 --- a/config.js.example +++ b/config.js.example @@ -77,7 +77,7 @@ const config = { // Simple check, if the guild owner id matches the message author's ID, then it will return true. // Otherwise it will return false. check: (message) => { - const serverOwner = message?.author ? message.author : message.user; + const serverOwner = message.author ?? message.user; return message.channel.type === "GUILD_TEXT" ? (message.guild.ownerId === serverOwner.id ? true : false) : false; } }, @@ -89,7 +89,7 @@ const config = { // The check is by reading if an ID is part of this array. Yes, this means you need to // change this and reboot the bot to add a support user. Make it better yourself! check: (message) => { - const botSupport = message?.author ? message.author : message.user; + const botSupport = message.author ?? message.user; return config.support.includes(botSupport.id); } }, @@ -98,7 +98,7 @@ const config = { { level: 9, name: "Bot Admin", check: (message) => { - const botAdmin = message?.author ? message.author : message.user; + const botAdmin = message.author ?? message.user; return config.admins.includes(botAdmin.id); } }, @@ -111,7 +111,7 @@ const config = { name: "Bot Owner", // Another simple check, compares the message author id to a list of owners found in the bot application. check: (message) => { - const owner = message?.author ? message.author : message.user; + const owner = message.author ?? message.user; return owner.id === process.env.OWNER; } } diff --git a/events/messageCreate.js b/events/messageCreate.js index 36bb71cc..fa09e1df 100644 --- a/events/messageCreate.js +++ b/events/messageCreate.js @@ -77,6 +77,7 @@ This command requires level ${container.levelCache[cmd.conf.permLevel]} (${cmd.c await cmd.run(client, message, args, level); logger.log(`${config.permLevels.find(l => l.level === level).name} ${message.author.id} ran command ${cmd.help.name}`, "cmd"); } catch (e) { + console.error(e); message.channel.send({ content: `There was a problem with your request.\n\`\`\`${e.message}\`\`\`` }) .catch(e => console.error("An error occurred replying on an error", e)); } From d6ebd3c4c4023ae31769524b78125584cd31055d Mon Sep 17 00:00:00 2001 From: York Date: Mon, 13 Sep 2021 19:51:20 +0100 Subject: [PATCH 4/5] Guild Owner fix This resolves #153, instead of over-engineering the issue it was successfully reduced to this thanks to @WilsontheWolf --- config.js.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.js.example b/config.js.example index 46d0a89d..cd6d8af5 100644 --- a/config.js.example +++ b/config.js.example @@ -78,7 +78,7 @@ const config = { // Otherwise it will return false. check: (message) => { const serverOwner = message.author ?? message.user; - return message.channel.type === "GUILD_TEXT" ? (message.guild.ownerId === serverOwner.id ? true : false) : false; + return message.guild?.ownerId === serverOwner.id; } }, From ea924da295a6b37833eec4da30962773ec14171b Mon Sep 17 00:00:00 2001 From: York Date: Tue, 14 Sep 2021 13:08:07 +0100 Subject: [PATCH 5/5] Correcting a mistake. If the command executioner can, or can't execute the command with the nature of the permission system we **must** respond otherwise it will error out and inform the user that it failed, giving them the illusion that they can use the command. --- events/interactionCreate.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/events/interactionCreate.js b/events/interactionCreate.js index 4ce39c3e..56e8c6e9 100644 --- a/events/interactionCreate.js +++ b/events/interactionCreate.js @@ -22,13 +22,15 @@ module.exports = async (client, interaction) => { // Since the permission system from Discord is rather limited in regarding to // Slash Commands, we'll just utilise our permission checker. if (level < client.container.levelCache[cmd.conf.permLevel]) { - if (settings.systemNotice === "true") { - return await interaction.reply(`You do not have permission to use this command. -Your permission level is ${level} (${config.permLevels.find(l => l.level === level).name}) -This command requires level ${client.container.levelCache[cmd.conf.permLevel]} (${cmd.conf.permLevel})`); - } else { - return; - } + // Due to the nature of interactions we **must** respond to them otherwise + // they will error out because we didn't respond to them. + return await interaction.reply({ + content: `This command can only be used by ${cmd.conf.permLevel}'s only`, + // This will basically set the ephemeral response to either announce + // to everyone, or just the command executioner. But we **HAVE** to + // respond. + ephemeral: settings.systemNotice !== "true" + }); } // If everything checks out, run the command