diff --git a/package.json b/package.json index 7a7967a..69190c0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "start": "node index.js", - "dev": "node --trace-warnings --unhandled-rejections=strict index.js", + "dev": "nodemon --delay 10 --trace-warnings --unhandled-rejections=strict index.js", "debug": "node --trace-warnings --inspect index.js", "update": "git pull && cd api && yarn update", "test": "jest --detectOpenHandles", diff --git a/src/constants/FilterPresets.json b/src/constants/FilterPresets.json index daecc39..2e574b3 100644 --- a/src/constants/FilterPresets.json +++ b/src/constants/FilterPresets.json @@ -416,6 +416,8 @@ "seamen", "coming", "seaman", + "seus", + "seuss", "rock", "dock", "lock", diff --git a/src/localization/en_gb/commands/en_gb_administration.lang b/src/localization/en_gb/commands/en_gb_administration.lang index 3a52bc4..c559b22 100644 --- a/src/localization/en_gb/commands/en_gb_administration.lang +++ b/src/localization/en_gb/commands/en_gb_administration.lang @@ -44,6 +44,9 @@ Global permissions [COMMAND_PERMISSIONS_SHOW_TITLE] Granted permissions +[COMMAND_PERMISSIONS_DESC] +Showing permissions {forin} **{target}** + [COMMAND_PERMISSIONS_NO_PERMS] No permissions granted diff --git a/src/localization/en_gb/commands/en_gb_moderation.lang b/src/localization/en_gb/commands/en_gb_moderation.lang index 4c6836a..c617f79 100644 --- a/src/localization/en_gb/commands/en_gb_moderation.lang +++ b/src/localization/en_gb/commands/en_gb_moderation.lang @@ -71,6 +71,9 @@ It seems like the mute role was deleted, use the command `-set mute` for more in [COMMAND_MUTE_MISSING_MODERATE_PERM] Missing permissions to timeout users (Moderate Member) +[COMMAND_MUTE_HIERARCHY_ERROR] +the bot cannot timeout a user above its highest role + [COMMAND_MUTE_MISSING_MANAGEROLE_PERM] Missing permissions to manage roles. @@ -303,6 +306,10 @@ the provided role(s) are higher than the bot, I cannot add them the provided role(s) are not on the grantable list //History Command +[COMMAND_HISTORY_HELP] +Display moderation history in the server. +Narrow the search down by using the parameters below. + [COMMAND_HISTORY_HISTORY] Display moderation history for the server or for certain users. @@ -332,6 +339,9 @@ Failed to display cases in one embed, try a smaller page size. for {targets} //target{plural}: +[COMMAND_HISTORY_SUCCESSMODERATOR] + by {moderator} + [COMMAND_HISTORY_NO_EXPORT_PERMS] Must be admin to export moderation history. diff --git a/src/localization/en_gb/commands/en_gb_utility.lang b/src/localization/en_gb/commands/en_gb_utility.lang index d4fb42f..f407399 100644 --- a/src/localization/en_gb/commands/en_gb_utility.lang +++ b/src/localization/en_gb/commands/en_gb_utility.lang @@ -1,3 +1,10 @@ +[COMMAND_PING_HELP] +Check if the bot is online. + +[COMMAND_AVATAR_HELP] +Display a user's avatar. +Use the member option to display their server avatar. + [COMMAND_AVATAR_FORMATERROR] Unable to find an avatar with those arguments, try a different size or format. @@ -37,6 +44,8 @@ You have no active reminders. The content in your reminder was filtered. // Poll command +[COMMAND_POLL_HELP] +Have the bot send a poll message in a channel with an optional duration. [COMMAND_POLL_QUESTIONS] Please respond with question {number}. diff --git a/src/localization/en_gb/general/en_gb_general.lang b/src/localization/en_gb/general/en_gb_general.lang index c513f7d..8ef3c18 100644 --- a/src/localization/en_gb/general/en_gb_general.lang +++ b/src/localization/en_gb/general/en_gb_general.lang @@ -20,6 +20,9 @@ Required Permissions Command takes no arguments // Generic setting field names +[GENERAL_PREFIX] +》 Prefix + [GENERAL_STATUS] 》 Status @@ -131,3 +134,11 @@ switch({toggle}) { 'off'; break; } + +[FOR_IN_TOGGLE] +switch({toggle}) { + case true: + 'for'; break; + case false: + 'in'; break; +} \ No newline at end of file diff --git a/src/localization/en_gb/general/en_gb_inhibitors.lang b/src/localization/en_gb/general/en_gb_inhibitors.lang index 130c94a..2843996 100644 --- a/src/localization/en_gb/general/en_gb_inhibitors.lang +++ b/src/localization/en_gb/general/en_gb_inhibitors.lang @@ -26,4 +26,4 @@ The command **{command}** requires the __bot__ to have permissions to use. The command **{command}** can only be run by developers. [INHIBITOR_GUILDONLY_ERROR] -The command **{command}** is only available in servers. \ No newline at end of file +The command **{command}** is only available in servers. \ No newline at end of file diff --git a/src/localization/en_gb/general/en_gb_moderation.lang b/src/localization/en_gb/general/en_gb_moderation.lang index 4e7f822..3b955f0 100644 --- a/src/localization/en_gb/general/en_gb_moderation.lang +++ b/src/localization/en_gb/general/en_gb_moderation.lang @@ -7,6 +7,9 @@ You can increase the amount of targets by upgrading to premium. [INFRACTION_ERROR] an error occured +[INFRACTION_PROTECTIONPOSITIONERROR_SAME] +they have the same role position as you + [INFRACTION_PROTECTIONPOSITIONERROR] they have hierarchy over you diff --git a/src/localization/en_gb/observers/en_gb_commandHandler.lang b/src/localization/en_gb/observers/en_gb_commandHandler.lang index 264ece6..781c11a 100644 --- a/src/localization/en_gb/observers/en_gb_commandHandler.lang +++ b/src/localization/en_gb/observers/en_gb_commandHandler.lang @@ -5,15 +5,24 @@ This is an issue that should be reported to a bot developer. [O_COMMANDHANDLER_GUILDONLY] This command can only be run in servers. +[O_COMMANDHANDLER_GUILDONLY_OPT] +The option `{option}` is only valid in servers. + [O_COMMANDHANDLER_TYPEINTEGER] The command option {option} requires an integer between `{min}` and `{max}`. +[O_COMMANDHANDLER_TYPEBOOLEAN] +The command option {option} requires a boolean resolveable (e.g. anything that can be interpreted as true or false) + [O_COMMANDHANDLER_TYPECOMMANDS] The command option {option} requires command resolveables (e.g. `command:ping`, `command:history`) [O_COMMANDHANDLER_TYPECOMMAND] The command option {option} requires a command resolveable (e.g. `command:ping`) +[O_COMMANDHANDLER_TYPEMODULE] +The command option {option} requires a module resolveable (e.g. moderation) + [O_COMMANDHANDLER_TYPESTRING] The command option {option} requires a string. @@ -23,6 +32,9 @@ The command option {option} requires a date in the format `YYYY/MM/DD` [O_COMMANDHANDLER_TYPETIME] The command option {option} requires a timestring (e.g. 5 min, 2w). +[O_COMMANDHANDLER_TYPEUSER] +The command option {option} requires a user. + [O_COMMANDHANDLER_TYPEUSERS] The command option {option} requires users. @@ -80,3 +92,14 @@ Command Error [O_COMMANDHANDLER_COMMAND_NORESPONSE] Command returned no response. This should not happen and is likely a bug. +[O_COMMANDHANDLER_UNRECOGNISED_FLAG] +Unrecognised flag: `{flag}` +See command help for valid flags. + +[O_COMMANDHANDLER_UNRECOGNISED_OPTIONS] +Unrecognised options: `{opts}` +See command help for valid options. + +[O_COMMANDHANDLER_INVALID_CHOICE] +`{value}` is an invalid choice for **{option}**. +Valid choices are `{choices}`. \ No newline at end of file diff --git a/src/localization/en_gb/settings/en_gb_administration.lang b/src/localization/en_gb/settings/en_gb_administration.lang index c3d8e3d..decf442 100644 --- a/src/localization/en_gb/settings/en_gb_administration.lang +++ b/src/localization/en_gb/settings/en_gb_administration.lang @@ -1,3 +1,6 @@ +[SETTING_TEXTCOMMANDS_HELP] +Enable or disable text commands or change the prefix. + [SETTING_PERMISSIONS_HELP] Configure which set of permissions the bot works with. diff --git a/src/middleware/ApiClientUtil.js b/src/middleware/ApiClientUtil.js index f55c381..23d2359 100644 --- a/src/middleware/ApiClientUtil.js +++ b/src/middleware/ApiClientUtil.js @@ -94,15 +94,17 @@ class ApiClientUtil { const { guildId } = message; const evalFunc = (client, { guildId }) => { - const guild = client.guilds.cache.get(guildId); - if (!guild) return null; - const wrapper = new client.wrapperClasses.GuildWrapper(client, guild); - return wrapper.toJSON(); + try { + const wrapper = client.getGuildWrapper(guildId); + return wrapper.toJSON(); + } catch { + return null; + } }; this.client.logger.debug(`guild-live request - shard: ${message.shard}, message id ${message.id}`); const result = await this.client.shardingManager.broadcastEval(evalFunc, { context: { guildId } }); - const guild = result.find((elem) => elem !== undefined); + const guild = result.find((elem) => elem !== null); return guild; } diff --git a/src/middleware/Logger.js b/src/middleware/Logger.js index 73e756a..7d5e717 100644 --- a/src/middleware/Logger.js +++ b/src/middleware/Logger.js @@ -70,7 +70,7 @@ class Logger { const maximumCharacters = Math.max(...Constants.Types.map((t) => t.length)); const spacers = maximumCharacters - type.length; - const text = `${chalk[color](type)}${" ".repeat(spacers)} ${header} : ${string}`; + const text = `${chalk[color](type)}${" ".repeat(spacers)} ${header}: ${string}`; const strippedText = text.replace(stripRegex, ''); console.log(text); //eslint-disable-line no-console @@ -86,6 +86,7 @@ class Logger { webhook(text, type) { + if (!this._webhook) return; const message = text.replace(new RegExp(process.env.DISCORD_TOKEN, 'gu'), '') .replace(new RegExp(username, 'gu'), ''); diff --git a/src/middleware/rest/SlashCommandManager.js b/src/middleware/rest/SlashCommandManager.js index 400f75f..7955bcb 100644 --- a/src/middleware/rest/SlashCommandManager.js +++ b/src/middleware/rest/SlashCommandManager.js @@ -3,6 +3,7 @@ const { Routes } = require('discord-api-types/v9'); const fs = require('fs'); const path = require('path'); const hash = require('object-hash'); +const { inspect } = require('util'); class SlashCommandManager { @@ -43,7 +44,7 @@ class SlashCommandManager { this.client.logger.write('info', `Commands hash: ${cmdHash}, ${guilds.length} out of date`); if (!guilds.length) return; const promises = []; - //console.log(JSON.stringify(commands)); + //fs.writeFileSync(path.join(process.cwd(), 'commands.json'), JSON.stringify(commands)); for(const guild of guilds) { promises.push(this.rest.put( Routes.applicationGuildCommands(clientId, guild), @@ -72,6 +73,10 @@ class SlashCommandManager { str += `${command.name}: `; const options = Object.keys(invalid[key].options); for (const optKey of options) { + if (!command.options[optKey]) { + this.client.logger.warn(`Missing properties for ${command.name}: ${optKey}\nOptions: ${inspect(command.options)}`); + continue; + } str += `${command.options[optKey].name}\t`; } str += `\n\n`; diff --git a/src/structure/DiscordClient.js b/src/structure/DiscordClient.js index b34c9b6..f38970c 100644 --- a/src/structure/DiscordClient.js +++ b/src/structure/DiscordClient.js @@ -91,8 +91,8 @@ class DiscordClient extends Client { // this.logger.error(`Uncaught exception:\n${err.stack || err}`); // }); - process.on('unhandledRejection', (err, reason) => { - this.logger.error(`Unhandled rejection:\n${err?.stack || err}\n${inspect(reason)}`); + process.on('unhandledRejection', (err) => { + this.logger.error(`Unhandled rejection:\n${err?.stack || err}`); }); process.on('message', this._handleMessage.bind(this)); @@ -309,6 +309,14 @@ class DiscordClient extends Client { } + format(index, params = {}, opts = {}) { + const { + code = false, + language = 'en_gb' + } = opts; + return this.localeLoader.format(language, index, params, code); + } + getGuildWrapper(id) { if (this.guildWrappers.has(id)) return this.guildWrappers.get(id); diff --git a/src/structure/client/RateLimiter.js b/src/structure/client/RateLimiter.js index 9035b3c..5d839d4 100644 --- a/src/structure/client/RateLimiter.js +++ b/src/structure/client/RateLimiter.js @@ -37,7 +37,7 @@ class RateLimiter { if (!channel || !(channel instanceof TextChannel)) reject(new Error('Missing channel')); if (!message || !(message instanceof Message)) reject(new Error('Missing message')); - if (!channel.permissionsFor(channel.guild.members.me).has('ManageMessages')) reject(new Error('Missing permission ManageMessages')); + if (!channel.permissionsFor(this.client.user).has('ManageMessages')) reject(new Error('Missing permission ManageMessages')); if (!this.deleteQueue[channel.id]) this.deleteQueue[channel.id] = []; this.deleteQueue[channel.id].push({ message, resolve, reject }); @@ -106,7 +106,7 @@ class RateLimiter { if (!channel || !(channel instanceof TextChannel)) reject(new Error('Missing channel.')); if (!message || !message.length) reject(new Error('Missing message.')); - if (!channel.permissionsFor(channel.guild.members.me).has('SendMessages')) reject(new Error('Missing permission SendMessages')); + if (!channel.permissionsFor(this.client.user).has('SendMessages')) reject(new Error('Missing permission SendMessages')); //Initiate queue if (!this.sendQueue[channel.id]) this.sendQueue[channel.id] = []; @@ -173,7 +173,7 @@ class RateLimiter { return new Promise((resolve, reject) => { if (!channel || !(channel instanceof TextChannel)) reject(new Error('Missing channel')); - if (!channel.permissionsFor(channel.guild.members.me).has('SendMessages')) reject(new Error('Missing permission SendMessages')); + if (!channel.permissionsFor(this.client.user).has('SendMessages')) reject(new Error('Missing permission SendMessages')); if (!message) reject(new Error('Missing message')); if (limit === null) limit = 15; diff --git a/src/structure/client/Resolver.js b/src/structure/client/Resolver.js index 946c0f1..35d2e17 100644 --- a/src/structure/client/Resolver.js +++ b/src/structure/client/Resolver.js @@ -346,7 +346,7 @@ class Resolver { const match = resolveable.match(id); const [, ch] = match; - const channel = await this.client.channels.fetch(ch).catch((e) => { }); //eslint-disable-line no-empty, no-empty-function, no-unused-vars + const channel = await CM.fetch(ch).catch((e) => { }); //eslint-disable-line no-empty, no-empty-function, no-unused-vars if (channel && filter(channel)) resolved.push(channel); } else if (name.test(resolveable)) { diff --git a/src/structure/client/wrappers/GuildWrapper.js b/src/structure/client/wrappers/GuildWrapper.js index 2c15a71..f639570 100644 --- a/src/structure/client/wrappers/GuildWrapper.js +++ b/src/structure/client/wrappers/GuildWrapper.js @@ -72,7 +72,7 @@ class GuildWrapper { startedIn = await this.resolveChannel(startedIn); const pollChannel = await this.resolveChannel(channel); if (pollChannel) { - const msg = await pollChannel.messages.fetch(message); + const msg = await pollChannel.messages.fetch(message).catch(() => null); if (msg) { const { reactions } = msg; const reactionEmojis = questions.length ? PollReactions.Multi : PollReactions.Single; @@ -360,7 +360,7 @@ class GuildWrapper { } get prefix() { - return this._settings.prefix || this.client.prefix; + return this._settings.textcommands.prefix || this.client.prefix; } get available() { diff --git a/src/structure/client/wrappers/InteractionWrapper.js b/src/structure/client/wrappers/InteractionWrapper.js index 5b0d859..b2c60c7 100644 --- a/src/structure/client/wrappers/InteractionWrapper.js +++ b/src/structure/client/wrappers/InteractionWrapper.js @@ -217,7 +217,7 @@ class InteractionWrapper { } isContextMenu() { - return this.interaction.isContextMenu(); + return this.interaction.isContextMenuCommand(); } isSelectMenu() { diff --git a/src/structure/client/wrappers/InvokerWrapper.js b/src/structure/client/wrappers/InvokerWrapper.js index 7d6f5fc..4e341b5 100644 --- a/src/structure/client/wrappers/InvokerWrapper.js +++ b/src/structure/client/wrappers/InvokerWrapper.js @@ -118,7 +118,7 @@ class InvokerWrapper { disableMentions: opts.disableMentions }; - if (opts.editReply) await this.editReply(data); + if (opts.editReply || this.deferred) await this.editReply(data); else await this.reply(data) //this.channel.send(data) .then((msg) => { if (opts.delete) msg.delete(); diff --git a/src/structure/client/wrappers/MessageWrapper.js b/src/structure/client/wrappers/MessageWrapper.js index 9d71f1f..d32b2e5 100644 --- a/src/structure/client/wrappers/MessageWrapper.js +++ b/src/structure/client/wrappers/MessageWrapper.js @@ -90,6 +90,7 @@ class MessageWrapper { async editReply(options) { if (!this._reply) throw new Error('Message not replied to'); + if (!options.allowedMentions) options.allowedMentions = { repliedUser: false }; // Disables the mention in the inline reply return this._reply.edit(options); } diff --git a/src/structure/components/commands/administration/Administration.js b/src/structure/components/commands/administration/Administration.js index 36bf678..fe7d977 100644 --- a/src/structure/components/commands/administration/Administration.js +++ b/src/structure/components/commands/administration/Administration.js @@ -5,6 +5,7 @@ class AdministrationModule extends SettingsCommand { constructor(client) { super(client, { name: 'administration', + aliases: ['admin'], description: 'Configure the administrative settings', module: 'administration' }); diff --git a/src/structure/components/commands/administration/Import.js b/src/structure/components/commands/administration/Import.js index a657e9f..bb1917b 100644 --- a/src/structure/components/commands/administration/Import.js +++ b/src/structure/components/commands/administration/Import.js @@ -39,7 +39,8 @@ class ImportCommand extends SlashCommand { }, { name: 'overwrite', description: 'Whether any existing logs should be overwritten by the imports. By default new ones are bumped', - type: 'BOOLEAN' + type: 'BOOLEAN', + flag: true, valueOptional: true, defaultValue: true }] }], clientPermissions: ['ManageWebhooks'] diff --git a/src/structure/components/commands/administration/Modstats.js b/src/structure/components/commands/administration/Modstats.js index b73bad3..156487d 100644 --- a/src/structure/components/commands/administration/Modstats.js +++ b/src/structure/components/commands/administration/Modstats.js @@ -46,7 +46,7 @@ class ModstatsCommand extends SlashCommand { const data = await this.client.mongodb.infractions.find(query, { projection: { executor: 1, type: 1 } }); for (const log of data) { - if (log.executor === guild.members.me.id) continue; + if (log.executor === this.client.user.id) continue; if (!result[log.executor]) { const user = await this.client.resolveUser(log.executor); result[log.executor] = { name: user.tag }; diff --git a/src/structure/components/commands/administration/Permissions.js b/src/structure/components/commands/administration/Permissions.js index eecb64b..9d22a0c 100644 --- a/src/structure/components/commands/administration/Permissions.js +++ b/src/structure/components/commands/administration/Permissions.js @@ -27,7 +27,8 @@ class PermissionsCommand extends SlashCommand { name: 'channel', description: 'The channel(s) in which this permission is granted to user or role', type: 'TEXT_CHANNELS', - dependsOn: ['permission'] + dependsOn: ['permission'], + flag: true }, { name: 'role', @@ -64,6 +65,7 @@ class PermissionsCommand extends SlashCommand { name: 'channel', description: 'The channel(s) to reset', type: 'TEXT_CHANNELS', + flag: true }, { name: 'role', @@ -233,6 +235,7 @@ class PermissionsCommand extends SlashCommand { const fields = []; let update = false; + const target = member?.value || role?.value || channel?.value; if (member || role) { @@ -340,6 +343,7 @@ class PermissionsCommand extends SlashCommand { if(!fields.length) return { index: 'COMMAND_PERMISSIONS_NO_PERMS' }; const embed = { + description: target ? interaction.format('COMMAND_PERMISSIONS_DESC', { target: target.tag || target.name || 'any', forin: interaction.format('FOR_IN_TOGGLE', { toggle: target.type === undefined }, { code: true }) }) : '', title: interaction.format('COMMAND_PERMISSIONS_SHOW_TITLE'), fields: [ ] }; diff --git a/src/structure/components/commands/administration/Settings.js b/src/structure/components/commands/administration/Settings.js index 9ef32de..0e4c4a1 100644 --- a/src/structure/components/commands/administration/Settings.js +++ b/src/structure/components/commands/administration/Settings.js @@ -1,4 +1,4 @@ -const { SlashCommand, CommandOption } = require("../../../interfaces"); +const { SlashCommand } = require("../../../interfaces"); const { Util } = require('../../../../utilities'); class SettingsCommand extends SlashCommand { @@ -10,11 +10,11 @@ class SettingsCommand extends SlashCommand { description: 'View settings', options: [ // Probably add reset options here too - new CommandOption({ + { name: 'list', description: 'List available settings', type: 'SUB_COMMAND' - }) + } ], memberPermissions: ['ManageGuild'] }); diff --git a/src/structure/components/commands/developer/Eval.js b/src/structure/components/commands/developer/Eval.js index efb7d39..876005b 100644 --- a/src/structure/components/commands/developer/Eval.js +++ b/src/structure/components/commands/developer/Eval.js @@ -13,16 +13,20 @@ class EvalCommand extends Command { name: 'eval', aliases: ['e', 'evaluate'], restricted: true, - module: 'developer' + module: 'developer', + options: [ + { name: 'code' }, + { name: 'async', flag: true, valueOptional: true, defaultValue: true } + ] }); } - async execute(invoker, { parameters: params }) { + async execute(invoker, { code, async }) { const args = {}; // Temporary args until I figure out how I want to implement them - params = params.join(' '); - if (args.async) params = `(async () => {${params}})()`; + let params = code.value; + if (async?.value) params = `(async () => {${params}})()`; const { guild, author, member, client } = invoker; //eslint-disable-line no-unused-vars let response = null; diff --git a/src/structure/components/commands/developer/Stats.js b/src/structure/components/commands/developer/Stats.js index 4cfaa43..9ebc1a5 100644 --- a/src/structure/components/commands/developer/Stats.js +++ b/src/structure/components/commands/developer/Stats.js @@ -17,7 +17,8 @@ class StatsCommand extends SlashCommand { name: 'log', type: 'BOOLEAN', types: ['FLAG'], - description: 'Logs the output in the console.' + description: 'Logs the output in the console.', + flag: true, valueOptional: true, defaultValue: true } ], clientPermissions: ['SendMessages', 'EmbedLinks'], diff --git a/src/structure/components/commands/information/Commands.js b/src/structure/components/commands/information/Commands.js index 00f7d8c..d2d91cd 100644 --- a/src/structure/components/commands/information/Commands.js +++ b/src/structure/components/commands/information/Commands.js @@ -15,7 +15,7 @@ class Commands extends SlashCommand { type: 'MODULE', description: ['List commands from a specific module'] }], - memberPermissions: ['ManageGuild'], + memberPermissions: [], guildOnly: true }); } diff --git a/src/structure/components/commands/moderation/Ban.js b/src/structure/components/commands/moderation/Ban.js index 0748166..1df0e06 100644 --- a/src/structure/components/commands/moderation/Ban.js +++ b/src/structure/components/commands/moderation/Ban.js @@ -18,7 +18,8 @@ class BanCommand extends ModerationCommand { type: 'INTEGER', description: 'How many days worth of messages to prune', minimum: 1, - maximum: 7 + maximum: 7, + flag: true }, { name: 'users', type: 'USERS', diff --git a/src/structure/components/commands/moderation/Case.js b/src/structure/components/commands/moderation/Case.js index ad1aa14..439eeee 100644 --- a/src/structure/components/commands/moderation/Case.js +++ b/src/structure/components/commands/moderation/Case.js @@ -23,7 +23,8 @@ class CaseCommand extends SlashCommand { 'Print out more detailed information about the case', 'List changes to the case' ], - depeondsOn: ['id'] + depeondsOn: ['id'], + flag: true, valueOptional: true, defaultValue: true }], guildOnly: true, showUsage: true, diff --git a/src/structure/components/commands/moderation/Edit.js b/src/structure/components/commands/moderation/Edit.js index 3133dcb..eb47fd0 100644 --- a/src/structure/components/commands/moderation/Edit.js +++ b/src/structure/components/commands/moderation/Edit.js @@ -24,14 +24,15 @@ class EditCommand extends SlashCommand { name: 'points', type: 'INTEGER', description: 'New point value for case', - minimum: 0, maximum: 100 + minimum: 0, maximum: 100, flag: true }, { name: ['expiration', 'duration'], type: 'TIME', description: [ 'New expiration for points, starts from the time the infraction was issued', 'Duration if the infraction is timed' - ] + ], + flag: true }] }); } diff --git a/src/structure/components/commands/moderation/History.js b/src/structure/components/commands/moderation/History.js index 96225a7..9a3346d 100644 --- a/src/structure/components/commands/moderation/History.js +++ b/src/structure/components/commands/moderation/History.js @@ -24,7 +24,8 @@ class HistoryCommand extends SlashCommand { options: [{ name: ['before', 'after'], type: 'DATE', - description: 'Filter by a date, must be in YYYY/MM/DD or YYYY-MM-DD format' + description: 'Filter by a date, must be in YYYY/MM/DD or YYYY-MM-DD format', + flag: true }, { name: ['verbose', 'oldest', 'export', 'private'], description: [ @@ -33,26 +34,35 @@ class HistoryCommand extends SlashCommand { 'Export the list of infractions', 'DM the command response' ], - type: 'BOOLEAN' + type: 'BOOLEAN', + flag: true, valueOptional: true, defaultValue: true }, { name: 'type', description: 'Filter infractions by type', choices: Infractions.map((inf) => { return { name: inf.toLowerCase(), value: inf }; - }) + }), + flag: true }, { name: ['pagesize', 'page'], description: ['Amount of infractions to list per page', 'Page to select'], type: 'INTEGER', - minimum: 1 + minimum: 1, + flag: true }, { - name: ['user', 'moderator'], // - description: ['User whose infractions to query, overrides channel if both are given', 'Query by moderator'], + name: 'user', + description: 'User whose infractions to query, overrides channel if both are given', type: 'USER' + }, { + name: 'moderator', + description: 'Query by moderator', + type: 'USER', + flag: true }, { name: 'channel', description: 'Infractions done on channels, e.g. slowmode, lockdown', - type: 'TEXT_CHANNEL' + type: 'TEXT_CHANNEL', + flag: true }] }); } @@ -95,6 +105,7 @@ class HistoryCommand extends SlashCommand { limit: pageSize }); + const me = await guild.resolveMember(this.client.user); const embed = { author: { name: 'Infraction History', @@ -104,7 +115,7 @@ class HistoryCommand extends SlashCommand { footer: { text: `• Page ${_page}/${maxPage} | ${resultsAmt} Results` }, - color: invoker.guild.members.me.roles.highest.color + color: me.roles.highest.color }; if (invoker.guild._settings.modpoints.enabled) { @@ -180,12 +191,18 @@ class HistoryCommand extends SlashCommand { const type = invoker.format('COMMAND_HISTORY_SUCCESSTYPE', { old: oldest?.value || false }, { code: true }); try { + let targets = ''; + if (user || channel) targets = invoker.format('COMMAND_HISTORY_SUCCESSTARGETS', { + //plural: parsed.length === 1 ? '' : 's', + targets: `**${Util.escapeMarkdown(user?.value.tag || channel?.value.name)}**` //parsed.map((p) => `**${Util.escapeMarkdown(p.display)}**`).join(' ') + }); + else if (moderator) targets = invoker.format('COMMAND_HISTORY_SUCCESSMODERATOR', { + moderator: `**${Util.escapeMarkdown(moderator.value.tag)}**` + }); + return { content: invoker.format('COMMAND_HISTORY_SUCCESS', { - targets: user || channel ? invoker.format('COMMAND_HISTORY_SUCCESSTARGETS', { - //plural: parsed.length === 1 ? '' : 's', - targets: `**${Util.escapeMarkdown(user?.value.tag || channel?.value.name)}**` //parsed.map((p) => `**${Util.escapeMarkdown(p.display)}**`).join(' ') - }) : '', + targets, type }), emoji: 'success', diff --git a/src/structure/components/commands/moderation/Mute.js b/src/structure/components/commands/moderation/Mute.js index 7f78540..c0848e8 100644 --- a/src/structure/components/commands/moderation/Mute.js +++ b/src/structure/components/commands/moderation/Mute.js @@ -27,8 +27,9 @@ class MuteCommand extends ModerationCommand { const { guild } = interaction; const settings = await guild.settings(); const { type } = settings.mute; - if (type === 3 && !guild.members.me.permissions.has('ModerateMembers')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ModerateMembers' } }); - else if (!guild.members.me.permissions.has('ManageRoles')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ManageRoles' } }); + const me = await guild.resolveMember(this.client.user); + if (type === 3 && !me.permissions.has('ModerateMembers')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ModerateMembers' } }); + else if (!me.permissions.has('ManageRoles')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ManageRoles' } }); return this.client.moderationManager.handleInfraction(Mute, interaction, { targets: users.value, diff --git a/src/structure/components/commands/moderation/Nickname.js b/src/structure/components/commands/moderation/Nickname.js index 5d52dc5..f015335 100644 --- a/src/structure/components/commands/moderation/Nickname.js +++ b/src/structure/components/commands/moderation/Nickname.js @@ -12,7 +12,7 @@ class NicknameCommand extends ModerationCommand { name: 'name', description: 'The new nickname to give', type: 'STRING', - required: true + required: true, flag: true }], memberPermissions: ['ManageNicknames'], clientPermissions: ['ManageNicknames'], diff --git a/src/structure/components/commands/moderation/Prune.js b/src/structure/components/commands/moderation/Prune.js index 10530fb..d3f4ffb 100644 --- a/src/structure/components/commands/moderation/Prune.js +++ b/src/structure/components/commands/moderation/Prune.js @@ -1,6 +1,10 @@ const { ModerationCommand, CommandError } = require('../../../interfaces'); const { Prune } = require('../../../infractions'); +const flag = true, + valueOptional = true, + defaultValue = true; + class PruneCommand extends ModerationCommand { constructor(client) { @@ -26,56 +30,69 @@ class PruneCommand extends ModerationCommand { }, { name: 'silent', type: 'BOOLEAN', - description: 'Prune quietly' + description: 'Prune quietly', + flag, valueOptional, defaultValue }, { name: 'bots', type: 'BOOLEAN', - description: 'Prune messages from bots' + description: 'Prune messages from bots', + flag, valueOptional, defaultValue }, { name: 'humans', type: 'BOOLEAN', - description: 'Prune messages from humans' + description: 'Prune messages from humans', + flag, valueOptional, defaultValue }, { name: 'contains', type: 'STRING', - description: 'Text to look for messages by' + description: 'Text to look for messages by', + flag }, { name: 'startswith', type: 'STRING', - description: 'Text the messages to delete start with' + description: 'Text the messages to delete start with', + flag }, { name: 'endswith', type: 'STRING', - description: 'Text the messages to delete end with' + description: 'Text the messages to delete end with', + flag }, { name: 'text', type: 'BOOLEAN', - description: 'Only delete messages containing text' + description: 'Only delete messages containing text', + flag, valueOptional, defaultValue }, { name: 'invites', type: 'BOOLEAN', - description: 'Delete messages containing invites' + description: 'Delete messages containing invites', + flag, valueOptional, defaultValue }, { name: 'links', type: 'BOOLEAN', - description: 'Delete messages containing links' + description: 'Delete messages containing links', + flag, valueOptional, defaultValue }, { name: 'emojis', type: 'BOOLEAN', - description: 'Prune messages cotaining emojis' + description: 'Prune messages cotaining emojis', + flag, valueOptional, defaultValue }, { name: 'after', type: 'STRING', - description: 'ID of message after which to start deleting' + description: 'ID of message after which to start deleting', + flag }, { name: 'before', type: 'STRING', - description: 'ID of message before which to start deleting' + description: 'ID of message before which to start deleting', + flag }, { name: 'logic', // type: '', choices: [{ name: 'AND', value: 'AND' }, { name: 'OR', value: 'OR' }], - description: 'Logic type to use for combining options' + description: 'Logic type to use for combining options', + flag }, { name: 'reason', type: 'STRING', diff --git a/src/structure/components/commands/moderation/Resolve.js b/src/structure/components/commands/moderation/Resolve.js index f38bec7..1d988f7 100644 --- a/src/structure/components/commands/moderation/Resolve.js +++ b/src/structure/components/commands/moderation/Resolve.js @@ -22,7 +22,8 @@ class ResolveCommand extends SlashCommand { }, { name: 'notify', description: 'Attempt to notify the user about the resolve, may not always be possible', - type: 'BOOLEAN' + type: 'BOOLEAN', + flag: true, valueOptional: true, defaultValue: true }] // Potentially add another option to enable a range of cases }); } diff --git a/src/structure/components/commands/moderation/Staff.js b/src/structure/components/commands/moderation/Staff.js index 7290651..0035b9d 100644 --- a/src/structure/components/commands/moderation/Staff.js +++ b/src/structure/components/commands/moderation/Staff.js @@ -27,7 +27,7 @@ class StaffCommand extends SlashCommand { // const role = await guild.resolveRole(staff.role); // if(!role) return invoker.editReply({ index: 'COMMAND_STAFF_ERROR', emoji: 'failure' }); - await channel.send({ + return channel.send({ content: guild.format('COMMAND_STAFF_SUMMON', { author: author.tag, role: staff.role }), allowedMentions: { parse: ['roles'] } // roles: [staff.role], }); diff --git a/src/structure/components/commands/moderation/Unmute.js b/src/structure/components/commands/moderation/Unmute.js index 0eae2b8..b55a09c 100644 --- a/src/structure/components/commands/moderation/Unmute.js +++ b/src/structure/components/commands/moderation/Unmute.js @@ -23,8 +23,9 @@ class UnmuteCommand extends ModerationCommand { const { guild } = interaction; const settings = await guild.settings(); const { type } = settings.mute; - if (type === 3 && !guild.members.me.permissions.has('ModerateMembers')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ModerateMembers' } }); - else if (!guild.members.me.permissions.has('ManageRoles')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ManageRoles' } }); + const me = await guild.resolveMember(this.client.user); + if (type === 3 && !me.permissions.has('ModerateMembers')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ModerateMembers' } }); + else if (!me.permissions.has('ManageRoles')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ManageRoles' } }); return this.client.moderationManager.handleInfraction(Unmute, interaction, { targets: users.value, diff --git a/src/structure/components/commands/moderation/Unresolve.js b/src/structure/components/commands/moderation/Unresolve.js index e1a9373..49917a9 100644 --- a/src/structure/components/commands/moderation/Unresolve.js +++ b/src/structure/components/commands/moderation/Unresolve.js @@ -12,8 +12,9 @@ const { SlashCommand, Infraction } = require("../../../interfaces"); } */ -class ResolveCommand extends SlashCommand { +class UnresolveCommand extends SlashCommand { + // TODO: make unresolving enact the infraction again constructor(client) { super(client, { name: 'unresolve', @@ -50,4 +51,4 @@ class ResolveCommand extends SlashCommand { } -module.exports = ResolveCommand; \ No newline at end of file +module.exports = UnresolveCommand; \ No newline at end of file diff --git a/src/structure/components/commands/utility/Avatar.js b/src/structure/components/commands/utility/Avatar.js index 9d3c49f..53404dc 100644 --- a/src/structure/components/commands/utility/Avatar.js +++ b/src/structure/components/commands/utility/Avatar.js @@ -13,14 +13,16 @@ class AvatarCommand extends SlashCommand { // type: 'INTEGER', choices: [16, 32, 64, 128, 256, 512, 1024, 2048].map((i) => { return { name: `${i}`, value: `${i}` }; - }) + }), + flag: true }, { name: 'format', description: 'Image format', // type: 'STRING' choices: ['webp', 'png', 'jpeg', 'jpg', 'gif'].map((i) => { return { name: i, value: i }; - }) + }), + flag: true }, { name: 'user', description: 'Use this for the user\'s global avatar', @@ -28,7 +30,8 @@ class AvatarCommand extends SlashCommand { }, { name: 'member', description: 'Use this for the user\'s server avatar', - type: 'MEMBER' + type: 'MEMBER', + flag: true }] }); } diff --git a/src/structure/components/commands/utility/Poll.js b/src/structure/components/commands/utility/Poll.js index 25236a9..6cf15a9 100644 --- a/src/structure/components/commands/utility/Poll.js +++ b/src/structure/components/commands/utility/Poll.js @@ -57,7 +57,7 @@ class PollCommand extends SlashCommand { const questions = []; const _channel = channel?.value || invoker.channel; - const botMissing = _channel.permissionsFor(guild.members.me).missing(['SendMessages', 'EmbedLinks']); + const botMissing = _channel.permissionsFor(this.client.user).missing(['SendMessages', 'EmbedLinks']); const userMissing = _channel.permissionsFor(member).missing(['SendMessages']); if (botMissing.length) return invoker.editReply({ index: 'COMMAND_POLL_BOT_PERMS', params: { missing: botMissing.join(', '), channel: _channel.id } }); if (userMissing.length) return invoker.editReply({ index: 'COMMAND_POLL_USER_PERMS', params: { missing: userMissing.join(', '), channel: _channel.id } }); @@ -65,10 +65,10 @@ class PollCommand extends SlashCommand { for (let i = 0; i < choices.value; i++) { const response = await invoker.promptMessage({ content: guild.format(`COMMAND_POLL_QUESTION${choices.value === 1 ? '' : 'S'}`, { number: i + 1 }) + '\n' + guild.format('COMMAND_POLL_ADDENDUM'), - time: 90, editReply: true + time: 90, editReply: invoker.replied }); if (!response || !response.content) return invoker.editReply({ index: 'COMMAND_POLL_TIMEOUT' }); - if(invoker.channel.permissionsFor(guild.members.me).has('ManageMessages')) await response.delete().catch(() => null); + if(invoker.channel.permissionsFor(this.client.user).has('ManageMessages')) await response.delete().catch(() => null); const { content } = response; if (content.toLowerCase() === 'stop') break; if (content.toLowerCase() === 'cancel') return invoker.editReply({ index: 'GENERAL_CANCELLED' }); diff --git a/src/structure/components/commands/utility/Selfrole.js b/src/structure/components/commands/utility/Selfrole.js index 11d4c63..5b08f3c 100644 --- a/src/structure/components/commands/utility/Selfrole.js +++ b/src/structure/components/commands/utility/Selfrole.js @@ -34,7 +34,8 @@ class SelfroleCommand extends SlashCommand { const { guild, member } = invoker; const { selfrole } = await guild.settings(); if (!selfrole.roles.length) return { index: 'COMMAND_SELFROLE_NONE', emoji: 'failure' }; - const ownHighest = guild.members.me.roles.highest; + const me = await guild.resolveMember(this.client.user); + const ownHighest = me.roles.highest; const memberRoles = member.roles.cache.map((r) => r.id); const tooHigh = roles?.value.filter((r) => r.position > ownHighest.position); diff --git a/src/structure/components/inhibitors/ClientPermissions.js b/src/structure/components/inhibitors/ClientPermissions.js index 00fe8ef..5587738 100644 --- a/src/structure/components/inhibitors/ClientPermissions.js +++ b/src/structure/components/inhibitors/ClientPermissions.js @@ -13,7 +13,7 @@ class ClientPermissions extends Inhibitor { async execute(invoker, command) { - const missing = invoker.channel.permissionsFor(invoker.guild.members.me).missing(command.clientPermissions); + const missing = invoker.channel.permissionsFor(this.client.user).missing(command.clientPermissions); if (missing.length) return super._fail({ error: true, missing: missing.join(', '), silent: true }); return super._succeed(); diff --git a/src/structure/components/observers/AuditLog.js b/src/structure/components/observers/AuditLog.js index 84e4465..725d57f 100644 --- a/src/structure/components/observers/AuditLog.js +++ b/src/structure/components/observers/AuditLog.js @@ -22,10 +22,10 @@ class AuditLogObserver extends Observer { } - async guildBanAdd({ guild, user, guildWrapper: wrapper }) { + async guildBanAdd({ user, guildWrapper: wrapper }) { const settings = await wrapper.settings(); if (!settings.moderation.channel || !settings.moderation.infractions.includes('BAN')) return undefined; //This is checked by the infraction handling, but it may save resources if checked earlier. - const audit = await this._fetchFirstEntry(guild, user, 'MemberBanAdd'); + const audit = await this._fetchFirstEntry(wrapper, user, 'MemberBanAdd'); if (!audit) return undefined; new Infraction(this.client, { type: 'BAN', @@ -37,10 +37,10 @@ class AuditLogObserver extends Observer { }).handle(); } - async guildBanRemove({ guild, user, guildWrapper: wrapper }) { + async guildBanRemove({ user, guildWrapper: wrapper }) { const settings = await wrapper.settings(); if (!settings.moderation.channel || !settings.moderation.infractions.includes('UNBAN')) return undefined; //This is checked by the infraction handling, but it may save resources if checked earlier. - const audit = await this._fetchFirstEntry(guild, user, 'MemberBanRemove'); + const audit = await this._fetchFirstEntry(wrapper, user, 'MemberBanRemove'); if (!audit) return undefined; new Infraction(this.client, { type: 'UNBAN', @@ -56,7 +56,7 @@ class AuditLogObserver extends Observer { const { guildWrapper: wrapper } = member; const settings = await wrapper.settings(); if (!settings.moderation.channel || !settings.moderation.infractions.includes('KICK')) return undefined; //This is checked by the infraction handling, but it may save resources if checked earlier. - const audit = await this._fetchFirstEntry(member.guild, member.user, 'MemberKick'); + const audit = await this._fetchFirstEntry(wrapper, member.user, 'MemberKick'); if (!audit) return undefined; new Infraction(this.client, { type: 'KICK', @@ -123,7 +123,7 @@ class AuditLogObserver extends Observer { const mutedRole = settings.mute.role; if (!mutedRole) return undefined; - const audit = await this._fetchFirstEntry(newMember.guild, newMember.user, 'MemberRoleUpdate'); + const audit = await this._fetchFirstEntry(wrapper, newMember.user, 'MemberRoleUpdate'); if (!audit) return undefined; let type = null; @@ -148,7 +148,8 @@ class AuditLogObserver extends Observer { } async _fetchFirstEntry(guild, user, type, subtype = null) { - if (!guild.members.me.permissions.has('ViewAuditLog')) return null; + const me = await guild.resolveMember(this.client.user); + if (!me.permissions.has('ViewAuditLog')) return null; type = AuditLogEvent[type]; const audit = await guild.fetchAuditLogs({ limit: 1, type }); if (audit.entries.size === 0) return null; diff --git a/src/structure/components/observers/Automoderation.js b/src/structure/components/observers/Automoderation.js index 00a7bef..47d13b6 100644 --- a/src/structure/components/observers/Automoderation.js +++ b/src/structure/components/observers/Automoderation.js @@ -133,7 +133,7 @@ module.exports = class AutoModeration extends Observer { if (!enabled || roles.some((r) => bypass.includes(r)) || ignore.includes(channel.id)) return; - const missing = channel.permissionsFor(guild.members.me).missing('ManageMessages'); + const missing = channel.permissionsFor(this.client.user).missing('ManageMessages'); if (missing.length) { this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'word', permissions: missing }); return; @@ -432,7 +432,7 @@ module.exports = class AutoModeration extends Observer { if (roles.some((r) => bypass.includes(r)) || ignore.includes(channel.id)) return; - const missing = channel.permissionsFor(guild.members.me).missing('ManageMessages'); + const missing = channel.permissionsFor(this.client.user).missing('ManageMessages'); if (missing.length) { this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'link', permissions: missing }); return; @@ -454,9 +454,10 @@ module.exports = class AutoModeration extends Observer { let log = `${guild.name} Link filter debug:`; for (const match of matches) { - const { domain } = match.match(this.regex.linkReg).groups; + let { domain } = match.match(this.regex.linkReg).groups; + domain = domain.toLowerCase(); // Invites are filtered separately - if (domain.toLowerCase() === 'discord.gg') continue; + if (domain === 'discord.gg') continue; log += `\nMatched link ${match}: `; const predicate = (dom) => { @@ -550,13 +551,13 @@ module.exports = class AutoModeration extends Observer { const member = message.member || await guild.members.fetch(author.id).catch(() => null); const settings = await wrapper.settings(); const { invitefilter: setting } = settings; - const { bypass, ignore, actions, silent, enabled, whitelist } = setting; + const { bypass, ignore, actions, silent, enabled, whitelist = [] } = setting; if (!enabled) return; const roles = member?.roles.cache.map((r) => r.id) || []; if (roles.some((r) => bypass.includes(r)) || ignore.includes(channel.id)) return; - const missing = channel.permissionsFor(guild.members.me).missing('ManageMessages'); + const missing = channel.permissionsFor(this.client.user).missing('ManageMessages'); if (missing.length) { this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'invite', permissions: missing }); return; @@ -613,7 +614,7 @@ module.exports = class AutoModeration extends Observer { if (!enabled || roles.some((r) => bypass.includes(r)) || ignore.includes(channel.id)) return; - const missing = channel.permissionsFor(guild.members.me).missing('ManageMessages'); + const missing = channel.permissionsFor(this.client.user).missing('ManageMessages'); if (missing.length) { this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'mention', permissions: missing }); return; diff --git a/src/structure/components/observers/CommandHandler.js b/src/structure/components/observers/CommandHandler.js index 82b666e..c29f3c1 100644 --- a/src/structure/components/observers/CommandHandler.js +++ b/src/structure/components/observers/CommandHandler.js @@ -2,6 +2,9 @@ const { EmbedBuilder, Message, ChannelType, ComponentType, ButtonStyle } = requi const { Util } = require('../../../utilities'); const { InvokerWrapper, MessageWrapper } = require('../../client/wrappers'); const { Observer, CommandError } = require('../../interfaces/'); +// const { inspect } = require('util'); + +const flagReg = /(?:^| )(?(?:--[a-z0-9]{3,})|(?:-[a-z]{1,2}))(?:$| )/iu; class CommandHandler extends Observer { @@ -29,8 +32,12 @@ class CommandHandler extends Observer { || message.author.bot || message.guild && !message.guild.available) return undefined; + const userWrapper = await this.client.getUserWrapper(message.author.id); - if (!userWrapper.developer) return; + if (message.guild) { + const settings = await message.guildWrapper.settings(); + if (!settings.textcommands.enabled && !userWrapper.developer) return; + } if(message.guild) { if(!message.member) await message.guild.members.fetch(message.author.id); @@ -55,7 +62,7 @@ class CommandHandler extends Observer { // There was an error if _parseResponse return value is truthy, i.e. an error message was sent if (await this._parseResponse(invoker, response)) return; - await this._executeCommand(invoker, command.slash ? response.options.args : response.options); + await this._executeCommand(invoker, response.options); } @@ -79,7 +86,7 @@ class CommandHandler extends Observer { if (inhibitors.length) return this._generateError(invoker, { type: 'inhibitor', ...inhibitors[0] }); await invoker.deferReply(); - const response = await this._parseInteraction(interaction, command); + const response = await this._parseInteraction(invoker, command); if (await this._parseResponse(invoker, response)) return; try { // Temp logging @@ -91,8 +98,31 @@ class CommandHandler extends Observer { } _parseResponse(invoker, response) { + + // Ensure option dependencies + outer: + if(response.options) for (const opt of Object.values(response.options)) { + let hasDep = false; + for (const dep of opt.dependsOn) { + // AND logic + if (!response.options[dep] && opt.dependsOnMode === 'AND') { + response = { option: opt, error: true, dependency: dep }; + break outer; + } + // OR logic + if (response.options[dep]) hasDep = true; + } + if (!hasDep && opt.dependsOnMode === 'OR') { + response = { option: opt, error: true, dependency: opt.dependsOn.join('** OR **') }; + break; + } + } + const { command } = invoker; - if (response.error) { + if (response.error && response.index) { + if(!response.emoji) response.emoji = 'failure'; + return invoker.reply(response); + } else if (response.error) { let content = invoker.format(`O_COMMANDHANDLER_TYPE${response.option.type}`, { option: response.option.name, min: response.option.minimum, max: response.option.maximum }); @@ -133,9 +163,7 @@ class CommandHandler extends Observer { } async _executeCommand(invoker, options) { - // TODO defer all replies -- need to go through all commands to ensure they're not deferrign them to avoid errors - // Why? Occasionally some interacitons don't complete in time due to taking longer than normal to reach the bot -- unsure if deferring all replies will fix it - + let response = null; const now = Date.now(); if (this.client.developmentMode && !this.client.developers.includes(invoker.user.id)) return invoker.reply({ @@ -147,7 +175,7 @@ class CommandHandler extends Observer { let debugstr = invoker.command.name; if (invoker.subcommandGroup) debugstr += ` ${invoker.subcommandGroup.name}`; if(invoker.subcommand) debugstr += ` ${invoker.subcommand.name}`; - this.logger.info(`${invoker.user.tag} (${invoker.user.id}) is executing ${debugstr} in ${invoker.guild?.name || 'dms'}`); + this.logger.info(`[${invoker.type.toUpperCase()}] ${invoker.user.tag} (${invoker.user.id}) is executing ${debugstr} in ${invoker.guild?.name || 'dms'}`); response = await invoker.command.execute(invoker, options); invoker.command.success(now); } catch (error) { @@ -174,9 +202,9 @@ class CommandHandler extends Observer { } - async _parseInteraction(interaction, command) { + async _parseInteraction(invoker, command) { - const { subcommand } = interaction; + const { subcommand, guild, target: interaction } = invoker; let error = null; const options = {}; @@ -193,14 +221,11 @@ class CommandHandler extends Observer { continue; } - // const newOption = new CommandOption({ - // name: matched.name, type: matched.type, - // minimum: matched.minimum, maximum: matched.maximum, - // _rawValue: option.value, - // dependsOn: matched.dependsOn, dependsOnMode: matched.dependsOnMode - // }); - const newOption = matched.clone(option.value); - const parsed = await this._parseOption(interaction, newOption); + if (matched.guildOnly && !guild) return { error: true, params: { option: matched.name }, index: 'O_COMMANDHANDLER_GUILDONLY_OPT' }; + const rawValue = matched.plural && typeof option.value === 'string' ? Util.parseQuotes(option.value).map(([x]) => x) : option.value; + const newOption = matched.clone(rawValue, guild, true); + const parsed = await newOption.parse(); + // const parsed = await this._parseOption(interaction, newOption); // console.log(parsed); if(parsed.error) { @@ -211,7 +236,7 @@ class CommandHandler extends Observer { break; } - newOption.value = parsed.value; + // newOption.value = parsed.value; options[matched.name] = newOption; } @@ -223,18 +248,6 @@ class CommandHandler extends Observer { if(!options[req.name]) return { option: req, error: true, required: true }; } - // Ensure option dependencies - for (const opt of Object.values(options)) { - let hasDep = false; - for (const dep of opt.dependsOn) { - // AND logic - if (!options[dep] && opt.dependsOnMode === 'AND') return { option: opt, error: true, dependency: dep }; - // OR logic - if (options[dep]) hasDep = true; - } - if(!hasDep && opt.dependsOnMode === 'OR') return { option: opt, error: true, dependency: opt.dependsOn.join('** OR **') }; - } - return { error: false, options @@ -244,13 +257,14 @@ class CommandHandler extends Observer { async _parseMessage(invoker, params) { - const { command, target: message } = invoker; + const { command, target: message, guild } = invoker; const { subcommands, subcommandGroups } = command; const args = {}; // console.log(options); let group = null, subcommand = null; + // Parse out subcommands if (subcommandGroups.length || subcommands.length) { const [first, second, ...rest] = params; group = command.subcommandGroup(first); @@ -258,11 +272,11 @@ class CommandHandler extends Observer { // Depending on how thoroughly I want to support old style commands this might have to try and resolve to other options // But for now I'm followin discord's structure for commands if (!group) { - subcommand = command.subcommand(first)?.raw; + subcommand = command.subcommand(first); params = []; if (second) params.push(second, ...rest); } else { - subcommand = command.subcommand(second)?.raw; + subcommand = command.subcommand(second); params = rest; } message.subcommand = subcommand; @@ -275,18 +289,166 @@ class CommandHandler extends Observer { const activeCommand = subcommand || command; const flags = activeCommand.options.filter((opt) => opt.flag); - for (const flag of flags) { + params = Util.parseQuotes(params.join(' ')).map(([x]) => x); + let currentFlag = null; + // console.log('params', params); + + // Parse flags + for (let index = 0; index < params.length;) { + + // console.log(params[index]); + const match = flagReg.exec(params[index]); + if (!match) { + // console.log('no match', currentFlag?.name); + if (currentFlag) { // Add potential value resolveables to the flag's raw value until next flag is hit, if there is one + if (currentFlag.plural) { // The parse function only parses consecutive values + if (!currentFlag._rawValue) currentFlag._rawValue = []; + currentFlag._rawValue.push(params[index]); + } else { + currentFlag._rawValue = params[index]; + currentFlag = null; + } + } + index++; + continue; + } + + // console.log('matched'); + const _flag = match.groups.flag.replace(/--?/u, '').toLowerCase(); + let aliased = false; + const flag = flags.find((f) => { + aliased = f.valueAsAlias && f.choices.some((c) => c.value === _flag); + return f.name === _flag || aliased; + }); + if (!flag) return { error: true, index: 'O_COMMANDHANDLER_UNRECOGNISED_FLAG', params: { flag: _flag } }; + else if (flag.guildOnly && !guild) return { error: true, params: { option: flag.name }, index: 'O_COMMANDHANDLER_GUILDONLY_OPT' }; + + // console.log('aliased', aliased); + params.splice(index, 1, null); + if (aliased) { + (args[flag.name] = flag.clone(_flag, guild))._aliased = true; + currentFlag = null; + } else { + currentFlag = flag.clone(null, guild); + args[flag.name] = currentFlag; + } + index++; + // console.log('------------------------------'); + + } + + // Clean up params for option parsing + for (const flag of Object.values(args)) { + // console.log('flags loop', flag.name, flag._rawValue); + const removed = await flag.parse(); + if (removed.error) { + if (flag.choices.length) { + return { error: true, index: 'O_COMMANDHANDLER_INVALID_CHOICE', params: { option: flag.name, value: flag._rawValue, choices: flag.choices.map((c) => c.value).join('`, `') } }; + } + return { option: flag, ...removed }; + } + for(const r of removed) params.splice(params.indexOf(r), 1); + } + + // console.log('params', params); + let options = activeCommand.options.filter((opt) => !opt.flag && (opt.type !== 'STRING' || opt.choices.length)); + if(!guild) options = options.filter((opt) => !opt.guildOnly); + // const choiceOpts = activeCommand.options.filter((opt) => opt.choices.length); + const stringOpts = activeCommand.options.filter((opt) => !opt.flag && !opt.choices.length && opt.type === 'STRING'); + // console.log('non-flag options', options.map((opt) => opt.name)); + // Parse out non-flag options + for (const option of options) { // String options are parsed separately at the end + // console.log(1, params); + if (!params.some((param) => param !== null)) break; + // console.log(2); + const cloned = option.clone(null, guild); + let removed = null; + if (cloned.plural) { // E.g. if the type is CHANNEL**S**, parse out any potential channels from the message + // console.log('plural'); + cloned._rawValue = params; + removed = await cloned.parse(); + } else for (let index = 0; index < params.length;) { // Attempt to parse out a value from each param + // console.log('singular'); + if (params[index] === null) { + index++; + continue; + } + cloned._rawValue = params[index]; + removed = await cloned.parse(); + if (!removed.error) break; + index++; + } + if (removed.error) continue; + + args[cloned.name] = cloned; + // Clean up params for string parsing + for (const r of removed) params.splice(params.indexOf(r), 1, null); } - return { options: { args, parameters: params }, verbose: true }; + const strings = []; + let tmpString = ''; + // console.log('strings loop'); + // console.log(params); + // Compile strings into groups of strings so we don't get odd looking strings from which options have been parsed out of + for (let index = 0; index < params.length;) { + const str = params[index]; + // console.log(str); + if (!str) { + // console.log('null string'); + if (tmpString.length) { + // console.log('pushing', tmpString); + strings.push(tmpString); + tmpString = ''; + } + index++; + continue; + } + params.splice(index, 1); + tmpString += ` ${str}`; + tmpString = tmpString.trim(); + } + // console.log('tmpString', tmpString); + if(tmpString.length) strings.push(tmpString); + + // console.log('params after', params); + // console.log('strings', strings); + + if (strings.length) for (const strOpt of stringOpts) { + const cloned = strOpt.clone(strings.shift()); + // console.log(cloned.name, cloned._rawValue); + await cloned.parse(); + args[cloned.name] = cloned; + } + + // This part is obsolete now, I think, the string option checks the choice value + for (const arg of Object.values(args)) { + // console.log(arg.name, arg.value); + if (!arg.choices.length) continue; + if (!arg.choices.some((choice) => { + if (typeof arg.value === 'string') return arg.value.toLowerCase() === choice.value.toLowerCase(); + return arg.value === choice.value; + })) return { error: true, index: 'O_COMMANDHANDLER_INVALID_CHOICE', params: { option: arg.name, value: arg.value, choices: arg.choices.map((c) => c.value).join('`, `') } }; + } + + for (const req of activeCommand.options.filter((opt) => opt.required)) + if (!args[req.name]) return { option: req, error: true, required: true }; + + // console.log('parsed args final', Object.values(args).map((arg) => `\n${arg.name}: ${arg.value}, ${inspect(arg._rawValue)}`).join('')); + if (strings.length) return { error: true, index: 'O_COMMANDHANDLER_UNRECOGNISED_OPTIONS', params: { opts: strings.join('`, `') } }; + + return { options: args, verbose: true }; } + // Should be unnecessary -- moved to commandoption async _parseOption(interaction, option) { const { guild } = interaction; const types = { + POINTS: (value) => { + return { value }; + }, ROLES: async (string) => { const args = Util.parseQuotes(string).map(([str]) => str); const roles = await guild.resolveRoles(args); @@ -372,6 +534,7 @@ class CommandHandler extends Observer { return { error: false, value: parseInt(integer) }; }, BOOLEAN: (boolean) => { + boolean = this.client.resolver.resolveBoolean(boolean); return { error: false, value: boolean }; }, MEMBER: async (user) => { @@ -430,7 +593,7 @@ class CommandHandler extends Observer { async _getCommand(message) { if (!this._mentionPattern) this._mentionPattern = new RegExp(`^(<@!?${this.client.user.id}>)`, 'iu'); - const [arg1, arg2, ...args] = message.content.split(' '); + const [arg1, arg2, ...args] = message.content.split(' ').filter((str) => str.length); if (message.guild) await message.guild.settings(); const userWrapper = await this.client.getUserWrapper(message.author.id); diff --git a/src/structure/components/observers/GuildLogging.js b/src/structure/components/observers/GuildLogging.js index 77767cd..d76836f 100644 --- a/src/structure/components/observers/GuildLogging.js +++ b/src/structure/components/observers/GuildLogging.js @@ -1,5 +1,5 @@ /* eslint-disable no-labels */ -const { WebhookClient, AttachmentBuilder } = require('discord.js'); +const { WebhookClient, AttachmentBuilder, AuditLogEvent } = require('discord.js'); const { stripIndents } = require('common-tags'); const moment = require('moment'); const { inspect } = require('util'); @@ -103,22 +103,23 @@ class GuildLogger extends Observer { const hook = await guild.getWebhook('messages'); if (!hook) { - this.logger.debug(`Missing messageLog hook in ${guild.name} (${guild.id})`); + // this.logger.debug(`Missing messageLog hook in ${guild.name} (${guild.id})`); return this.client.emit('logError', { guild, logger: 'threadLogger', reason: 'MSGLOG_NO_HOOK' }); } let actor = null; - const auditLogPerm = guild.members.me.permissions.has('ViewAuditLog'); + const me = await this.client.resolver.resolveMember(this.client.user, null, guild); + const auditLogPerm = me.permissions.has('ViewAuditLog'); if (type === 'CREATE') actor = owner; else if (type === 'DELETE' && auditLogPerm) { - const auditLogs = await guild.fetchAuditLogs({ type: 'THREAD_DELETE', limit: 1 }); + const auditLogs = await guild.fetchAuditLogs({ type: AuditLogEvent.ThreadDelete, limit: 1 }); const log = auditLogs.entries.first(); if (log) { if (thread.id !== log.target.id) return; actor = log.executor; } } else if (['ARCHIVE', 'UNARCHIVE'].includes(type) && auditLogPerm) { - const auditLogs = await guild.fetchAuditLogs({ type: 'THREAD_UPDATE', limit: 1 }); + const auditLogs = await guild.fetchAuditLogs({ type: AuditLogEvent.ThreadUpdate, limit: 1 }); const log = auditLogs.entries.first(); if (log) { if (thread.id !== log.target.id) return; @@ -178,7 +179,7 @@ class GuildLogger extends Observer { const hook = await wrapper.getWebhook('messages'); if (!hook) { - this.logger.debug(`Missing messageLog hook in ${wrapper.name} (${wrapper.id})`); + // this.logger.debug(`Missing messageLog hook in ${wrapper.name} (${wrapper.id})`); return this.client.emit('logError', { guild: wrapper, logger: 'messageLogger', reason: 'MSGLOG_NO_HOOK' }); } @@ -340,13 +341,13 @@ class GuildLogger extends Observer { const { ignore, bypass } = chatlogs; if (ignore.includes(channel.id)) return; - const missing = logChannel.permissionsFor(guild.members.me).missing(['ViewChannel', 'EmbedLinks', 'SendMessages', 'ManageWebhooks']); + const missing = logChannel.permissionsFor(this.client.user).missing(['ViewChannel', 'EmbedLinks', 'SendMessages', 'ManageWebhooks']); if (missing.length) return this.client.emit('logError', { guild: wrapper, logger: 'messageLogger', reason: 'MSGLOG_NO_PERMS', params: { missing: missing.join(', ') } }); const hook = await wrapper.getWebhook('messages'); if (!hook) { - this.logger.debug(`Missing messageLog hook in ${guild.name} (${guild.id})`); + // this.logger.debug(`Missing messageLog hook in ${guild.name} (${guild.id})`); return this.client.emit('logError', { guild: wrapper, logger: 'messageLogger', reason: 'MSGLOG_NO_HOOK' }); } @@ -364,6 +365,7 @@ class GuildLogger extends Observer { content = Util.escapeMarkdown(content); if (author.bot) continue; + // TODO: Apparently the cache for this doesn't work properly and the bot hits rate limits occasionally, make own cache at some point if (!member || member.partial) member = await guild.members.fetch(message.author.id).catch(() => { return false; }); @@ -533,7 +535,7 @@ class GuildLogger extends Observer { const hook = await wrapper.getWebhook('messages'); if (!hook) { - this.logger.debug(`Missing messageLog hook in ${guild.name} (${guild.id})`); + // this.logger.debug(`Missing messageLog hook in ${guild.name} (${guild.id})`); return this.client.emit('logError', { guild: wrapper, logger: 'messageLogger', reason: 'MSGLOG_NO_HOOK' }); } @@ -710,7 +712,7 @@ class GuildLogger extends Observer { async memberLeave(member) { - const { guild, guildWrapper: wrapper } = member; + const { guildWrapper: wrapper } = member; const settings = await wrapper.settings(); const setting = settings.members; if (!setting.channel || !setting.enabled) return; @@ -732,7 +734,7 @@ class GuildLogger extends Observer { if (oldMember.nickname === newMember.nickname) return; - const { guild, user, guildWrapper: wrapper } = oldMember; + const { user, guildWrapper: wrapper } = oldMember; const settings = await wrapper.settings(); const setting = settings.nicknames; if (!setting.channel || !setting.enabled) return; diff --git a/src/structure/components/observers/Metrics.js b/src/structure/components/observers/Metrics.js new file mode 100644 index 0000000..dd4e321 --- /dev/null +++ b/src/structure/components/observers/Metrics.js @@ -0,0 +1,31 @@ +const { Observer } = require("../../interfaces"); + +class Metrics extends Observer { + + constructor(client) { + super(client, { + name: 'metrics', + priority: 10, + disabled: false + }); + + this.hooks = [ + ['apiRequest', this.request.bind(this)], + ['apiResponse', this.response.bind(this)] + ]; + + this.cache = {}; + + } + + async request(request) { + // console.log(request); + } + + async response(request, response) { + // console.log(response, await response.json(), response.status); + } + +} + +module.exports = Metrics; \ No newline at end of file diff --git a/src/structure/components/observers/UtilityHook.js b/src/structure/components/observers/UtilityHook.js index 78dc52c..d562098 100644 --- a/src/structure/components/observers/UtilityHook.js +++ b/src/structure/components/observers/UtilityHook.js @@ -51,7 +51,8 @@ class UtilityHook extends Observer { const { guildWrapper: guild } = member; const settings = await guild.settings(); const setting = settings.mute; - if (!guild.members.me.permissions.has('ManageRoles')) return; + const me = await guild.resolveMember(this.client.user); + if (!me.permissions.has('ManageRoles')) return; const infraction = await this.client.storageManager.mongodb.infractions.findOne({ duration: { $gt: 0 }, @@ -79,7 +80,7 @@ class UtilityHook extends Observer { if (managed) { await member.roles.add(setting.role, 'automute upon rejoin, type 1'); await member.roles.remove(remove, 'removing excess roles for type 1 mute'); - } else await member.roles.set(setting.role, 'automute upon join, type 1'); + } else await member.roles.set([role], 'automute upon join, type 1'); } else if (infraction.data.muteType === 2) { @@ -100,7 +101,8 @@ class UtilityHook extends Observer { const settings = await guild.settings(); const setting = settings.stickyrole; if (!setting.roles.length || guild.premium < 1) return; - if (!guild.members.me.permissions.has('ManageRoles')) return; + const me = await guild.resolveMember(this.client.user); + if (!me.permissions.has('ManageRoles')) return; const data = await this.client.storageManager.mongodb.role_cache.findOne({ guild: guild.id, member: member.id }); if (!data) return; @@ -115,7 +117,8 @@ class UtilityHook extends Observer { const settings = await guild.settings(); const setting = settings.autorole; if (!setting.enabled) return; - if (!guild.members.me.permissions.has('ManageRoles')) return; + const me = await guild.resolveMember(this.client.user); + if (!me.permissions.has('ManageRoles')) return; const _roles = await guild.resolveRoles(setting.roles); const roles = _roles.map((r) => r.id); @@ -153,7 +156,8 @@ class UtilityHook extends Observer { const { guild } = invite; if (!guild) return; - if (!guild.members.me.permissions.has('ManageGuild')) return; + const me = await this.client.resolver.resolveMember(this.client.user, null, guild); + if (!me.permissions.has('ManageGuild')) return; if (!guild.invites) guild.invites = await guild.fetchInvites(); guild.invites.set(invite.code, invite); @@ -163,7 +167,8 @@ class UtilityHook extends Observer { const { guild } = invite; if (!guild) return; - if (!guild.members.me.permissions.has('ManageGuild')) return; + const me = await this.client.resolver.resolveMember(this.client.user, null, guild); + if (!me.permissions.has('ManageGuild')) return; if (!guild.invites) guild.invites = await guild.fetchInvites(); guild.invites.delete(invite.code); @@ -203,7 +208,8 @@ class UtilityHook extends Observer { !selfrole.channel || selfrole.channel !== channel.id || !selfrole.roles.length) return; - const missing = guild.members.me.permissions.missing(['ManageRoles']); + const me = await guild.resolveMember(this.client.user); + const missing = me.permissions.missing(['ManageRoles']); if (missing.length) return this.client.emit('utilityError', { guild, utility: 'selfrole', reason: 'UTILITY_SELFROLE_PERMS', params: { missing: missing.join(', ') } }); diff --git a/src/structure/components/settings/administration/IgnoreChannels.js b/src/structure/components/settings/administration/IgnoreChannels.js index dbff60b..1cf7d58 100644 --- a/src/structure/components/settings/administration/IgnoreChannels.js +++ b/src/structure/components/settings/administration/IgnoreChannels.js @@ -1,5 +1,5 @@ const { Util } = require("../../../../utilities"); -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); class IgnoreSetting extends Setting { @@ -19,7 +19,7 @@ class IgnoreSetting extends Setting { bypass: { ARRAY: 'GUILD_ROLE' } }, commandOptions: [ - new CommandOption({ + { name: 'list', description: 'List to act on', type: 'STRING', @@ -27,9 +27,9 @@ class IgnoreSetting extends Setting { { name: 'channels', value: 'channels' }, { name: 'bypass', value: 'bypass' } ], - dependsOn: ['method'] - }), - new CommandOption({ + dependsOn: ['method']//, valueAsAlias: true, flag: true + }, + { name: 'method', description: 'Method of modifying', type: 'STRING', @@ -39,8 +39,8 @@ class IgnoreSetting extends Setting { { name: 'set', value: 'set' }, { name: 'reset', value: 'reset' }, ], - dependsOn: ['list'] - }) + dependsOn: ['list']//, valueAsAlias: true, flag: true + } ] }); diff --git a/src/structure/components/settings/administration/PermissionType.js b/src/structure/components/settings/administration/PermissionType.js index 2ace0d2..f8ae481 100644 --- a/src/structure/components/settings/administration/PermissionType.js +++ b/src/structure/components/settings/administration/PermissionType.js @@ -1,4 +1,4 @@ -const { Setting, CommandOption } = require('../../../interfaces/'); +const { Setting } = require('../../../interfaces/'); class PermissionType extends Setting { @@ -16,15 +16,16 @@ class PermissionType extends Setting { type: 'STRING' }, commandOptions: [ - new CommandOption({ + { type: 'STRING', name: 'type', + description: 'Where to read permissions from', choices: [ { name: 'discord', value: 'discord' }, { name: 'both', value: 'both' }, { name: 'grant', value: 'grant' } ] - }) + } ] }); diff --git a/src/structure/components/settings/administration/Protection.js b/src/structure/components/settings/administration/Protection.js index 4397b89..402e951 100644 --- a/src/structure/components/settings/administration/Protection.js +++ b/src/structure/components/settings/administration/Protection.js @@ -1,5 +1,5 @@ const { Util } = require("../../../../utilities"); -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); class ProtectionSetting extends Setting { @@ -21,7 +21,7 @@ class ProtectionSetting extends Setting { enabled: 'BOOLEAN' }, commandOptions: [ - new CommandOption({ + { type: 'STRING', name: 'type', description: 'Select protection type', @@ -30,8 +30,8 @@ class ProtectionSetting extends Setting { { name: 'position', value: 'position' } ], dependsOn: [] - }), - new CommandOption({ + }, + { type: 'STRING', name: 'roles', description: 'Method of modifying', @@ -42,12 +42,12 @@ class ProtectionSetting extends Setting { { name: 'reset', value: 'reset' }, ], dependsOn: [] - }), - new CommandOption({ + }, + { type: 'BOOLEAN', name: 'enabled', description: 'Whether setting is active or not' - }) + } ] }); diff --git a/src/structure/components/settings/administration/Silent.js b/src/structure/components/settings/administration/Silent.js index 889bdb3..869e4fa 100644 --- a/src/structure/components/settings/administration/Silent.js +++ b/src/structure/components/settings/administration/Silent.js @@ -1,4 +1,4 @@ -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); class SilentSetting extends Setting { @@ -17,11 +17,11 @@ class SilentSetting extends Setting { enabled: 'BOOLEAN' }, commandOptions: [ - new CommandOption({ - type: 'BOOLEAN', + { + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, name: 'enabled', description: 'Toggle state' - }) + } ] }); diff --git a/src/structure/components/settings/administration/TextCommands.js b/src/structure/components/settings/administration/TextCommands.js new file mode 100644 index 0000000..6ae5d5a --- /dev/null +++ b/src/structure/components/settings/administration/TextCommands.js @@ -0,0 +1,55 @@ +const { Setting } = require("../../../interfaces"); + +class TextCommands extends Setting { + + constructor(client) { + super(client, { + name: 'textcommands', + display: 'Text Commands', + description: 'Message based commands configuration', + module: 'administration', + default: { + enabled: false, + prefix: '-' + }, + commandOptions: [ + { + name: 'enabled', + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, + description: 'Toggle enable state' + }, + { + name: 'prefix', + type: 'STRING', + description: 'Prefix to use' + } + ] + }); + } + + async execute(invoker, { enabled, prefix }, setting) { + + if (enabled) setting.enabled = enabled.value; + if (prefix) setting.prefix = prefix.value; + return { index: 'SETTING_SUCCESS_ALT' }; + + } + + fields(guild) { + const setting = guild._settings[this.name]; + return [{ + name: 'GENERAL_STATUS', + value: guild.format('GENERAL_STATE', { + bool: setting.enabled + }, { code: true }), + inline: true + }, { + name: 'GENERAL_PREFIX', + value: setting.prefix, + inline: true + }]; + } + +} + +module.exports = TextCommands; \ No newline at end of file diff --git a/src/structure/components/settings/logging/DmInfraction.js b/src/structure/components/settings/logging/DmInfraction.js index 114e96b..23f1970 100644 --- a/src/structure/components/settings/logging/DmInfraction.js +++ b/src/structure/components/settings/logging/DmInfraction.js @@ -1,4 +1,4 @@ -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); const Infractions = [ 'NOTE', 'WARN', @@ -57,13 +57,13 @@ class DmInfraction extends Setting { } }, commandOptions: [ - new CommandOption({ + { name: 'message', description: 'Set the message for an infraction type', type: 'STRING', dependsOn: ['infraction'] - }), - new CommandOption({ + }, + { name: 'infraction', description: 'Choose the infraction for which to modify the message', type: 'STRING', @@ -71,8 +71,8 @@ class DmInfraction extends Setting { return { name: inf, value: inf }; }), dependsOn: ['message'] - }), - new CommandOption({ + }, + { name: 'infractions', description: 'Modify the list of infractions that are sent', type: 'STRING', @@ -82,15 +82,15 @@ class DmInfraction extends Setting { { name: 'set', value: 'set' }, { name: 'reset', value: 'reset' }, ] - }), - new CommandOption({ + }, + { name: 'enabled', description: 'Enable or disable the sending of infractions in DMs', - type: 'BOOLEAN' - }), + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + }, { name: 'anonymous', - type: 'BOOLEAN', + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, description: 'Whether who issued the infraction is shown in moderation logs' } ] diff --git a/src/structure/components/settings/logging/Errors.js b/src/structure/components/settings/logging/Errors.js index 9952286..9d70d04 100644 --- a/src/structure/components/settings/logging/Errors.js +++ b/src/structure/components/settings/logging/Errors.js @@ -1,4 +1,4 @@ -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); class MessageLog extends Setting { @@ -22,16 +22,16 @@ class MessageLog extends Setting { types: { ARRAY: 'ERROR_TYPES' } // TODO: Error types }, commandOptions: [ - new CommandOption({ + { name: 'channel', description: 'Channel in which to output logs', type: 'TEXT_CHANNEL' - }), - new CommandOption({ + }, + { name: 'enabled', description: 'Toggle logging on or off', - type: 'BOOLEAN' - }) + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + } ] }); @@ -39,14 +39,12 @@ class MessageLog extends Setting { async execute(interaction, opts, setting) { - const { guild } = interaction; - if (opts.enabled?.value === false) setting.channel = null; if (opts.channel) { const channel = opts.channel.value; - const perms = channel.permissionsFor(guild.members.me); + const perms = channel.permissionsFor(this.client.user); const missingPerms = perms.missing(['ViewChannel', 'EmbedLinks', 'SendMessages']); if (missingPerms.length) return { error: true, diff --git a/src/structure/components/settings/logging/Members.js b/src/structure/components/settings/logging/Members.js index 99b73b9..ea05539 100644 --- a/src/structure/components/settings/logging/Members.js +++ b/src/structure/components/settings/logging/Members.js @@ -1,4 +1,4 @@ -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); class MemberLog extends Setting { @@ -21,26 +21,26 @@ class MemberLog extends Setting { leave: 'STRING' }, commandOptions: [ - new CommandOption({ + { name: 'enabled', description: 'Enable/disable member logs', - type: 'BOOLEAN' - }), - new CommandOption({ + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + }, + { name: 'channel', description: 'Select the log output channel', type: 'TEXT_CHANNEL' - }), - new CommandOption({ + }, + { name: 'join', description: 'Set the join message', - type: 'STRING' - }), - new CommandOption({ + type: 'STRING', flag: true + }, + { name: 'leave', description: 'Set the leave message', - type: 'STRING' - }) + type: 'STRING', flag: true + } ] }); diff --git a/src/structure/components/settings/logging/Messages.js b/src/structure/components/settings/logging/Messages.js index 12125c6..dbbb964 100644 --- a/src/structure/components/settings/logging/Messages.js +++ b/src/structure/components/settings/logging/Messages.js @@ -1,4 +1,4 @@ -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); const { Util } = require('../../../../utilities'); class MessageLog extends Setting { @@ -26,22 +26,22 @@ class MessageLog extends Setting { attachments: 'BOOLEAN' }, commandOptions: [ - new CommandOption({ + { name: 'channel', description: 'Channel in which to output logs', type: 'TEXT_CHANNEL' - }), - new CommandOption({ + }, + { name: 'enabled', description: 'Toggle logging on or off', - type: 'BOOLEAN' - }), - new CommandOption({ + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + }, + { name: 'attachments', description: 'Whether to log attachments. PREMIUM TIER 1', - type: 'BOOLEAN' - }), - new CommandOption({ + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + }, + { name: 'list', description: 'Select which list to modify', type: 'STRING', @@ -50,8 +50,8 @@ class MessageLog extends Setting { { name: 'ignore', value: 'ignore' }, ], dependsOn: ['method'] - }), - new CommandOption({ + }, + { name: 'method', description: 'Select which modification method to use', type: 'STRING', @@ -62,7 +62,7 @@ class MessageLog extends Setting { { name: 'reset', value: 'reset' }, ], dependsOn: ['list'] - }), + }, ] }); @@ -82,7 +82,7 @@ class MessageLog extends Setting { if (opts.channel) { const channel = opts.channel.value; - const perms = channel.permissionsFor(guild.members.me); + const perms = channel.permissionsFor(this.client.user); const missingPerms = perms.missing(['ViewChannel', 'EmbedLinks', 'SendMessages', 'ManageWebhooks']); if (missingPerms.length) return { error: true, diff --git a/src/structure/components/settings/logging/Moderation.js b/src/structure/components/settings/logging/Moderation.js index 87731d6..28ab163 100644 --- a/src/structure/components/settings/logging/Moderation.js +++ b/src/structure/components/settings/logging/Moderation.js @@ -1,5 +1,5 @@ const { Infractions } = require("../../../../constants/Constants"); -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); // [ // 'NOTE', @@ -44,17 +44,17 @@ class ModerationLog extends Setting { } }, commandOptions: [ - new CommandOption({ + { name: 'enabled', description: 'Enable/disable member logs', - type: 'BOOLEAN' - }), - new CommandOption({ + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + }, + { name: 'channel', description: 'Logging channel', type: 'TEXT_CHANNEL' - }), - new CommandOption({ + }, + { name: 'infractions', description: 'Modify the list of infractions that are sent', type: 'STRING', @@ -64,10 +64,10 @@ class ModerationLog extends Setting { { name: 'set', value: 'set' }, { name: 'reset', value: 'reset' }, ] - }), + }, { name: 'anonymous', - type: 'BOOLEAN', + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, description: 'Whether who issued the infraction is shown in moderation logs' } ] diff --git a/src/structure/components/settings/logging/Nicknames.js b/src/structure/components/settings/logging/Nicknames.js index 6ff906f..779a2af 100644 --- a/src/structure/components/settings/logging/Nicknames.js +++ b/src/structure/components/settings/logging/Nicknames.js @@ -1,4 +1,4 @@ -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); class Nicknames extends Setting { @@ -16,16 +16,16 @@ class Nicknames extends Setting { channel: 'GUILD_TEXT' }, commandOptions: [ - new CommandOption({ + { name: 'enabled', description: 'Toggle logging on or off', - type: 'BOOLEAN' - }), - new CommandOption({ + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + }, + { name: 'channel', type: 'TEXT_CHANNEL', description: 'Set the channel for nickname logging' - }) + } ] }); } diff --git a/src/structure/components/settings/logging/Voice.js b/src/structure/components/settings/logging/Voice.js index ebbf632..e665cb6 100644 --- a/src/structure/components/settings/logging/Voice.js +++ b/src/structure/components/settings/logging/Voice.js @@ -1,4 +1,4 @@ -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); class Voice extends Setting { @@ -15,16 +15,16 @@ class Voice extends Setting { channel: 'GUILD_TEXT' }, commandOptions: [ - new CommandOption({ + { name: 'enabled', description: 'Toggle logging on or off', - type: 'BOOLEAN' - }), - new CommandOption({ + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + }, + { name: 'channel', type: 'TEXT_CHANNEL', description: 'Set the channel for voice join/leave logging' - }) + } ] }); } diff --git a/src/structure/components/settings/moderation/AutoModeration.js b/src/structure/components/settings/moderation/AutoModeration.js index dc93074..32a2242 100644 --- a/src/structure/components/settings/moderation/AutoModeration.js +++ b/src/structure/components/settings/moderation/AutoModeration.js @@ -1,5 +1,5 @@ const { Util } = require("../../../../utilities"); -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); const Infractions = [ 'WARN', 'MUTE', @@ -17,7 +17,7 @@ class Automod extends Setting { super(client, { name: 'automod', description: 'Define automatic infraction escalation', - display: 'Automatic Moderation', + display: 'Automod', module: 'moderation', default: { enabled: false, @@ -36,17 +36,17 @@ class Automod extends Setting { } }, commandOptions: [ - new CommandOption({ + { name: 'enabled', description: 'Toggle state', - type: 'BOOLEAN' - }), - new CommandOption({ + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + }, + { name: 'useprevious', description: 'Use the previously passed threshold if the point total lands between two thresholds', - type: 'BOOLEAN' - }), - new CommandOption({ + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + }, + { name: 'threshold', description: 'The threshold at which to issue an infraction', type: 'INTEGER', @@ -54,8 +54,8 @@ class Automod extends Setting { maximum: 100, dependsOn: ['infraction', 'length'], dependsOnMode: 'OR' - }), - new CommandOption({ + }, + { name: 'infraction', description: 'The type of infraction to issue', type: 'STRING', @@ -63,13 +63,13 @@ class Automod extends Setting { return { name: inf, value: inf }; }), dependsOn: ['threshold'] - }), - new CommandOption({ + }, + { name: 'length', description: 'The duration for a tempban or a mute', type: 'TIME', dependsOn: ['threshold'] - }) + } ] }); } diff --git a/src/structure/components/settings/moderation/Grantable.js b/src/structure/components/settings/moderation/Grantable.js index 20c31f6..be636cc 100644 --- a/src/structure/components/settings/moderation/Grantable.js +++ b/src/structure/components/settings/moderation/Grantable.js @@ -1,4 +1,4 @@ -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); const { Util } = require("../../../../utilities"); class Grantable extends Setting { @@ -18,12 +18,12 @@ class Grantable extends Setting { enabled: 'BOOLEAN' }, commandOptions: [ - new CommandOption({ - type: 'BOOLEAN', + { + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, name: 'enabled', description: 'Toggle state' - }), - new CommandOption({ + }, + { name: 'roles', description: '', type: 'STRING', @@ -33,7 +33,7 @@ class Grantable extends Setting { { name: 'set', value: 'set' }, { name: 'reset', value: 'reset' }, ] - }) + } ] }); } diff --git a/src/structure/components/settings/moderation/InviteFilter.js b/src/structure/components/settings/moderation/InviteFilter.js index ac31e9a..c5a08bb 100644 --- a/src/structure/components/settings/moderation/InviteFilter.js +++ b/src/structure/components/settings/moderation/InviteFilter.js @@ -1,4 +1,4 @@ -const { FilterSetting, CommandOption } = require('../../../interfaces/'); +const { FilterSetting } = require('../../../interfaces/'); const { Util } = require("../../../../utilities"); class InviteFilterSetting extends FilterSetting { @@ -36,7 +36,7 @@ class InviteFilterSetting extends FilterSetting { actions: { ARRAY: 'ACTION' } }, commandOptions: [ - new CommandOption({ + { type: 'STRING', name: 'method', description: 'Select which modification method to use', @@ -49,8 +49,8 @@ class InviteFilterSetting extends FilterSetting { { name: 'list', value: 'list' } ], dependsOn: ['list'] - }), - new CommandOption({ + }, + { type: 'STRING', name: 'list', description: 'Select which list to modify', @@ -61,17 +61,17 @@ class InviteFilterSetting extends FilterSetting { { name: 'actions', value: 'actions' }, ], dependsOn: ['method'] - }), - new CommandOption({ - type: 'BOOLEAN', + }, + { + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, name: 'enabled', description: 'Toggle enable state' - }), - new CommandOption({ - type: 'BOOLEAN', + }, + { + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, name: 'silent', description: 'Toggle silent operation' - }) + } ] }); diff --git a/src/structure/components/settings/moderation/LinkFilter.js b/src/structure/components/settings/moderation/LinkFilter.js index 9a323c1..e7f5c8b 100644 --- a/src/structure/components/settings/moderation/LinkFilter.js +++ b/src/structure/components/settings/moderation/LinkFilter.js @@ -1,4 +1,4 @@ -const { FilterSetting, CommandOption } = require('../../../interfaces/'); +const { FilterSetting } = require('../../../interfaces/'); const { Util } = require("../../../../utilities"); const { FilterPresets } = require('../../../../constants'); @@ -43,7 +43,7 @@ class LinkFilterSetting extends FilterSetting { actions: { ARRAY: 'ACTION' } }, commandOptions: [ - new CommandOption({ + { type: 'STRING', name: 'method', description: 'Select which modification method to use', @@ -56,8 +56,8 @@ class LinkFilterSetting extends FilterSetting { { name: 'list', value: 'list' } ], dependsOn: ['list'] - }), - new CommandOption({ + }, + { type: 'STRING', name: 'list', description: 'Select which list to modify', @@ -71,22 +71,22 @@ class LinkFilterSetting extends FilterSetting { { name: 'presets', value: 'presets' } ], dependsOn: ['method'] - }), - new CommandOption({ - type: 'BOOLEAN', + }, + { + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, name: 'enabled', description: 'Toggle enable state' - }), - new CommandOption({ - type: 'BOOLEAN', + }, + { + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, name: 'whitelist', description: 'Toggle whitelist mode' - }), - new CommandOption({ - type: 'BOOLEAN', + }, + { + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, name: 'silent', description: 'Toggle silent operation' - }) + } ] }); diff --git a/src/structure/components/settings/moderation/MentionFilter.js b/src/structure/components/settings/moderation/MentionFilter.js index 452407b..0d32037 100644 --- a/src/structure/components/settings/moderation/MentionFilter.js +++ b/src/structure/components/settings/moderation/MentionFilter.js @@ -1,4 +1,4 @@ -const { FilterSetting, CommandOption } = require('../../../interfaces/'); +const { FilterSetting } = require('../../../interfaces/'); const { Util } = require("../../../../utilities"); class MentionFilter extends FilterSetting { @@ -28,27 +28,27 @@ class MentionFilter extends FilterSetting { ignore: { ARRAY: 'GUILD_TEXT' } }, commandOptions: [ - new CommandOption({ + { name: 'enabled', description: 'Toggle state', - type: 'BOOLEAN' - }), - new CommandOption({ + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + }, + { name: 'silent', description: 'Whether the bot will respond in chat', - type: 'BOOLEAN' - }), - new CommandOption({ + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + }, + { name: 'unique', description: 'Mentions for the same user count as one', - type: 'BOOLEAN' - }), - new CommandOption({ + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + }, + { name: 'limit', description: 'How many mentions are allowed in a message', type: 'INTEGER' - }), - new CommandOption({ + }, + { type: 'STRING', name: 'method', description: 'Select which modification method to use', @@ -61,8 +61,8 @@ class MentionFilter extends FilterSetting { { name: 'list', value: 'list' } ], dependsOn: ['list'] - }), - new CommandOption({ + }, + { type: 'STRING', name: 'list', description: 'Select which list to modify', @@ -72,7 +72,7 @@ class MentionFilter extends FilterSetting { { name: 'actions', value: 'actions' }, ], dependsOn: ['method'] - }), + }, ] }); } diff --git a/src/structure/components/settings/moderation/ModerationPoints.js b/src/structure/components/settings/moderation/ModerationPoints.js index ac58082..191ac90 100644 --- a/src/structure/components/settings/moderation/ModerationPoints.js +++ b/src/structure/components/settings/moderation/ModerationPoints.js @@ -1,4 +1,4 @@ -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); const { Util } = require("../../../../utilities"); const INFRACTIONS = ['WARN', 'MUTE', 'KICK', 'SOFTBAN', 'BAN', 'VCMUTE', 'VCKICK', 'VCBAN']; @@ -37,22 +37,22 @@ class ModerationPoints extends Setting { multiplier: false }, commandOptions: [ - new CommandOption({ + { name: 'points', description: 'Point value', type: 'INTEGER', - dependsOn: ['associate', 'type'], + dependsOn: ['associate', 'infraction'], dependsOnMode: 'OR', minimum: 0, maximum: 100 - }), - new CommandOption({ + }, + { name: 'expire', description: 'How long the points are counted for', type: 'TIME', - dependsOn: ['type'] - }), - new CommandOption({ + dependsOn: ['infraction'] + }, + { name: 'infraction', description: 'Type of infraction', type: 'STRING', @@ -61,36 +61,36 @@ class ModerationPoints extends Setting { }), dependsOn: ['points', 'expire'], dependsOnMode: 'OR' - }), - new CommandOption({ + }, + { name: 'enabled', description: 'Toggle on or off', - type: 'BOOLEAN' - }), - new CommandOption({ + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + }, + { name: 'multiplier', description: 'Use points as a multiplier for the expiration', - type: 'BOOLEAN' - }), - new CommandOption({ + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true + }, + { name: 'associate', description: 'Associate a word within a reason to a point value', type: 'STRING', - dependsOn: ['points'] - }), + dependsOn: ['points'], flag: true + }, ] }); } async execute(interaction, opts, setting) { - const { points, type, enabled, associate, expire, multiplier } = opts; + const { points, infraction, enabled, associate, expire, multiplier } = opts; if (multiplier) setting.multiplier = multiplier.value; if (enabled) setting.enabled = enabled.value; - if (expire) setting.expirations[type.value] = expire.value * 1000; + if (expire) setting.expirations[infraction.value] = expire.value * 1000; if (associate) setting.associations[associate.value.toLowerCase()] = points.value; - if (type && points) setting.points[type.value] = points.value; + if (infraction && points) setting.points[infraction.value] = points.value; return { index: 'SETTING_SUCCESS_ALT' }; diff --git a/src/structure/components/settings/moderation/Mute.js b/src/structure/components/settings/moderation/Mute.js index 4afe440..b8afbb0 100644 --- a/src/structure/components/settings/moderation/Mute.js +++ b/src/structure/components/settings/moderation/Mute.js @@ -1,4 +1,4 @@ -const { Setting, CommandOption } = require('../../../interfaces/'); +const { Setting } = require('../../../interfaces/'); const { inspect } = require('util'); const { Util } = require("../../../../utilities"); @@ -52,28 +52,28 @@ class MuteSetting extends Setting { permanent: 'BOOLEAN' }, commandOptions: [ - new CommandOption({ - type: 'STRING', + { + type: 'STRING', flag: true, name: 'create', description: 'Create a mute role, mutually exclusive with role' - }), - new CommandOption({ + }, + { type: 'ROLE', name: 'role', description: 'Select the role to use for mutes, mutually exclusive with create' - }), - new CommandOption({ - type: 'TIME', + }, + { + type: 'TIME', flag: true, name: 'default', description: 'Set the default duration for mutes' - }), - new CommandOption({ - type: 'BOOLEAN', + }, + { + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, name: 'permanent', description: 'Whether to allow permanent mutes or fall back to default mute duration' - }), - new CommandOption({ - type: 'INTEGER', + }, + { + type: 'INTEGER', flag: true, name: 'type', description: 'Select the type of mute behaviour', choices: [ { @@ -89,7 +89,7 @@ class MuteSetting extends Setting { name: 'Type 3 (Use Discord timeouts)', value: 3 } ] - }) + } ] }); @@ -187,7 +187,8 @@ class MuteSetting extends Setting { return role; }; - const hasPermission = guild.members.me.permissions.has('ManageRoles'); + const me = await guild.resolveMember(this.client.user); + const hasPermission = me.permissions.has('ManageRoles'); if (!hasPermission) return { index: 'SETTING_MUTE_ROLEMISSINGPERMISSION', error: true @@ -251,7 +252,7 @@ class MuteSetting extends Setting { for (const channel of channels.values()) { - if (!channel.permissionsFor(guild.members.me).has('ManageRoles')) { + if (!channel.permissionsFor(this.client.user).has('ManageRoles')) { issues.push({ type: 'permission', channel: channel.name }); continue; } diff --git a/src/structure/components/settings/moderation/Staff.js b/src/structure/components/settings/moderation/Staff.js index 6d1da2b..82633b0 100644 --- a/src/structure/components/settings/moderation/Staff.js +++ b/src/structure/components/settings/moderation/Staff.js @@ -23,7 +23,7 @@ class StaffSetting extends Setting { }, { name: 'enabled', description: 'Whether the staff command is in use', - type: 'BOOLEAN' + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true }] }); } diff --git a/src/structure/components/settings/moderation/WordFilter.js b/src/structure/components/settings/moderation/WordFilter.js index 26c49f2..65e9043 100644 --- a/src/structure/components/settings/moderation/WordFilter.js +++ b/src/structure/components/settings/moderation/WordFilter.js @@ -1,5 +1,5 @@ /* eslint-disable camelcase */ -const { FilterSetting, CommandOption } = require('../../../interfaces/'); +const { FilterSetting } = require('../../../interfaces/'); const { Util } = require("../../../../utilities"); const { FilterPresets } = require('../../../../constants'); @@ -44,7 +44,7 @@ class WordFilterSetting extends FilterSetting { actions: { ARRAY: 'ACTION' } }, commandOptions: [ - new CommandOption({ + { type: 'STRING', name: 'method', description: 'Select which modification method to use', @@ -57,8 +57,8 @@ class WordFilterSetting extends FilterSetting { { name: 'list', value: 'list' } ], dependsOn: ['list'] - }), - new CommandOption({ + }, + { type: 'STRING', name: 'list', description: 'Select which list to modify', @@ -73,17 +73,17 @@ class WordFilterSetting extends FilterSetting { { name: 'actions', value: 'actions' }, ], dependsOn: ['method'] - }), - new CommandOption({ - type: 'BOOLEAN', + }, + { + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, name: 'enabled', description: 'Toggle enable state' - }), - new CommandOption({ - type: 'BOOLEAN', + }, + { + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, name: 'silent', description: 'Toggle silent operation' - }) + } ] }); diff --git a/src/structure/components/settings/moderation/WordWatcher.js b/src/structure/components/settings/moderation/WordWatcher.js index 514ae82..7d8a232 100644 --- a/src/structure/components/settings/moderation/WordWatcher.js +++ b/src/structure/components/settings/moderation/WordWatcher.js @@ -1,4 +1,4 @@ -const { FilterSetting, CommandOption } = require('../../../interfaces/'); +const { FilterSetting } = require('../../../interfaces/'); const { Util } = require("../../../../utilities"); class WordWatcher extends FilterSetting { @@ -10,6 +10,7 @@ class WordWatcher extends FilterSetting { description: 'Flag messages for potentially offensive content instead of deleting automatically', module: 'moderation', default: { + enabled: false, channel: null, words: [], regex: [], @@ -18,7 +19,7 @@ class WordWatcher extends FilterSetting { actions: [] }, commandOptions: [ - new CommandOption({ + { type: 'STRING', name: 'method', description: 'Select which modification method to use', @@ -31,8 +32,8 @@ class WordWatcher extends FilterSetting { { name: 'list', value: 'list' } ], dependsOn: ['list'] - }), - new CommandOption({ + }, + { type: 'STRING', name: 'list', description: 'Select which list to modify', @@ -44,12 +45,17 @@ class WordWatcher extends FilterSetting { { name: 'actions', value: 'actions' }, ], dependsOn: ['method'] - }), - new CommandOption({ + }, + { + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, + name: 'enabled', + description: 'Toggle enable state' + }, + { name: 'channel', type: 'TEXT_CHANNEL', - description: 'Where to output flagged messages' - }) + description: 'Where to output flagged messages', + } ] }); } diff --git a/src/structure/components/settings/utility/Autorole.js b/src/structure/components/settings/utility/Autorole.js index 124dca7..cdfd252 100644 --- a/src/structure/components/settings/utility/Autorole.js +++ b/src/structure/components/settings/utility/Autorole.js @@ -1,4 +1,4 @@ -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); const { Util } = require("../../../../utilities"); class Autorole extends Setting { @@ -18,7 +18,7 @@ class Autorole extends Setting { enabled: 'BOOLEAN' }, commandOptions: [ - new CommandOption({ + { type: 'STRING', name: 'roles', description: 'Modification method for roles', @@ -30,12 +30,12 @@ class Autorole extends Setting { { name: 'edit', value: 'edit' }, { name: 'list', value: 'list' } ] - }), - new CommandOption({ - type: 'BOOLEAN', + }, + { + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, name: 'enabled', description: 'Toggle enable state' - }) + } ] }); } diff --git a/src/structure/components/settings/utility/StickyRole.js b/src/structure/components/settings/utility/StickyRole.js index c930a9b..521b43a 100644 --- a/src/structure/components/settings/utility/StickyRole.js +++ b/src/structure/components/settings/utility/StickyRole.js @@ -1,4 +1,4 @@ -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); const { Util } = require("../../../../utilities"); class Autorole extends Setting { @@ -18,7 +18,7 @@ class Autorole extends Setting { enabled: 'BOOLEAN' }, commandOptions: [ - new CommandOption({ + { type: 'STRING', name: 'roles', description: 'Modification method for roles', @@ -30,12 +30,12 @@ class Autorole extends Setting { { name: 'edit', value: 'edit' }, { name: 'list', value: 'list' } ] - }), - new CommandOption({ - type: 'BOOLEAN', + }, + { + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, name: 'enabled', description: 'Toggle enable state' - }) + } ], premium: 1 }); diff --git a/src/structure/components/settings/utility/Welcomer.js b/src/structure/components/settings/utility/Welcomer.js index 820202f..b3c6065 100644 --- a/src/structure/components/settings/utility/Welcomer.js +++ b/src/structure/components/settings/utility/Welcomer.js @@ -1,4 +1,4 @@ -const { Setting, CommandOption } = require("../../../interfaces"); +const { Setting } = require("../../../interfaces"); class Autorole extends Setting { @@ -17,16 +17,16 @@ class Autorole extends Setting { enabled: 'BOOLEAN' }, commandOptions: [ - new CommandOption({ + { type: 'STRING', name: 'message', description: 'Set the welcome message' - }), - new CommandOption({ - type: 'BOOLEAN', + }, + { + type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true, name: 'enabled', description: 'Toggle enable state' - }) + } ] }); } diff --git a/src/structure/infractions/Addrole.js b/src/structure/infractions/Addrole.js index e0ab45d..04a8c2e 100644 --- a/src/structure/infractions/Addrole.js +++ b/src/structure/infractions/Addrole.js @@ -53,7 +53,8 @@ class AddroleInfraction extends Infraction { const { grantable } = await this.guild.settings(); let filtered = []; - const { highest: clientHighest } = this.guild.members.me.roles; + const me = await this.guild.resolveMember(this.client.user); + const { highest: clientHighest } = me.roles; filtered = this.data.roles.filter((r) => r.comparePositionTo(clientHighest) < 0); if (filtered.length === 0) { return super._fail('C_ADDROLE_ROLEHIERARCHYBOT'); diff --git a/src/structure/infractions/Ban.js b/src/structure/infractions/Ban.js index b658092..f596a47 100644 --- a/src/structure/infractions/Ban.js +++ b/src/structure/infractions/Ban.js @@ -48,7 +48,7 @@ class BanInfraction extends Infraction { const callbacks = this.client.moderationManager.callbacks.filter((c) => c.infraction.type === 'BAN' && c.infraction.target === this.target.id); - if (callbacks.size > 0) callbacks.map((c) => this.client.moderationManager.removeCallback(c.infraction)); + if (callbacks.size > 0) callbacks.map((c) => this.client.moderationManager.removeCallback(c.infraction, true)); return this._succeed(); @@ -56,13 +56,14 @@ class BanInfraction extends Infraction { async verify() { - if (this.target instanceof GuildMember) { - if (!this.member.bannable) return super._fail('C_BAN_CANNOTBEBANNED'); - } + const member = await this.guild.resolveMember(this.target.id); + if (member && !member.bannable) + return super._fail('C_BAN_CANNOTBEBANNED'); + let alreadyBanned = null; try { - alreadyBanned = await this.guild.bans.fetch(this.member.id); + alreadyBanned = await this.guild.bans.fetch(this.target.id); } catch (e) { } //eslint-disable-line no-empty if (alreadyBanned) return super._fail('C_BAN_ALREADYBANNED'); diff --git a/src/structure/infractions/Lockdown.js b/src/structure/infractions/Lockdown.js index d25bb23..59657a9 100644 --- a/src/structure/infractions/Lockdown.js +++ b/src/structure/infractions/Lockdown.js @@ -140,9 +140,9 @@ class LockdownInfraction extends Infraction { async verify() { - const perms = this.target.permissionsFor(this.guild.members.me); + const perms = this.target.permissionsFor(this.client.user); const missing = perms.missing(['ManageRoles', 'SendMessages', 'AddReactions']); - if(missing.length) return this._fail('INFRACTION_LOCKDOWN_MISSING_PERMS', { missing: missing.join('**, **') }); + if (missing.length) return this._fail(this.guild.format('INFRACTION_LOCKDOWN_MISSING_PERMS', { missing: missing.join('**, **') }), null, true); return super._verify(); } diff --git a/src/structure/infractions/Mute.js b/src/structure/infractions/Mute.js index 68a0d43..780aa72 100644 --- a/src/structure/infractions/Mute.js +++ b/src/structure/infractions/Mute.js @@ -48,6 +48,8 @@ class MuteInfraction extends Infraction { role = await this.client.resolver.resolveRole(setting.role, true, this.guild); } + const me = await this.guild.resolveMember(this.client.user); + let removed = []; switch (setting.type) { case 0: @@ -60,12 +62,12 @@ class MuteInfraction extends Infraction { break; case 1: removed = this.member.roles.cache.filter((r) => !r.managed && - r.comparePositionTo(this.guild.members.me.roles.highest) < 0 && + r.comparePositionTo(me.roles.highest) < 0 && r.id !== this.guild.id); try { await this.member.roles.set([ ...this.member.roles.cache.filter((r) => r.managed || - r.comparePositionTo(this.guild.members.me.roles.highest) >= 0 || + r.comparePositionTo(me.roles.highest) >= 0 || r.id === this.guild.id).values(), role ], this._reason); @@ -76,11 +78,11 @@ class MuteInfraction extends Infraction { break; case 2: removed = this.member.roles.cache.filter((r) => !r.managed && - r.comparePositionTo(this.guild.members.me.roles.highest) < 0 && + r.comparePositionTo(me.roles.highest) < 0 && r.id !== this.guild.id); try { await this.member.roles.set(this.member.roles.cache.filter((r) => r.managed || - r.comparePositionTo(this.guild.members.me.roles.highest) >= 0 || + r.comparePositionTo(me.roles.highest) >= 0 || r.id === this.guild.id), this._reason); } catch (error) { this.client.logger.error(`Mute infraction failed to calculate removeable roles, might want to check this out.\n${error.stack || error}`); @@ -96,6 +98,8 @@ class MuteInfraction extends Infraction { } } + if (this.member.voice.channel) await this.member.voice.disconnect(this._reason); + this.data = { removedRoles: removed.map((r) => r.id), muteType: setting.type, @@ -107,7 +111,7 @@ class MuteInfraction extends Infraction { if (callback) { this.data.removedRoles = [...new Set([...this.data.removedRoles, ...callback.infraction.data.removedRoles])]; - this.client.moderationManager.removeCallback(callback.infraction); + this.client.moderationManager.removeCallback(callback.infraction, true); } // if(callbacks.size > 0) callbacks.map((c) => this.client.moderationManager._removeExpiration(c)); @@ -134,10 +138,12 @@ class MuteInfraction extends Infraction { return this._fail('COMMAND_MUTE_INVALIDMUTEROLE', true); } } + const me = await this.guild.resolveMember(this.client.user); if (settings.mute.type === 3) { - if (this.guild.members.me.permissions.missing('ModerateMembers').length) return this._fail('COMMAND_MUTE_MISSING_MODERATE_PERM', true); + if (me.permissions.missing('ModerateMembers').length) return this._fail('COMMAND_MUTE_MISSING_MODERATE_PERM', true); + if (me.roles.highest.position <= this.member.roles.highest.position) return this._fail('COMMAND_MUTE_HIERARCHY_ERROR'); // if (!this.duration && !settings.mute.default) - } else if (this.guild.members.me.permissions.missing('ManageRoles').length) return this._fail('COMMAND_MUTE_MISSING_MANAGEROLE_PERM'); + } else if (me.permissions.missing('ManageRoles').length) return this._fail('COMMAND_MUTE_MISSING_MANAGEROLE_PERM'); return super._verify(); diff --git a/src/structure/infractions/Nickname.js b/src/structure/infractions/Nickname.js index 5721dbe..b0618db 100644 --- a/src/structure/infractions/Nickname.js +++ b/src/structure/infractions/Nickname.js @@ -82,7 +82,8 @@ class NicknameInfraction extends Infraction { async verify() { const { highest } = this.member.roles; - if (highest.comparePositionTo(this.guild.members.me.roles.highest) > 0 || !this.guild.members.me.permissions.has('ManageNicknames')) { + const me = await this.guild.resolveMember(this.client.user); + if (highest.comparePositionTo(me.roles.highest) > 0 || !me.permissions.has('ManageNicknames')) { return this._fail('C_NICKNAME_MISSINGPERMISSIONS'); } diff --git a/src/structure/infractions/Removerole.js b/src/structure/infractions/Removerole.js index 58ff10b..b3b041b 100644 --- a/src/structure/infractions/Removerole.js +++ b/src/structure/infractions/Removerole.js @@ -53,7 +53,8 @@ class RemoveroleInfraction extends Infraction { const { grantable } = await this.guild.settings(); let filtered = []; - const { highest: clientHighest } = this.guild.members.me.roles; + const me = await this.guild.resolveMember(this.client.user); + const { highest: clientHighest } = me.roles; filtered = this.data.roles.filter((r) => r.comparePositionTo(clientHighest) < 0); if (filtered.length === 0) { return super._fail('C_REMOVEROLE_ROLEHIERARCHYBOT'); diff --git a/src/structure/infractions/Unban.js b/src/structure/infractions/Unban.js index 4e25ba2..6d757ef 100644 --- a/src/structure/infractions/Unban.js +++ b/src/structure/infractions/Unban.js @@ -39,7 +39,7 @@ class UnbanInfraction extends Infraction { const callbacks = this.client.moderationManager.callbacks.filter((c) => c.infraction.type === 'BAN' && c.infraction.target === this.target.id); - if (callbacks.size > 0) callbacks.map((c) => this.client.moderationManager.removeCallback(c.infraction)); + if (callbacks.size > 0) callbacks.map((c) => this.client.moderationManager.removeCallback(c.infraction, true)); await this.handle(); return this._succeed(); diff --git a/src/structure/infractions/Unlockdown.js b/src/structure/infractions/Unlockdown.js index 6e40292..4654ed1 100644 --- a/src/structure/infractions/Unlockdown.js +++ b/src/structure/infractions/Unlockdown.js @@ -95,9 +95,9 @@ class UnlockdownInfraction extends Infraction { async verify() { - const perms = this.target.permissionsFor(this.guild.members.me); + const perms = this.target.permissionsFor(this.client.user); const missing = perms.missing(['ManageRoles', 'SendMessages', 'AddReactions']); - if(missing.length) return this._fail('INFRACTION_LOCKDOWN_MISSING_PERMS', { missing: missing.join('**, **') }); + if (missing.length) this._fail(this.guild.format('INFRACTION_LOCKDOWN_MISSING_PERMS', { missing: missing.join('**, **') }), null, true); return super._verify(); } diff --git a/src/structure/interfaces/CommandOption.js b/src/structure/interfaces/CommandOption.js index 238b36d..e8c4a1e 100644 --- a/src/structure/interfaces/CommandOption.js +++ b/src/structure/interfaces/CommandOption.js @@ -1,5 +1,7 @@ /* eslint-disable camelcase */ +const { ChannelType } = require("discord.js"); + const Constants = { CommandOptionTypes: { SUB_COMMAND: 1, @@ -28,7 +30,8 @@ const Constants = { ROLE: 8, MENTIONABLE: 9, NUMBER: 10, - FLOAT: 10 + FLOAT: 10, + POINTS: 4 }, ChannelTypes: { TEXT_CHANNEL: 0, @@ -36,12 +39,17 @@ const Constants = { } }; +const PointsReg = /^([-+]?[0-9]+) ?(points|point|pts|pt|p)$/iu; + class CommandOption { constructor(options = {}) { + this._options = options; this.name = options.name; this.description = options.description || "A missing description, let a bot developer know."; + if(!options.client) throw new Error(`${this.name} is missing client`); + this.client = options.client; this.type = Object.keys(Constants.CommandOptionTypes).includes(options.type) ? options.type : 'STRING'; this.required = Boolean(options.required); @@ -52,8 +60,10 @@ class CommandOption { if (options.options) for (const opt of options.options) { // console.log(opt); - if (opt instanceof CommandOption) this.options.push(opt); - else if (opt.name instanceof Array) { + if (opt instanceof CommandOption) { + opt.client = this.client; + this.options.push(opt); + } else if (opt.name instanceof Array) { const { name: names, description, type, dependsOn, ...opts } = opt; for (const name of names) { // console.log(name); @@ -69,9 +79,12 @@ class CommandOption { if (dependsOn instanceof Array) { _dependsOn = dependsOn[index]; } - this.options.push(new CommandOption({ name, type: _type, description: desc, dependsOn: _dependsOn, ...opts })); + this.options.push(new CommandOption({ + client: this.client, name, type: _type, + description: desc, dependsOn: _dependsOn, ...opts + })); } - } else this.options.push(new CommandOption(opt)); + } else this.options.push(new CommandOption({ client: this.client, ...opt })); } // this.options = options.options || []; //Used for SUB_COMMAND/SUB_COMMAND_GROUP types. @@ -83,32 +96,51 @@ class CommandOption { this.minimum = typeof options.minimum === 'number' ? options.minimum : undefined; //Used for INTEGER/NUMBER/FLOAT types. this.maximum = typeof options.maximum === 'number' ? options.maximum : undefined; - this.flag = true; // used with message based command options + this.slashOption = options.slashOption || false; + this.flag = options.flag ?? false; // used with message based command options + this.valueOptional = options.valueOptional ?? false; + this.defaultValue = options.defaultValue ?? null; + this.valueAsAlias = options.choices?.length && (options.valueAsAlias ?? false); + // this.words = options.words ?? null; // Used when parsing strings if the command has multiple string types that aren't flags this.value = undefined; + // Used in cloned options when parsing final value + this.guild = options.guild || null; this._rawValue = options._rawValue ?? null; //Raw value input from Discord. -- use ?? where the value is potentially false, otherwise we end up with false -> null } - usage(guild) { - const name = `》 ${this.name.toUpperCase()} [${this.type}]`; + get guildOnly() { + return ['ROLE', 'MEMBER', 'CHANNEL'].some((t) => this.type.includes(t)); + } + + usage(guild, verbose = false) { + let name = `》 ${this.name.toUpperCase()} [${this.type}]`; + let flagProps = ['flag']; + if (this.valueOptional) flagProps.push('optional value'); + if (this.defaultValue !== null) flagProps.push(`default value: \`${this.defaultValue}\``); + flagProps = `(${flagProps.join(', ')})`; + if(this.flag) name += ` ${flagProps}`; let value = null; + const format = (...args) => guild ? guild.format(...args) : this.client.format(...args); + if (this.type === 'SUB_COMMAND_GROUP') { value = this.options.map((opt) => { - const usage = opt.usage(guild); + const usage = opt.usage(guild, true); return `__${usage.name.replace('》', '').trim()}__\n${usage.value}`; }).join('\n\n'); } else if (this.type === 'SUB_COMMAND') { - if (!this.options.length) value = guild.format('GENERAL_NO_ARGS'); - else value = this.options.map((opt) => opt.usage(guild).value).join('\n'); + if (!this.options.length) value = format('GENERAL_NO_ARGS'); + else value = this.options.map((opt) => opt.usage(guild, true).value).join('\n'); } else { - value = `**${this.name} [${this.type}]:** ${this.description}`; + value = `${verbose ? `**${this.name} [${this.type}]** ${this.flag ? flagProps : ''}\n` : ''}`; + value += `${this.description}`; if (this.choices.length) - value += `\n__${guild.format('GENERAL_CHOICES')}__: ${this.choices.map((choice) => choice.name).join(', ')}`; + value += `\n__${format('GENERAL_CHOICES')}__: ${this.choices.map((choice) => choice.name).join(', ')}`; if (this.dependsOn.length) - value += `\n${guild.format('GENERAL_DEPENDSON', { dependencies: this.dependsOn.join('`, `') })}`; + value += `\n${format('GENERAL_DEPENDSON', { dependencies: this.dependsOn.join('`, `') })}`; if (this.minimum !== undefined) value += `\nMIN: \`${this.minimum}\``; if (this.maximum !== undefined) { @@ -130,20 +162,268 @@ class CommandOption { * @return {CommandOption} * @memberof CommandOption */ - clone(value) { + clone(_rawValue, guild, slashOption = false) { return new CommandOption({ - name: this.name, type: this.type, - minimum: this.minimum, maximum: this.maximum, - dependsOn: this.dependsOn, dependsOnMode: this.dependsOnMode, - _rawValue: value, strict: this.strict + ...this._options, _rawValue, guild, slashOption }); } + async parse() { + if(!this._rawValue && !this.valueOptional) throw new Error(`Null _rawValue`); + // console.log('-------PARSE BEGIN---------'); + // console.log('1', this.name, this._rawValue, this.valueOptional); + const { removed, value, error } = await this.types[this.type](); + // console.log('2', removed, value, error); + // console.log('--------PARSE END----------'); + if(error) return { error }; + this.value = value; + return removed || []; + } + + format(index, params, opts) { + if (this.guild) return this.guild.format(index, params, opts); + return this.client.format(index, params, opts); + } + + get types() { + return { + POINTS: () => { + if(this.slashOption) return { value: this._rawValue }; + let value = null, + removed = null; + for (const str of this._rawValue) { + const num = parseInt(str); + if (isNaN(num)) continue; + if(PointsReg.test(str)) { + value = num; removed = [str]; + break; + } + const index = this._rawValue.indexOf(str); + const next = this._rawValue[index + 1]; + const tmp = str + next; + if (PointsReg.test(tmp)) { + value = num; removed= [str, next]; + break; + } + } + + if (this.minimum !== undefined && value < this.minimum) return { error: true }; + if (this.maximum !== undefined && value > this.maximum) return { error: true }; + return { value, removed }; + }, + ROLES: async () => { + const roles = [], + removed = []; + for (const str of this._rawValue) { + const role = await this.guild.resolveRole(str, this.strict); + if (role) { + roles.push(role); + removed.push(str); + } else if(roles.length) break; + } + if (!roles.length) return { error: true }; + return { value: roles, removed }; + }, + MEMBERS: async () => { + const members = [], + removed = []; + for (const arg of this._rawValue) { + const member = await this.guild.resolveMember(arg, this.strict); + if (member) { + members.push(member); + removed.push(arg); + } else if(members.length) break; + } + if (!members.length) return { error: true, message: this.strict ? this.format('O_COMMANDHANDLER_TYPEMEMBER_STRICT') : null }; + return { value: members, removed }; + }, + USERS: async () => { + const users = [], + removed = []; + for (const arg of this._rawValue) { + const user = await this.client.resolveUser(arg, this.strict); + if (user) { + users.push(user); + removed.push(arg); + } else if(users.length) break; + } + if (!users.length) return { error: true, message: this.strict ? this.format('O_COMMANDHANDLER_TYPEUSERS_STRICT') : null }; + return { value: users, removed }; + }, + CHANNELS: async () => { + const channels = [], + removed = []; + for (const arg of this._rawValue) { + const channel = await this.guild.resolveChannel(arg, this.strict); + if (channel) { + channels.push(channel); + removed.push(arg); + } else if(channels.length) break; + } + if (!channels.length) return { error: true }; + return { value: channels, removed }; + }, + TEXT_CHANNELS: async () => { + const channels = [], + removed = []; + for(const arg of this._rawValue) { + const channel = await this.guild.resolveChannel(arg, this.strict, (channel) => channel.type === ChannelType.GuildText); + if (channel) { + channels.push(channel); + removed.push(arg); + } else if(channels.length) break; + } + if (!channels.length) return { error: true }; + return { value: channels, removed }; + }, + VOICE_CHANNELS: async () => { + const channels = [], + removed = []; + for (const arg of this._rawValue) { + const channel = await this.guild.resolveChannel(arg, this.strict, (channel) => channel.type === ChannelType.GuildVoice); + if (channel) { + channels.push(channel); + removed.push(arg); + } else if(channels.length) break; + } + if (!channels.length) return { error: true }; + return { value: channels, removed }; + }, + TIME: () => { + const value = this.client.resolver.resolveTime(this._rawValue); + if (value === null) return { error: true }; + return { value, removed: [this._rawValue] }; + }, + COMPONENT: () => { + const [component] = this.client.resolver.components(this._rawValue, 'any'); + if (!component) return { error: true }; + return { value: component, removed: [this._rawValue] }; + }, + COMPONENTS: () => { + const strings = this._rawValue; + const components = [], + removed = []; + for (const str of strings) { + const [component] = this.client.resolver.components(str, 'any'); + if (component && !components.includes(component)) { + components.push(component); + removed.push(str); + } else if(components.length) break; + } + if (!components.length) return { error: true }; + return { value: components, removed }; + }, + COMMAND: () => { + const [command] = this.client.resolver.components(this._rawValue, 'command'); + if (!command) return { error: true }; + return { value: command, removed: [this._rawValue] }; + }, + COMMANDS: () => { + const strings = this._rawValue; + const commands = [], + removed = []; + for (const str of strings) { + const [command] = this.client.resolver.components(str, 'command'); + if (command && !commands.includes(command)) { + commands.push(command); + removed.push(str); + } else if(commands.length) break; + } + if (!commands.length) return { error: true }; + return { value: commands, removed }; + }, + MODULE: () => { + const [module] = this.client.resolver.components(this._rawValue, 'module'); + if (!module) return { error: true }; + return { value: module, removed: [this._rawValue] }; + }, + STRING: () => { + if (this.slashOption) return { value: this._rawValue }; + if (this._aliased) return { value: this._rawValue, removed: [] }; + if (this.choices.length) { + const found = this.choices.find((c) => c.value.toLowerCase() === this._rawValue.toLowerCase()); + if (found) return { value: found.value, removed: [this._rawValue] }; + return { error: true }; + } + return { value: this._rawValue, removed: [this._rawValue] }; + }, + INTEGER: () => { + const integer = parseInt(this._rawValue); + if(isNaN(integer)) return { error: true }; + if (this.minimum !== undefined && integer < this.minimum) return { error: true }; + if (this.maximum !== undefined && integer > this.maximum) return { error: true }; + return { value: integer, removed: [this._rawValue] }; + }, + BOOLEAN: () => { + const boolean = this.client.resolver.resolveBoolean(this._rawValue); + if (boolean === null && this.valueOptional) return { value: this.defaultValue, removed: [] }; + else if(boolean === null) return { error: true }; + return { value: boolean, removed: [this._rawValue] }; + }, + MEMBER: async () => { + const member = await this.guild.resolveMember(this._rawValue, this.strict); + if (!member) return { error: true }; + return { value: member, removed: [this._rawValue] }; + }, + USER: async () => { + const user = await this.client.resolver.resolveUser(this._rawValue, this.strict); + if(!user) return { error: true }; + return { value: user, removed: [this._rawValue] }; + }, + TEXT_CHANNEL: async () => { + const channel = await this.guild.resolveChannel(this._rawValue); + if (!channel || channel.type !== ChannelType.GuildText) return { error: true }; + return { value: channel, removed: [this._rawValue] }; + }, + VOICE_CHANNEL: async () => { + const channel = await this.guild.resolveChannel(this._rawValue); + if (!channel || channel.type !== ChannelType.GuildVoice) return { error: true }; + return { value: channel, removed: [this._rawValue] }; + }, + CHANNEL: async () => { + const channel = await this.guild.resolveChannel(this._rawValue); + if(!channel) return { error: true }; + return { value: channel, removed: [this._rawValue] }; + }, + ROLE: async () => { + const role = await this.guild.resolveRole(this._rawValue); + if(!role) return { error: true }; + return { value: role, removed: [this._rawValue] }; + }, + MENTIONABLE: (mentionable) => { + return { value: mentionable }; + }, + NUMBER: () => { + const number = parseFloat(this._rawValue); + if(isNaN(number))return { error: true }; + if (this.minimum !== undefined && number < this.minimum) return { error: true }; + if (this.maximum !== undefined && number > this.maximum) return { error: true }; + return { value: number, removed: [this._rawValue] }; + }, + FLOAT: () => { + const float = parseFloat(this._rawValue); + if(isNaN(float)) return { error: true }; + if (this.minimum !== undefined && float < this.minimum) return { error: true }; + if (this.maximum !== undefined && float > this.maximum) return { error: true }; + return { value: parseFloat(float), removed: [this._rawValue] }; + }, + DATE: async () => { + const date = await this.client.resolver.resolveDate(this._rawValue); + if (!date) return { error: true }; + return { value: date, removed: [this._rawValue] }; + } + }; + } + + get plural() { + return this.type.endsWith('S'); + } + get raw() { return { name: this.name, type: this.type, - options: [] + options: this.options.map((opt) => opt.raw) }; } diff --git a/src/structure/interfaces/Infraction.js b/src/structure/interfaces/Infraction.js index cdac3da..4793e8d 100644 --- a/src/structure/interfaces/Infraction.js +++ b/src/structure/interfaces/Infraction.js @@ -9,6 +9,7 @@ const { } = require('../../constants'); const { Util } = require('../../utilities'); +const { inspect } = require('util'); const Constants = { MaxCharacters: 1024, // Max embed description is 2048 characters, however some of those description characters are going to usernames, types, filler text, etc. @@ -147,7 +148,7 @@ class Infraction { if(this._mongoId) filter._id = this._mongoId; return this.client.storageManager.mongodb.infractions.updateOne(filter, this.json) .catch((error) => { - this.client.logger.error(`There was an issue saving infraction data to the database.\n${error.stack || error}\nInfraction data:\n${this.json}`); + this.client.logger.error(`There was an issue saving infraction data to the database.\n${error.stack || error}\nInfraction data:\n${inspect(this.json)}`); }); } @@ -325,9 +326,10 @@ class Infraction { if (protection.type === 'position') { const executorHighest = executor.roles.highest; const targetHighest = target.roles.highest; - if (executorHighest.comparePositionTo(targetHighest) < 0) { + if (executorHighest.comparePositionTo(targetHighest) === 0) + return this._fail('INFRACTION_PROTECTIONPOSITIONERROR_SAME'); + if (executorHighest.comparePositionTo(targetHighest) < 0) return this._fail('INFRACTION_PROTECTIONPOSITIONERROR'); - } } else if (protection.type === 'role') { const contains = target.roles.cache.some((r) => protection.roles.includes(r.id)); if (contains) { diff --git a/src/structure/interfaces/Setting.js b/src/structure/interfaces/Setting.js index 768568e..070cc73 100644 --- a/src/structure/interfaces/Setting.js +++ b/src/structure/interfaces/Setting.js @@ -7,7 +7,6 @@ const Component = require("./Component.js"); // Imports to enable JSDocs typing // eslint-disable-next-line no-unused-vars const InteractionWrapper = require("../client/wrappers/InteractionWrapper.js"); -const CommandOption = require('./CommandOption.js'); // eslint-disable-next-line no-unused-vars // const { DiscordClient } = require("../DiscordClient.js"); @@ -70,29 +69,9 @@ class Setting extends Component { this.default = { [this.name]: options.default || {} }; this.definitions = options.definitions || {}; // Used for the API for field definitions - this.commandOptions = []; + this.commandOptions = options.commandOptions || []; this.commandType = options.commandType || 'SUB_COMMAND'; - if (options.commandOptions) - for (const opt of options.commandOptions) { - if (opt instanceof CommandOption) this.commandOptions.push(opt); - else if (opt.name instanceof Array) { - const { name: names, description, type, ...opts } = opt; - for (const name of names) { - // console.log(name); - const index = names.indexOf(name); - let desc = description, - _type = type; - if (description instanceof Array) desc = description[index] || 'Missing description'; - if (type instanceof Array) { - _type = type[index]; - if (!_type) throw new Error(`Missing type for option ${name} in command ${this.name}`); - } - this.commandOptions.push(new CommandOption({ name, type: _type, description: desc, ...opts })); - } - } else this.commandOptions.push(new CommandOption(opt)); - } - this.clientPermissions = options.clientPermissions || []; this.memberPermissions = options.memberPermissions || []; // Idk if we'll end up using this but it's here anyway @@ -125,22 +104,23 @@ class Setting extends Component { fields.push({ name: `》 ${guild.format(`GENERAL_OPTIONS`)}`, - value: options.map( - (opt) => { - let msg = `**${opt.name} [${opt.type}]:** ${opt.description}`; - if (opt.choices.length) - msg += `\n__${guild.format('GENERAL_CHOICES')}__: ${opt.choices.map((choice) => choice.name).join(', ')}`; - if (opt.dependsOn.length) - msg += `\n${guild.format('GENERAL_DEPENDSON', { dependencies: opt.dependsOn.join('`, `') })}`; - if (opt.minimum !== undefined) - msg += `\nMIN: \`${opt.minimum}\``; - if (opt.maximum !== undefined) { - const newline = opt.minimum !== undefined ? ', ' : '\n'; - msg += `${newline}MAX: \`${opt.maximum}\``; - } - return msg; - } - ).join('\n\n') + value: options.map((opt) => opt.usage(guild, true)).map((f) => f.value).join('\n\n') + // value: options.map( + // (opt) => { + // let msg = `**${opt.name} [${opt.type}]:** ${opt.description}`; + // if (opt.choices.length) + // msg += `\n__${guild.format('GENERAL_CHOICES')}__: ${opt.choices.map((choice) => choice.name).join(', ')}`; + // if (opt.dependsOn.length) + // msg += `\n${guild.format('GENERAL_DEPENDSON', { dependencies: opt.dependsOn.join('`, `') })}`; + // if (opt.minimum !== undefined) + // msg += `\nMIN: \`${opt.minimum}\``; + // if (opt.maximum !== undefined) { + // const newline = opt.minimum !== undefined ? ', ' : '\n'; + // msg += `${newline}MAX: \`${opt.maximum}\``; + // } + // return msg; + // } + // ).join('\n\n') }); } @@ -203,16 +183,16 @@ class Setting extends Component { if (!message.length && !index && !embed) throw new Error('Must declare either message, index or embeds'); const response = await invoker.promptMessage( index ? invoker.format(index, params) : message, - { time, editReply: true, embed } + { time, editReply: invoker.replied, embed } ); if (!response) return { error: true, message: invoker.format('ERR_TIMEOUT') }; const content = response.content.toLowerCase(); - if(invoker.channel.permissionsFor(invoker.guild.members.me).has('ManageMessages')) + if (invoker.channel.permissionsFor(this.client.user).has('ManageMessages')) await response.delete().catch(() => null); if (['cancel', 'abort', 'exit'].includes(content)) return { error: true, - message: invoker.format('ERR_CANCEL') + content: invoker.format('ERR_CANCEL') }; else if (!content.length) return { error: true, index: 'SETTING_NOCONTENT' }; @@ -241,7 +221,8 @@ class Setting extends Component { for (const param of params) { if (list.includes(param)) { - list.splice(list.indexOf(!caseSensitive ? param : param.toLowerCase()), 1); + const [removed] = list.splice(list.indexOf(!caseSensitive ? param : param.toLowerCase()), 1); + modified.push(removed); } else skipped.push(param); } diff --git a/src/structure/interfaces/commands/Command.js b/src/structure/interfaces/commands/Command.js index 681f54d..55d47f7 100644 --- a/src/structure/interfaces/commands/Command.js +++ b/src/structure/interfaces/commands/Command.js @@ -51,8 +51,10 @@ class Command extends Component { this.options = []; if (options.options) for (const opt of options.options) { - if (opt instanceof CommandOption) this.options.push(opt); - else if (opt.name instanceof Array) { + if (opt instanceof CommandOption) { + opt.client = client; + this.options.push(opt); + } else if (opt.name instanceof Array) { // Allows easy templating of subcommands that share arguments const { name: names, description, type, ...opts } = opt; for (const name of names) { @@ -64,9 +66,9 @@ class Command extends Component { _type = type[index]; if (!_type) throw new Error(`Missing type for option ${name} in command ${this.name}`); } - this.options.push(new CommandOption({ name, type: _type, description: desc, ...opts })); + this.options.push(new CommandOption({ name, type: _type, description: desc, ...opts, client })); } - } else this.options.push(new CommandOption(opt)); + } else this.options.push(new CommandOption({ ...opt, client })); } this.options.sort((a, b) => { @@ -107,7 +109,11 @@ class Command extends Component { const fields = []; const { guild, subcommand, subcommandGroup } = invoker; - const { permissions: { type } } = guild._settings; + + let type = null; + const format = (index) => guild ? guild.format(index) : this.client.format(index); + if (guild) ({ permissions: { type } } = guild._settings); + if (this.options.length) { if (verbose) fields.push(...this.options.map((opt) => opt.usage(guild))); @@ -126,7 +132,7 @@ class Command extends Component { else if (type === 'grant') required = [this.resolveable]; else required = [this.resolveable, ...this.memberPermissions]; fields.push({ - name: `》 ${guild.format('GENERAL_PERMISSIONS')}`, + name: `》 ${format('GENERAL_PERMISSIONS')}`, value: `\`${required.join('`, `')}\`` }); } @@ -135,7 +141,7 @@ class Command extends Component { author: { name: `${this.name} [module:${this.module.name}]` }, - description: guild.format(`COMMAND_${this.name.toUpperCase()}_HELP`), + description: format(`COMMAND_${this.name.toUpperCase()}_HELP`), fields }); diff --git a/src/structure/interfaces/commands/ModerationCommand.js b/src/structure/interfaces/commands/ModerationCommand.js index ecf339b..ade1577 100644 --- a/src/structure/interfaces/commands/ModerationCommand.js +++ b/src/structure/interfaces/commands/ModerationCommand.js @@ -4,6 +4,7 @@ class ModerationCommand extends SlashCommand { constructor(client, opts) { + const flag = true; let baseOptions = [ { name: 'users', @@ -13,28 +14,32 @@ class ModerationCommand extends SlashCommand { strict: true }, { name: 'points', - type: 'INTEGER', + type: 'POINTS', description: 'The amount of points to assign to the infraction', minimum: 0, maximum: 100 }, { name: 'expiration', type: 'TIME', - description: 'How long until the points expire' + description: 'How long until the points expire', + flag }, { name: 'prune', type: 'INTEGER', description: 'How many messages to prune', minimum: 2, - maximum: 100 + maximum: 100, + flag }, { name: 'force', type: 'BOOLEAN', - description: 'Whether to override automod' + description: 'Whether to override automod', + flag, valueOptional: true, defaultValue: true }, { name: 'silent', type: 'BOOLEAN', - description: 'Whether the user should receive the infraction' + description: 'Whether the user should receive the infraction', + flag, valueOptional: true, defaultValue: true }, { name: 'reason', type: 'STRING', @@ -42,7 +47,6 @@ class ModerationCommand extends SlashCommand { } ]; - // Probably temporary if(!opts.memberPermissions) throw new Error(`MISSING PERMS ${opts.name}`); if (opts.skipOptions) for (const opt of opts.skipOptions) { diff --git a/src/structure/interfaces/commands/SettingsCommand.js b/src/structure/interfaces/commands/SettingsCommand.js index 64b034d..dfa3866 100644 --- a/src/structure/interfaces/commands/SettingsCommand.js +++ b/src/structure/interfaces/commands/SettingsCommand.js @@ -10,26 +10,16 @@ class SettingsCommand extends SlashCommand { super(client, { ...options, guildOnly: true, - memberPermissions: ['ManageGuild'] - }); - - /* - { - name: 'settings', - description: "Configure the bot's behaviour in your server", - module: 'administration', + memberPermissions: ['ManageGuild'], options: [ - - ], - guildOnly: true - } - */ - - this.options.push(new CommandOption({ - type: 'SUB_COMMAND', - name: 'list', - description: 'List available settings' - })); + { + type: 'SUB_COMMAND', + name: 'list', + description: 'List available settings' + } + ] + }); + this.build(); } @@ -50,9 +40,12 @@ class SettingsCommand extends SlashCommand { name: setting.name, description: setting.description, type: setting.commandType, //'SUB_COMMAND', - options: setting.commandOptions + options: setting.commandOptions, + client: this.client }); this.options.push(subCommand); + // Overwrite the setting options with the built CommandOption structures + setting.commandOptions = subCommand.options; } // for (const module of modules) { @@ -89,7 +82,8 @@ class SettingsCommand extends SlashCommand { if (!setting) return invoker.reply('Something went wrong, could not find setting'); if (setting.clientPermissions.length) { - const missing = guild.members.me.permissions.missing(setting.clientPermissions); + const me = await guild.resolveMember(this.client.user); + const missing = me.permissions.missing(setting.clientPermissions); if (missing.length) return invoker.reply({ emoji: 'failure', index: 'SETTING_MISSING_CLIENTPERMISSIONS', diff --git a/src/utilities/FilterUtil.js b/src/utilities/FilterUtil.js index 39e0ae4..032d68f 100644 --- a/src/utilities/FilterUtil.js +++ b/src/utilities/FilterUtil.js @@ -197,6 +197,8 @@ module.exports = class FilterUtility { //Zero width character (UTF-16 8206) content = content.replace(/‎/gu, ''); + content = content.replace(/['"‘’“”]/gu, ''); + //Replace the weird letters with their normal text counterparts // eslint-disable-next-line no-useless-escape const match = (/[a-z0-9\w\(\)\.\\\/\?!]+/gimu).exec(content); @@ -321,7 +323,9 @@ module.exports = class FilterUtility { matched: true, _matcher: _word, matcher: `fuzzy [\`${_word}\`, \`${sim}\`, \`${threshold}\`]`, - type: 'fuzzy' + type: 'fuzzy', + _threshold: threshold, + _sim: sim }; } diff --git a/src/utilities/SettingsMigrator.js b/src/utilities/SettingsMigrator.js index 12676ef..9e93a3d 100644 --- a/src/utilities/SettingsMigrator.js +++ b/src/utilities/SettingsMigrator.js @@ -206,7 +206,7 @@ class SettingsMigrator { stickyrole: result.stickyrole ? { ...result.stickyrole, enabled: Boolean(result.stickyrole.roles.length) } : undefined, welcomer: result.welcomer, commands: { disabled: result.disabledCommands, custom: {} }, - prefix: result.prefix + textcommands: { prefix: result.prefix, enabled: false } }; return settings; } @@ -287,14 +287,15 @@ class SettingsMigrator { }; if (linkfilter) settings.linkfilter = { - enabled: result.linkfilter?.enabled || false, - silent: result.linkfilter?.silent || false, - ignore: result.linkfilter?.channels || [], - bypass: result.linkfilter?.roles || [], + enabled: linkfilter.enabled || false, + silent: linkfilter.silent || false, + ignore: linkfilter.channels || [], + bypass: linkfilter.roles || [], actions: [], - whitelist: result.linkfilter?.whitelist === true ? result.linkfilter.filter : [], - blacklist: result.linkfilter?.whitelist === false ? result.linkfilter.filter : [], - greylist: [] + whitelist: linkfilter.whitelist === true ? linkfilter.filter : [], + blacklist: linkfilter.whitelist === false ? linkfilter.filter : [], + greylist: [], + whitelistMode: linkfilter.whitelist || false }; if (ignore) settings.ignore = { @@ -334,7 +335,7 @@ class SettingsMigrator { }; if (autorole) settings.autorole = autorole; - if (prefix) settings.prefix = prefix; + if (prefix) settings.textcommands = { prefix, enabled: false }; if (welcomer) { settings.welcomer.enabled = welcomer.enabled; settings.welcomer.message = welcomer.message || null; @@ -398,7 +399,8 @@ class SettingsMigrator { silent: false, bypass: result.invitefilter.roles, enabled: result.invitefilter.enabled, - actions: [] + actions: [], + whitelist: [] }; const channels = Object.entries(invitefilter.channels || {}); for (const [id, value] of channels) {