From 41b23ccdef11d334fd2bd66264e1b21e7d6bfafb Mon Sep 17 00:00:00 2001 From: "Navy.gif" Date: Fri, 29 Jul 2022 14:16:43 +0300 Subject: [PATCH] buncha stuff --- src/Parser.ts | 106 +++++++++++++++++++------------- src/classes/Command.ts | 7 ++- src/classes/CommandOption.ts | 25 ++++++-- src/interfaces/Command.ts | 6 +- src/interfaces/CommandOption.ts | 18 ++++-- src/interfaces/Resolver.ts | 18 ++++++ 6 files changed, 120 insertions(+), 60 deletions(-) create mode 100644 src/interfaces/Resolver.ts diff --git a/src/Parser.ts b/src/Parser.ts index 396c775..1aa370e 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -1,5 +1,9 @@ +import { EventEmitter } from 'events'; +import CommandOption from './classes/CommandOption'; + 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 Util from "./util/Util"; @@ -9,18 +13,33 @@ type ParseResult = { subcommandGroup: string | null } +type ParserOptions = { + commands: Iterable, + prefix?: string, + debug?: boolean, + resolver: IResolver +} + const flagReg = /(?:^| )(?(?:--[a-z0-9]{3,})|(?:-[a-z]{1,2}))(?:$| )/iu; -class Parser { +class Parser extends EventEmitter { private commands: ExtendedMap; + private _debug = false; + prefix: string; - constructor(commands: Iterable, prefix?: string) { + resolver: IResolver; + constructor({ commands, prefix, debug = false, resolver }: ParserOptions) { + + super(); + + this._debug = debug; this.commands = new ExtendedMap(); for (const command of commands) this.commands.set(command.name, command); + this.resolver = resolver; this.prefix = prefix || ''; @@ -33,26 +52,26 @@ class Parser { return command || null; } - async parseMessage(message: string) { + async parseMessage(message: string, prefix = this.prefix) { - if (!message.startsWith(this.prefix) || !message.length) return null; - let params: string[] = message.replace(this.prefix, '').split(' ').filter((str) => str.length); + if (!message.startsWith(prefix) || !message.length) return null; + let params: string[] = message.replace(prefix, '').split(' ').filter((str) => str.length); const [commandName] = params.shift() || []; if (!commandName) return null; const command = this.matchCommand(commandName); if (!command) return null; - // const { command, target: message, guild } = invoker; + this.debug(`Matched command ${command.name}`); const { subcommands, subcommandGroups } = command; - const args: {[key: string]: ICommandOption} = {}; + const args: {[key: string]: CommandOption} = {}; const parseResult: ParseResult = { args, subcommand: null, subcommandGroup: null }; - // console.log(options); let group = null, subcommand = null; // Parse out subcommands if (subcommandGroups.length || subcommands.length) { + this.debug(`Expecting subcommand`); const [first, second, ...rest] = params; group = command.subcommandGroup(first); @@ -69,25 +88,25 @@ class Parser { parseResult.subcommand = subcommand?.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 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; - // console.log('params', params); // Parse flags for (let index = 0; index < params.length;) { - // console.log(params[index]); const match = flagReg.exec(params[index]); 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 + this.debug(`Appending value to ${currentFlag.name}: ${params[index]}`); if (currentFlag.plural) { // The parse function only parses consecutive values if (!currentFlag.rawValue) currentFlag.rawValue = []; currentFlag.rawValue.push(params[index]); @@ -100,16 +119,15 @@ class Parser { continue; } - // console.log('matched'); const _flag = match.groups.flag.replace(/--?/u, '').toLowerCase(); let aliased = false; const flag = flags.find((f) => { aliased = f.valueAsAlias && f.choices.some((c) => c === _flag); 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, ''); if (aliased) { (args[flag.name] = flag.clone([_flag])).aliased = true; @@ -119,42 +137,44 @@ class Parser { args[flag.name] = currentFlag; } index++; - // console.log('------------------------------'); } // Clean up params for option parsing 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(); if (result.error) { 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); } - // 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 choiceOpts = activeCommand.options.filter((opt) => opt.choices.length); 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 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); + if (!params.some((param) => param !== null)) { + this.debug(`No potential values left in params`); + break; + } + + this.debug(`Trying ${option.name}, plural: ${option.plural}`); + const cloned = option.clone(); let removed: string[] = [], error = false; 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; @@ -164,7 +184,10 @@ class Parser { if (!error) break; 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; // 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 = []; 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 for (let index = 0; index < params.length;) { const str = params[index]; - // console.log(str); if (!str) { - // console.log('null string'); if (tmpString.length) { - // console.log('pushing', tmpString); strings.push(tmpString); tmpString = ''; } @@ -194,24 +214,18 @@ class Parser { tmpString += ` ${str}`; tmpString = tmpString.trim(); } - // console.log('tmpString', tmpString); if (tmpString.length) strings.push(tmpString); - // console.log('params after', params); - // console.log('strings', strings); - if (strings.length) for (const strOpt of stringOpts) { const val = strings.shift(); if (!val) break; const cloned = strOpt.clone([val]); - // console.log(cloned.name, cloned._rawValue); await cloned.parse(); args[cloned.name] = cloned; } // This part is obsolete now, I think, the string option checks the choice value 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' && 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('`, `') } }; } + this.debug(`Making sure required options were given.`); for (const req of activeCommand.options.filter((opt) => opt.required)) if (!args[req.name]) 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('`, `') } }; return { options: args, verbose: true }; } + + private debug(message: string) { + if(this._debug) this.emit('debug', `[PARSER] ${message}`); + } + } export { Parser }; \ No newline at end of file diff --git a/src/classes/Command.ts b/src/classes/Command.ts index 49863bb..77d8ce4 100644 --- a/src/classes/Command.ts +++ b/src/classes/Command.ts @@ -1,7 +1,8 @@ -import { ICommandOption, OptionType } from "../interfaces/CommandOption"; +import { OptionType } from "../interfaces/CommandOption"; import { ICommand, CommandDefinition } from "../interfaces/Command"; import SubcommandOption from "./SubcommandOption"; import SubcommandGroupOption from "./SubcommandGroupOption"; +import CommandOption from "./CommandOption"; class Command implements ICommand { @@ -9,7 +10,7 @@ class Command implements ICommand { aliases: string[]; - options: ICommandOption[]; + options: CommandOption[]; constructor(def: CommandDefinition) { @@ -39,7 +40,7 @@ class Command implements ICommand { return this.subcommandGroups.find((opt) => opt.name === name) || null; } - private _subcommands(options: ICommandOption[]): SubcommandOption[] { + private _subcommands(options: CommandOption[]): SubcommandOption[] { const subcommands: SubcommandOption[] = []; for (const opt of options) { if (opt.type === OptionType.SUB_COMMAND) subcommands.push(opt); diff --git a/src/classes/CommandOption.ts b/src/classes/CommandOption.ts index ccc016d..f3d9bb9 100644 --- a/src/classes/CommandOption.ts +++ b/src/classes/CommandOption.ts @@ -1,12 +1,16 @@ import ICommandOption, { Choice, CommandOptionDefinition, DependsOnMode, OptionType, ParseResult } from "../interfaces/CommandOption"; +import IResolver from "../interfaces/Resolver"; class CommandOption implements ICommandOption { + [key: string]: unknown; + name: string; aliases: string[]; - options: ICommandOption[]; + // eslint-disable-next-line no-use-before-define + options: CommandOption[]; type: OptionType; @@ -36,6 +40,8 @@ class CommandOption implements ICommandOption { aliased = false; + private resolver?: IResolver|undefined = undefined; + constructor(def: CommandOptionDefinition|ICommandOption) { this.name = def.name; @@ -58,20 +64,31 @@ class CommandOption implements ICommandOption { } - clone(rawValue?: string[]): ICommandOption { + clone(rawValue?: string[], resolver?: IResolver): CommandOption { const opt = new CommandOption(this); opt.rawValue = rawValue; + opt.resolver = resolver || undefined; return opt; } - parse(): ParseResult { - return { error: false, removed: [] }; + async parse(): Promise { + 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 { 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 }; diff --git a/src/interfaces/Command.ts b/src/interfaces/Command.ts index 3e877b0..57f43c0 100644 --- a/src/interfaces/Command.ts +++ b/src/interfaces/Command.ts @@ -1,13 +1,13 @@ +import CommandOption from '../classes/CommandOption'; import SubcommandGroupOption from '../classes/SubcommandGroupOption'; import SubcommandOption from '../classes/SubcommandOption'; -import { ICommandOption } from './CommandOption'; interface ICommand { name: string, aliases: string[] - options: ICommandOption[] + options: CommandOption[] get subcommands(): SubcommandOption[] @@ -22,7 +22,7 @@ interface ICommand { type CommandDefinition = { name: string; aliases?: string[]; - options?: ICommandOption[]; + options?: CommandOption[]; } export { ICommand, CommandDefinition }; diff --git a/src/interfaces/CommandOption.ts b/src/interfaces/CommandOption.ts index 1e2dffb..6cb1081 100644 --- a/src/interfaces/CommandOption.ts +++ b/src/interfaces/CommandOption.ts @@ -28,6 +28,9 @@ // | 'FLOAT' // | 'POINTS' +import CommandOption from "../classes/CommandOption"; +import IResolver from "./Resolver"; + enum OptionType { SUB_COMMAND, SUB_COMMAND_GROUP, @@ -53,10 +56,8 @@ enum OptionType { VOICE_CHANNEL, CHANNEL, ROLE, - MENTIONABLE, NUMBER, FLOAT, - POINTS } type Choice = string | number | boolean @@ -71,7 +72,7 @@ type CommandOptionDefinition = { name: string; aliases?: string[]; // eslint-disable-next-line no-use-before-define - options?: ICommandOption[]; + options?: CommandOption[]; type?: OptionType; required?: boolean; @@ -89,13 +90,16 @@ type CommandOptionDefinition = { }; interface ICommandOption { + + // Allows for accessing the class properties with string indices, e.g. this['string'] + [key: string]: unknown; // Option name name: string; // Optional alises aliases: string[]; // Sub options - options: ICommandOption[]; + options: CommandOption[]; choices: Choice[] type: OptionType; @@ -117,11 +121,13 @@ interface ICommandOption { rawValue?: string[] aliased: boolean + // resolver?: IResolver|undefined + // private _options?: CommandOptionDefinition - clone(rawValue?: string[]): ICommandOption + clone(rawValue?: string[], resolver?: IResolver): CommandOption - parse(): ParseResult + parse(): Promise get plural(): boolean diff --git a/src/interfaces/Resolver.ts b/src/interfaces/Resolver.ts new file mode 100644 index 0000000..87728c1 --- /dev/null +++ b/src/interfaces/Resolver.ts @@ -0,0 +1,18 @@ +interface IResolver { + + 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; \ No newline at end of file