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/observers/en_gb_commandHandler.lang b/src/localization/en_gb/observers/en_gb_commandHandler.lang index bbe55fc..c9a9990 100644 --- a/src/localization/en_gb/observers/en_gb_commandHandler.lang +++ b/src/localization/en_gb/observers/en_gb_commandHandler.lang @@ -8,12 +8,18 @@ This command can only be run 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 +29,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. @@ -82,4 +91,12 @@ 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. \ No newline at end of file +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/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/components/observers/CommandHandler.js b/src/structure/components/observers/CommandHandler.js index b5fe6f1..273c78f 100644 --- a/src/structure/components/observers/CommandHandler.js +++ b/src/structure/components/observers/CommandHandler.js @@ -2,6 +2,7 @@ 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'); class CommandHandler extends Observer { @@ -55,7 +56,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 +80,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,6 +92,26 @@ 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 && response.index) { return invoker.reply(response); @@ -135,9 +156,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({ @@ -176,9 +195,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 = {}; @@ -195,14 +214,10 @@ 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); + 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) { @@ -213,7 +228,7 @@ class CommandHandler extends Observer { break; } - newOption.value = parsed.value; + // newOption.value = parsed.value; options[matched.name] = newOption; } @@ -225,18 +240,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 @@ -246,13 +249,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); @@ -260,11 +264,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; @@ -278,11 +282,23 @@ class CommandHandler extends Observer { const flags = activeCommand.options.filter((opt) => opt.flag); params = Util.parseQuotes(params.join(' ')).map(([x]) => x); + let currentFlag = null; + // console.log('params', params); + // Parse flags for (let index = 0; index < params.length;) { const match = (/(?:^| )(?(?:--[a-z0-9]{3,})|(?:-[a-z]{1,2}))(?:$| )/iu).exec(params[index]); if (!match) { + 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; } @@ -291,23 +307,112 @@ class CommandHandler extends Observer { const flag = flags.find((f) => f.name === _flag.toLowerCase()); if (!flag) return { error: true, index: 'O_COMMANDHANDLER_UNRECOGNISED_FLAG', params: { flag: _flag } }; - params.splice(index, 1); - args[flag.name] = flag.clone(params[index]); - params.splice(index, 1); + params.splice(index, 1, null); + currentFlag = flag.clone(null, guild); + args[flag.name] = currentFlag; } - const options = activeCommand.options.filter((opt) => !opt.flag); + // 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) return { option: flag, ...removed }; + for(const r of removed) params.splice(params.indexOf(r), 1); + } + // console.log('params', params); + const options = activeCommand.options.filter((opt) => !opt.flag && opt.type !== 'STRING'); + const stringOpts = activeCommand.options.filter((opt) => !opt.flag && 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; - return { options: { args, parameters: params }, verbose: true }; + args[cloned.name] = cloned; + // Clean up params for string parsing + for (const r of removed) params.splice(params.indexOf(r), 1, null); + + } + + const strings = []; + let tmpString = ''; + // console.log('strings loop'); + // Compile strings into groups of strings so we don't get odd looking strings from which options have been parsed out of + for (const str of params) { + // console.log(str); + if (!str) { + // console.log('null string'); + if (tmpString.length) { + // console.log('pushing'); + strings.push(tmpString); + tmpString = ''; + } + continue; + } + // params.splice(params.indexOf(str), 1); + tmpString += ` ${str}`; + tmpString = tmpString.trim(); + } + 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; + } + + 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; + 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);