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.
|
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.
|
@ -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}`.
|
@ -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();
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user