forked from Galactic/galactic-bot
text based command parsing
This commit is contained in:
parent
ac0af3f35e
commit
9b6c792514
@ -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.
|
||||
The command **{command}** is only available in servers.
|
@ -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.
|
||||
|
||||
@ -83,3 +92,11 @@ 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}`.
|
@ -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();
|
||||
|
@ -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,8 +156,6 @@ 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();
|
||||
@ -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 = (/(?:^| )(?<flag>(?:--[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);
|
||||
|
Loading…
Reference in New Issue
Block a user