buncha stuff

This commit is contained in:
Erik 2022-07-29 14:16:43 +03:00
parent a0e89367dd
commit 41b23ccdef
Signed by: Navy.gif
GPG Key ID: 811EC0CD80E7E5FB
6 changed files with 120 additions and 60 deletions

View File

@ -1,5 +1,9 @@
import { EventEmitter } from 'events';
import CommandOption from './classes/CommandOption';
import ICommand from "./interfaces/Command"; import ICommand from "./interfaces/Command";
import ICommandOption, { OptionType } from "./interfaces/CommandOption"; import { OptionType } from "./interfaces/CommandOption";
import IResolver from './interfaces/Resolver';
import ExtendedMap from "./util/Map"; import ExtendedMap from "./util/Map";
import Util from "./util/Util"; import Util from "./util/Util";
@ -9,18 +13,33 @@ type ParseResult = {
subcommandGroup: string | null subcommandGroup: string | null
} }
type ParserOptions = {
commands: Iterable<ICommand>,
prefix?: string,
debug?: boolean,
resolver: IResolver<unknown, unknown, unknown, unknown>
}
const flagReg = /(?:^| )(?<flag>(?:--[a-z0-9]{3,})|(?:-[a-z]{1,2}))(?:$| )/iu; const flagReg = /(?:^| )(?<flag>(?:--[a-z0-9]{3,})|(?:-[a-z]{1,2}))(?:$| )/iu;
class Parser { class Parser extends EventEmitter {
private commands: ExtendedMap<string, ICommand>; private commands: ExtendedMap<string, ICommand>;
private _debug = false;
prefix: string; prefix: string;
constructor(commands: Iterable<ICommand>, prefix?: string) { resolver: IResolver<unknown, unknown, unknown, unknown>;
constructor({ commands, prefix, debug = false, resolver }: ParserOptions) {
super();
this._debug = debug;
this.commands = new ExtendedMap<string, ICommand>(); this.commands = new ExtendedMap<string, ICommand>();
for (const command of commands) this.commands.set(command.name, command); for (const command of commands) this.commands.set(command.name, command);
this.resolver = resolver;
this.prefix = prefix || ''; this.prefix = prefix || '';
@ -33,26 +52,26 @@ class Parser {
return command || null; return command || null;
} }
async parseMessage(message: string) { async parseMessage(message: string, prefix = this.prefix) {
if (!message.startsWith(this.prefix) || !message.length) return null; if (!message.startsWith(prefix) || !message.length) return null;
let params: string[] = message.replace(this.prefix, '').split(' ').filter((str) => str.length); let params: string[] = message.replace(prefix, '').split(' ').filter((str) => str.length);
const [commandName] = params.shift() || []; const [commandName] = params.shift() || [];
if (!commandName) return null; if (!commandName) return null;
const command = this.matchCommand(commandName); const command = this.matchCommand(commandName);
if (!command) return null; if (!command) return null;
// const { command, target: message, guild } = invoker; this.debug(`Matched command ${command.name}`);
const { subcommands, subcommandGroups } = command; const { subcommands, subcommandGroups } = command;
const args: {[key: string]: ICommandOption} = {}; const args: {[key: string]: CommandOption} = {};
const parseResult: ParseResult = { args, subcommand: null, subcommandGroup: null }; const parseResult: ParseResult = { args, subcommand: null, subcommandGroup: null };
// console.log(options);
let group = null, let group = null,
subcommand = null; subcommand = null;
// Parse out subcommands // Parse out subcommands
if (subcommandGroups.length || subcommands.length) { if (subcommandGroups.length || subcommands.length) {
this.debug(`Expecting subcommand`);
const [first, second, ...rest] = params; const [first, second, ...rest] = params;
group = command.subcommandGroup(first); group = command.subcommandGroup(first);
@ -69,25 +88,25 @@ class Parser {
parseResult.subcommand = subcommand?.name || null; parseResult.subcommand = subcommand?.name || null;
parseResult.subcommandGroup = group?.name || null; parseResult.subcommandGroup = group?.name || null;
if (!subcommand) return { showUsage: true, verbose: true }; if (!subcommand) throw new Error(`Expecting subcommand, one of ${subcommands.map((s) => s.name)}`); //return { showUsage: true, verbose: true };
this.debug(`Got ${subcommand.name}`);
} }
const activeCommand = subcommand || command; const activeCommand = subcommand || command;
const flags = activeCommand.options.filter((opt) => opt.flag); const flags = activeCommand.options.filter((opt) => opt.flag);
params = Util.parseQuotes(params.join(' ')).map(([str]: (string|boolean)[]): string => str.toString()); params = Util.parseQuotes(params.join(' ')).map(([str]: (string | boolean)[]): string => str.toString());
this.debug(`Given params: "${params.join('", "')}"`);
let currentFlag = null; let currentFlag = null;
// console.log('params', params);
// Parse flags // Parse flags
for (let index = 0; index < params.length;) { for (let index = 0; index < params.length;) {
// console.log(params[index]);
const match = flagReg.exec(params[index]); const match = flagReg.exec(params[index]);
if (!match || !match.groups) { if (!match || !match.groups) {
// 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) { // Add potential value resolveables to the flag's raw value until next flag is hit, if there is one
this.debug(`Appending value to ${currentFlag.name}: ${params[index]}`);
if (currentFlag.plural) { // The parse function only parses consecutive values if (currentFlag.plural) { // The parse function only parses consecutive values
if (!currentFlag.rawValue) currentFlag.rawValue = []; if (!currentFlag.rawValue) currentFlag.rawValue = [];
currentFlag.rawValue.push(params[index]); currentFlag.rawValue.push(params[index]);
@ -100,16 +119,15 @@ class Parser {
continue; continue;
} }
// console.log('matched');
const _flag = match.groups.flag.replace(/--?/u, '').toLowerCase(); const _flag = match.groups.flag.replace(/--?/u, '').toLowerCase();
let aliased = false; let aliased = false;
const flag = flags.find((f) => { const flag = flags.find((f) => {
aliased = f.valueAsAlias && f.choices.some((c) => c === _flag); aliased = f.valueAsAlias && f.choices.some((c) => c === _flag);
return f.name === _flag || aliased; return f.name === _flag || aliased;
}); });
if (!flag) throw new Error(`Unrecognised flag: ${_flag}`); //{ error: true, index: 'O_COMMANDHANDLER_UNRECOGNISED_FLAG', params: { flag: _flag } }; if (!flag) throw new Error(`Unrecognised flag: ${_flag}`);
this.debug(`Matched flag: ${flag.name} with ${_flag}`);
// console.log('aliased', aliased);
params.splice(index, 1, ''); params.splice(index, 1, '');
if (aliased) { if (aliased) {
(args[flag.name] = flag.clone([_flag])).aliased = true; (args[flag.name] = flag.clone([_flag])).aliased = true;
@ -119,42 +137,44 @@ class Parser {
args[flag.name] = currentFlag; args[flag.name] = currentFlag;
} }
index++; index++;
// console.log('------------------------------');
} }
// Clean up params for option parsing // Clean up params for option parsing
for (const flag of Object.values(args)) { for (const flag of Object.values(args)) {
// console.log('flags loop', flag.name, flag._rawValue); this.debug(`Running parser for ${flag.name}`);
const result = await flag.parse(); const result = await flag.parse();
if (result.error) { if (result.error) {
if (flag.choices.length) { 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).join('`, `') } }; throw new Error(`Invalid choice for ${flag.name}, Valid choices are ${flag.choices.join(', ')}.`);
} }
return { option: flag, ...result.removed }; throw new Error(`Failed to parse value for ${flag.name}, expected value type: ${flag.type}`); //return { option: flag, ...result.removed };
} }
this.debug(`Cleaning up params after ${flag.name}`);
for (const r of result.removed) params.splice(params.indexOf(r), 1); for (const r of result.removed) params.splice(params.indexOf(r), 1);
} }
// console.log('params', params); this.debug(`Params after parsing "${params.join('", "')}"`);
const options = activeCommand.options.filter((opt) => !opt.flag && (opt.type !== OptionType.STRING || opt.choices.length)); const options = activeCommand.options.filter((opt) => !opt.flag && (opt.type !== OptionType.STRING || opt.choices.length));
// const choiceOpts = activeCommand.options.filter((opt) => opt.choices.length);
const stringOpts = activeCommand.options.filter((opt) => !opt.flag && !opt.choices.length && opt.type === OptionType.STRING); const stringOpts = activeCommand.options.filter((opt) => !opt.flag && !opt.choices.length && opt.type === OptionType.STRING);
// console.log('non-flag options', options.map((opt) => opt.name));
this.debug(`Parsing non-flag options`);
// Parse out non-flag options // Parse out non-flag options
for (const option of options) { // String options are parsed separately at the end for (const option of options) { // String options are parsed separately at the end
// console.log(1, params); if (!params.some((param) => param !== null)) {
if (!params.some((param) => param !== null)) break; this.debug(`No potential values left in params`);
// console.log(2); break;
}
this.debug(`Trying ${option.name}, plural: ${option.plural}`);
const cloned = option.clone(); const cloned = option.clone();
let removed: string[] = [], let removed: string[] = [],
error = false; error = false;
if (cloned.plural) { // E.g. if the type is CHANNEL**S**, parse out any potential channels from the message 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; cloned.rawValue = params;
({ removed } = await cloned.parse()); ({ removed } = await cloned.parse());
} else for (let index = 0; index < params.length;) { // Attempt to parse out a value from each param } else for (let index = 0; index < params.length;) { // Attempt to parse out a value from each param
// console.log('singular');
if (params[index] === null) { if (params[index] === null) {
index++; index++;
continue; continue;
@ -164,7 +184,10 @@ class Parser {
if (!error) break; if (!error) break;
index++; index++;
} }
if (error) continue; if (error) {
this.debug(`Failed to parse any values for ${option.name} with params "${option.rawValue?.join('", "')}"`);
continue;
}
args[cloned.name] = cloned; args[cloned.name] = cloned;
// Clean up params for string parsing // Clean up params for string parsing
@ -172,18 +195,15 @@ class Parser {
} }
this.debug(`Going through remaining params for string values: "${params.join('", "')}"`);
const strings = []; const strings = [];
let tmpString = ''; 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 // 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;) { for (let index = 0; index < params.length;) {
const str = params[index]; const str = params[index];
// console.log(str);
if (!str) { if (!str) {
// console.log('null string');
if (tmpString.length) { if (tmpString.length) {
// console.log('pushing', tmpString);
strings.push(tmpString); strings.push(tmpString);
tmpString = ''; tmpString = '';
} }
@ -194,24 +214,18 @@ class Parser {
tmpString += ` ${str}`; tmpString += ` ${str}`;
tmpString = tmpString.trim(); tmpString = tmpString.trim();
} }
// console.log('tmpString', tmpString);
if (tmpString.length) strings.push(tmpString); if (tmpString.length) strings.push(tmpString);
// console.log('params after', params);
// console.log('strings', strings);
if (strings.length) for (const strOpt of stringOpts) { if (strings.length) for (const strOpt of stringOpts) {
const val = strings.shift(); const val = strings.shift();
if (!val) break; if (!val) break;
const cloned = strOpt.clone([val]); const cloned = strOpt.clone([val]);
// console.log(cloned.name, cloned._rawValue);
await cloned.parse(); await cloned.parse();
args[cloned.name] = cloned; args[cloned.name] = cloned;
} }
// This part is obsolete now, I think, the string option checks the choice value // This part is obsolete now, I think, the string option checks the choice value
for (const arg of Object.values(args)) { for (const arg of Object.values(args)) {
// console.log(arg.name, arg.value);
if (!arg.choices.length) continue; if (!arg.choices.length) continue;
if (!arg.choices.some((choice) => { if (!arg.choices.some((choice) => {
if (typeof arg.value === 'string' && typeof choice === 'string') return arg.value.toLowerCase() === choice.toLowerCase(); if (typeof arg.value === 'string' && typeof choice === 'string') return arg.value.toLowerCase() === choice.toLowerCase();
@ -219,17 +233,21 @@ class Parser {
})) throw new Error(`Invalid choice: ${arg.name} value must be one of ${arg.choices.join(', ')}`); //return { error: true, index: 'O_COMMANDHANDLER_INVALID_CHOICE', params: { option: arg.name, value: arg.value, choices: arg.choices.map((c) => c).join('`, `') } }; })) throw new Error(`Invalid choice: ${arg.name} value must be one of ${arg.choices.join(', ')}`); //return { error: true, index: 'O_COMMANDHANDLER_INVALID_CHOICE', params: { option: arg.name, value: arg.value, choices: arg.choices.map((c) => c).join('`, `') } };
} }
this.debug(`Making sure required options were given.`);
for (const req of activeCommand.options.filter((opt) => opt.required)) for (const req of activeCommand.options.filter((opt) => opt.required))
if (!args[req.name]) if (!args[req.name])
throw new Error(`${req.name} is a required option`); throw new Error(`${req.name} is a required option`);
//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) throw new Error(`Unrecognised option(s): "${strings.join('", "')}"`);//return { error: true, index: 'O_COMMANDHANDLER_UNRECOGNISED_OPTIONS', params: { opts: strings.join('`, `') } }; if (strings.length) throw new Error(`Unrecognised option(s): "${strings.join('", "')}"`);//return { error: true, index: 'O_COMMANDHANDLER_UNRECOGNISED_OPTIONS', params: { opts: strings.join('`, `') } };
return { options: args, verbose: true }; return { options: args, verbose: true };
} }
private debug(message: string) {
if(this._debug) this.emit('debug', `[PARSER] ${message}`);
}
} }
export { Parser }; export { Parser };

View File

@ -1,7 +1,8 @@
import { ICommandOption, OptionType } from "../interfaces/CommandOption"; import { OptionType } from "../interfaces/CommandOption";
import { ICommand, CommandDefinition } from "../interfaces/Command"; import { ICommand, CommandDefinition } from "../interfaces/Command";
import SubcommandOption from "./SubcommandOption"; import SubcommandOption from "./SubcommandOption";
import SubcommandGroupOption from "./SubcommandGroupOption"; import SubcommandGroupOption from "./SubcommandGroupOption";
import CommandOption from "./CommandOption";
class Command implements ICommand { class Command implements ICommand {
@ -9,7 +10,7 @@ class Command implements ICommand {
aliases: string[]; aliases: string[];
options: ICommandOption[]; options: CommandOption[];
constructor(def: CommandDefinition) { constructor(def: CommandDefinition) {
@ -39,7 +40,7 @@ class Command implements ICommand {
return this.subcommandGroups.find((opt) => opt.name === name) || null; return this.subcommandGroups.find((opt) => opt.name === name) || null;
} }
private _subcommands(options: ICommandOption[]): SubcommandOption[] { private _subcommands(options: CommandOption[]): SubcommandOption[] {
const subcommands: SubcommandOption[] = []; const subcommands: SubcommandOption[] = [];
for (const opt of options) { for (const opt of options) {
if (opt.type === OptionType.SUB_COMMAND) subcommands.push(opt); if (opt.type === OptionType.SUB_COMMAND) subcommands.push(opt);

View File

@ -1,12 +1,16 @@
import ICommandOption, { Choice, CommandOptionDefinition, DependsOnMode, OptionType, ParseResult } from "../interfaces/CommandOption"; import ICommandOption, { Choice, CommandOptionDefinition, DependsOnMode, OptionType, ParseResult } from "../interfaces/CommandOption";
import IResolver from "../interfaces/Resolver";
class CommandOption implements ICommandOption { class CommandOption implements ICommandOption {
[key: string]: unknown;
name: string; name: string;
aliases: string[]; aliases: string[];
options: ICommandOption[]; // eslint-disable-next-line no-use-before-define
options: CommandOption[];
type: OptionType; type: OptionType;
@ -36,6 +40,8 @@ class CommandOption implements ICommandOption {
aliased = false; aliased = false;
private resolver?: IResolver<unknown, unknown, unknown, unknown>|undefined = undefined;
constructor(def: CommandOptionDefinition|ICommandOption) { constructor(def: CommandOptionDefinition|ICommandOption) {
this.name = def.name; this.name = def.name;
@ -58,20 +64,31 @@ class CommandOption implements ICommandOption {
} }
clone(rawValue?: string[]): ICommandOption { clone(rawValue?: string[], resolver?: IResolver<unknown, unknown, unknown, unknown>): CommandOption {
const opt = new CommandOption(this); const opt = new CommandOption(this);
opt.rawValue = rawValue; opt.rawValue = rawValue;
opt.resolver = resolver || undefined;
return opt; return opt;
} }
parse(): ParseResult { async parse(): Promise<ParseResult> {
return { error: false, removed: [] }; if(!this[OptionType[this.type]]) throw new Error(`Missing parsing function for ${this.type}`);
const result = await this[OptionType[this.type]];
return result as ParseResult;
} }
get plural(): boolean { get plural(): boolean {
return this.type.toString().endsWith('S'); return this.type.toString().endsWith('S');
} }
protected async MEMBER() {
if (!this.resolver) throw new Error('Missing resolver');
if(!this.rawValue) throw new Error('Missing raw value');
const member = await this.resolver?.resolveMember(this.rawValue[0]);
if (!member) return { error: true };
return { value: member, removed: this.rawValue };
}
} }
export { CommandOption }; export { CommandOption };

View File

@ -1,13 +1,13 @@
import CommandOption from '../classes/CommandOption';
import SubcommandGroupOption from '../classes/SubcommandGroupOption'; import SubcommandGroupOption from '../classes/SubcommandGroupOption';
import SubcommandOption from '../classes/SubcommandOption'; import SubcommandOption from '../classes/SubcommandOption';
import { ICommandOption } from './CommandOption';
interface ICommand { interface ICommand {
name: string, name: string,
aliases: string[] aliases: string[]
options: ICommandOption[] options: CommandOption[]
get subcommands(): SubcommandOption[] get subcommands(): SubcommandOption[]
@ -22,7 +22,7 @@ interface ICommand {
type CommandDefinition = { type CommandDefinition = {
name: string; name: string;
aliases?: string[]; aliases?: string[];
options?: ICommandOption[]; options?: CommandOption[];
} }
export { ICommand, CommandDefinition }; export { ICommand, CommandDefinition };

View File

@ -28,6 +28,9 @@
// | 'FLOAT' // | 'FLOAT'
// | 'POINTS' // | 'POINTS'
import CommandOption from "../classes/CommandOption";
import IResolver from "./Resolver";
enum OptionType { enum OptionType {
SUB_COMMAND, SUB_COMMAND,
SUB_COMMAND_GROUP, SUB_COMMAND_GROUP,
@ -53,10 +56,8 @@ enum OptionType {
VOICE_CHANNEL, VOICE_CHANNEL,
CHANNEL, CHANNEL,
ROLE, ROLE,
MENTIONABLE,
NUMBER, NUMBER,
FLOAT, FLOAT,
POINTS
} }
type Choice = string | number | boolean type Choice = string | number | boolean
@ -71,7 +72,7 @@ type CommandOptionDefinition = {
name: string; name: string;
aliases?: string[]; aliases?: string[];
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
options?: ICommandOption[]; options?: CommandOption[];
type?: OptionType; type?: OptionType;
required?: boolean; required?: boolean;
@ -89,13 +90,16 @@ type CommandOptionDefinition = {
}; };
interface ICommandOption { interface ICommandOption {
// Allows for accessing the class properties with string indices, e.g. this['string']
[key: string]: unknown;
// Option name // Option name
name: string; name: string;
// Optional alises // Optional alises
aliases: string[]; aliases: string[];
// Sub options // Sub options
options: ICommandOption[]; options: CommandOption[];
choices: Choice[] choices: Choice[]
type: OptionType; type: OptionType;
@ -117,11 +121,13 @@ interface ICommandOption {
rawValue?: string[] rawValue?: string[]
aliased: boolean aliased: boolean
// resolver?: IResolver<unknown, unknown, unknown, unknown>|undefined
// private _options?: CommandOptionDefinition // private _options?: CommandOptionDefinition
clone(rawValue?: string[]): ICommandOption clone(rawValue?: string[], resolver?: IResolver<unknown, unknown, unknown, unknown>): CommandOption
parse(): ParseResult parse(): Promise<ParseResult>
get plural(): boolean get plural(): boolean

View File

@ -0,0 +1,18 @@
interface IResolver <User, Member, Channel, Role> {
resolveUser(resolveable: string): User
resolveMember(resolveable: string): Member
resolveChannel(resolveable: string): Channel
resolveRole(resolveable: string): Role
resolveBoolean(resolveable: string): boolean
resolveTime(resolveable: string): number
}
export { IResolver };
export default IResolver;