text based command parsing

This commit is contained in:
Erik 2022-07-26 20:36:34 +03:00
parent ac0af3f35e
commit 9b6c792514
Signed by untrusted user: Navy.gif
GPG Key ID: 811EC0CD80E7E5FB
4 changed files with 161 additions and 39 deletions

View File

@ -26,4 +26,4 @@ The command **{command}** requires the __bot__ to have permissions to use.
The command **{command}** can only be run by developers. The command **{command}** can only be run by developers.
[INHIBITOR_GUILDONLY_ERROR] [INHIBITOR_GUILDONLY_ERROR]
The command **{command}** is only available in servers. The command **{command}** is only available in servers.

View File

@ -8,12 +8,18 @@ This command can only be run in servers.
[O_COMMANDHANDLER_TYPEINTEGER] [O_COMMANDHANDLER_TYPEINTEGER]
The command option {option} requires an integer between `{min}` and `{max}`. 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] [O_COMMANDHANDLER_TYPECOMMANDS]
The command option {option} requires command resolveables (e.g. `command:ping`, `command:history`) The command option {option} requires command resolveables (e.g. `command:ping`, `command:history`)
[O_COMMANDHANDLER_TYPECOMMAND] [O_COMMANDHANDLER_TYPECOMMAND]
The command option {option} requires a command resolveable (e.g. `command:ping`) 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] [O_COMMANDHANDLER_TYPESTRING]
The command option {option} requires a string. 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] [O_COMMANDHANDLER_TYPETIME]
The command option {option} requires a timestring (e.g. 5 min, 2w). 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] [O_COMMANDHANDLER_TYPEUSERS]
The command option {option} requires users. 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] [O_COMMANDHANDLER_UNRECOGNISED_FLAG]
Unrecognised flag: `{flag}` Unrecognised flag: `{flag}`
See command help for valid flags. 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}`.

View File

@ -118,7 +118,7 @@ class InvokerWrapper {
disableMentions: opts.disableMentions 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) else await this.reply(data) //this.channel.send(data)
.then((msg) => { .then((msg) => {
if (opts.delete) msg.delete(); if (opts.delete) msg.delete();

View File

@ -2,6 +2,7 @@ const { EmbedBuilder, Message, ChannelType, ComponentType, ButtonStyle } = requi
const { Util } = require('../../../utilities'); const { Util } = require('../../../utilities');
const { InvokerWrapper, MessageWrapper } = require('../../client/wrappers'); const { InvokerWrapper, MessageWrapper } = require('../../client/wrappers');
const { Observer, CommandError } = require('../../interfaces/'); const { Observer, CommandError } = require('../../interfaces/');
const { inspect } = require('util');
class CommandHandler extends Observer { 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 // There was an error if _parseResponse return value is truthy, i.e. an error message was sent
if (await this._parseResponse(invoker, response)) return; 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] }); if (inhibitors.length) return this._generateError(invoker, { type: 'inhibitor', ...inhibitors[0] });
await invoker.deferReply(); await invoker.deferReply();
const response = await this._parseInteraction(interaction, command); const response = await this._parseInteraction(invoker, command);
if (await this._parseResponse(invoker, response)) return; if (await this._parseResponse(invoker, response)) return;
try { // Temp logging try { // Temp logging
@ -91,6 +92,26 @@ class CommandHandler extends Observer {
} }
_parseResponse(invoker, response) { _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; const { command } = invoker;
if (response.error && response.index) { if (response.error && response.index) {
return invoker.reply(response); return invoker.reply(response);
@ -135,9 +156,7 @@ class CommandHandler extends Observer {
} }
async _executeCommand(invoker, options) { 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; let response = null;
const now = Date.now(); const now = Date.now();
if (this.client.developmentMode && !this.client.developers.includes(invoker.user.id)) return invoker.reply({ 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; let error = null;
const options = {}; const options = {};
@ -195,14 +214,10 @@ class CommandHandler extends Observer {
continue; continue;
} }
// const newOption = new CommandOption({ const rawValue = matched.plural && typeof option.value === 'string' ? Util.parseQuotes(option.value).map(([x]) => x) : option.value;
// name: matched.name, type: matched.type, const newOption = matched.clone(rawValue, guild, true);
// minimum: matched.minimum, maximum: matched.maximum, const parsed = await newOption.parse();
// _rawValue: option.value, // const parsed = await this._parseOption(interaction, newOption);
// dependsOn: matched.dependsOn, dependsOnMode: matched.dependsOnMode
// });
const newOption = matched.clone(option.value);
const parsed = await this._parseOption(interaction, newOption);
// console.log(parsed); // console.log(parsed);
if(parsed.error) { if(parsed.error) {
@ -213,7 +228,7 @@ class CommandHandler extends Observer {
break; break;
} }
newOption.value = parsed.value; // newOption.value = parsed.value;
options[matched.name] = newOption; options[matched.name] = newOption;
} }
@ -225,18 +240,6 @@ class CommandHandler extends Observer {
if(!options[req.name]) return { option: req, error: true, required: true }; 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 { return {
error: false, error: false,
options options
@ -246,13 +249,14 @@ class CommandHandler extends Observer {
async _parseMessage(invoker, params) { async _parseMessage(invoker, params) {
const { command, target: message } = invoker; const { command, target: message, guild } = invoker;
const { subcommands, subcommandGroups } = command; const { subcommands, subcommandGroups } = command;
const args = {}; const args = {};
// console.log(options); // console.log(options);
let group = null, let group = null,
subcommand = null; subcommand = null;
// Parse out subcommands
if (subcommandGroups.length || subcommands.length) { if (subcommandGroups.length || subcommands.length) {
const [first, second, ...rest] = params; const [first, second, ...rest] = params;
group = command.subcommandGroup(first); 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 // 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 // But for now I'm followin discord's structure for commands
if (!group) { if (!group) {
subcommand = command.subcommand(first)?.raw; subcommand = command.subcommand(first);
params = []; params = [];
if (second) params.push(second, ...rest); if (second) params.push(second, ...rest);
} else { } else {
subcommand = command.subcommand(second)?.raw; subcommand = command.subcommand(second);
params = rest; params = rest;
} }
message.subcommand = subcommand; message.subcommand = subcommand;
@ -278,11 +282,23 @@ class CommandHandler extends Observer {
const flags = activeCommand.options.filter((opt) => opt.flag); const flags = activeCommand.options.filter((opt) => opt.flag);
params = Util.parseQuotes(params.join(' ')).map(([x]) => x); params = Util.parseQuotes(params.join(' ')).map(([x]) => x);
let currentFlag = null;
// console.log('params', params);
// Parse flags
for (let index = 0; index < params.length;) { for (let index = 0; index < params.length;) {
const match = (/(?:^| )(?<flag>(?:--[a-z0-9]{3,})|(?:-[a-z]{1,2}))(?:$| )/iu).exec(params[index]); const match = (/(?:^| )(?<flag>(?:--[a-z0-9]{3,})|(?:-[a-z]{1,2}))(?:$| )/iu).exec(params[index]);
if (!match) { 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++; index++;
continue; continue;
} }
@ -291,23 +307,112 @@ class CommandHandler extends Observer {
const flag = flags.find((f) => f.name === _flag.toLowerCase()); const flag = flags.find((f) => f.name === _flag.toLowerCase());
if (!flag) return { error: true, index: 'O_COMMANDHANDLER_UNRECOGNISED_FLAG', params: { flag: _flag } }; if (!flag) return { error: true, index: 'O_COMMANDHANDLER_UNRECOGNISED_FLAG', params: { flag: _flag } };
params.splice(index, 1); params.splice(index, 1, null);
args[flag.name] = flag.clone(params[index]); currentFlag = flag.clone(null, guild);
params.splice(index, 1); 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) { async _parseOption(interaction, option) {
const { guild } = interaction; const { guild } = interaction;
const types = { const types = {
POINTS: (value) => {
return { value };
},
ROLES: async (string) => { ROLES: async (string) => {
const args = Util.parseQuotes(string).map(([str]) => str); const args = Util.parseQuotes(string).map(([str]) => str);
const roles = await guild.resolveRoles(args); const roles = await guild.resolveRoles(args);