const { stripIndents } = require('common-tags'); const { escapeRegex } = require('escape-string-regexp'); const { Observer } = require('../../../interfaces/'); class CommandHandler extends Observer { constructor(client) { super(client, { name: 'commandHandler', priority: 5, guarded: true }); this.client = client; this.hooks = [ ['message', this.handleMessage.bind(this)] ]; } async handleMessage(message) { if(!this.client._built || message.webhookID || message.author.bot || (message.guild && !message.guild.available)) return undefined; if(message.guild && !message.member) { await message.guild.members.fetch(message.author.id); } const content = message.content; const args = content.split(' '); const { command, newArgs } = await this._getCommand(message, args); if(!command) return undefined; message.command = command; return await this.handleCommand(message, newArgs); } async _getCommand(message, [arg1, arg2, ...args]) { const prefix = this.client._options.bot.prefix; //Change this for guild prefix settings. let command = null; let remains = []; if(arg1 && arg1.startsWith(prefix)) { const commandName = arg1.slice(1); command = await this._matchCommand(message, commandName); remains = [arg2, ...args]; } else if(arg1 && arg2 && arg1.startsWith('<@')){ const pattern = new RegExp(`^(<@!?${this.client.user.id}>)`, 'i'); if(arg2 && pattern.test(arg1)) { command = await this._matchCommand(message, arg2); } remains = args } return { command, newArgs: remains }; } async _matchCommand(message, commandName) { const command = this.client.resolver.components(commandName, 'command', true)[0]; if(!command) return null; //Eventually search for custom commands here. return command; } /* Command Handling */ async handleCommand(message, args) { const inhibitor = await this._handleInhibitors(message); if(inhibitor.error) { message.channel.send(`${inhibitor.resolveable} failed to pass.`); return undefined; } const parsedArguments = await this._parseArguments(message, args); } async _handleInhibitors(message) { const inhibitors = this.client.registry.components.filter(c=>c.type === 'inhibitor' && !c.disabled); if(inhibitors.size === 0) return { error: false }; const promises = []; for(const inhibitor of inhibitors.values()) { if(inhibitor.guild && !message.guild) continue; promises.push((async () => { let inhibited = inhibitor.execute(message); if(inhibited instanceof Promise) inhibited = await inhibited; return inhibited; })()); } const reasons = (await Promise.all(promises)).filter(p=>p.error); if(reasons.length === 0) return { error: false }; reasons.sort((a, b) => b.inhibitor.priority - a.inhibitor.priority); return reasons[0]; } async _parseArguments(message, args = []) { const command = message.command; let parsedArguments = []; const parsedFlags = {}; const { shortFlags, longFlags, keys } = await this._createFlags(command.arguments); // -prune 50 -c #test // -prune 50 -b -c #test let currentArgument = null; for(const word of args) { const [one,two,...chars] = word.split(''); if(one === '-' && two !== '-') { //short flag maybe? const name = [ two, ...chars ].join(''); if(currentArgument) { if(currentArgument.required) { //Cannot use another flag after a required flag. //Handle requirement error for currentArgument. } } if(!keys.includes(name)) continue; currentArgument = shortFlags[name]; } else if(one === '-' && two === '-') { //long flag maybe? if(currentArgument.required) { //Cannot use another flag after a required flag. //Handle requirement error for currentArgument. } const name = chars.join(''); if(!keys.includes(name)) continue; currentArgument = longFlags[name]; } else { const regex = new RegExp('([0-9]*)([a-zA-Z]+)([0-9]*)', 'g'); let match = regex.exec(word); if(currentArgument && !match) { await this._handleTypeParsing(currentArgument, word); if(!currentArgument.infinite) currentArgument = null; } if(!keys[match[2]]) { //do sometihng there } currentArgument = longFlags[match[2]]; const value = match[1] || match[3]; } } } async _handleTypeParsing(argument, string) { const { error, value } = await this.constructor.parseType(argument, string); //Cannot access static functions through "this". if(error) { //Failed to parse correctly. prompt.invalid } if(['INTEGER', 'FLOAT'].includes(argument.type)) { const { min, max } = argument; if(value > max) { //Failed to parse correctly. prompt.invalid } if(value < min) { //Failed to parse correctly. prompt.invalid } } } async _createFlags(args) { let shortFlags = {}; let longFlags = {}; let keys = []; for(const arg of args) { let letters = []; let names = [ arg.name, ...arg.aliases ]; keys = [...keys, ...names]; for(const name of names) { longFlags[name] = arg; if(!arg.types.includes('FLAG')) continue; let letter = name.slice(0, 1); if(letters.includes(letter)) continue; if(keys.includes(letter)) letter = letter.toUpperCase(); if(keys.includes(letter)) break; keys.push(letter); letters.push(letter); shortFlags[letter] = arg; } } return { shortFlags, longFlags, keys }; } static async parseType(type, str) { //this is in the class for a reason, will soon reference to a user resolver etc. //INTEGER AND FLOAT ARE SAME FUNCTION const types = { STRING: (str) => { return { error: false, value: `${str}` }; }, INTEGER: (str) => { const int = parseInt(str); if(Number.isNaN(int)) return { error: true } return { error: false, value: int }; }, FLOAT: (str) => { const float = parseInt(str); if(Number.isNaN(float)) return { error: true } return { error: false, value: float }; }, BOOLEAN: (str) => { const truthy = ['yes', 'y', 'true', 't', 'on', 'enable']; const falsey = ['no', 'n', 'false', 'f', 'off', 'disable']; if(typeof str === 'boolean') return { error: false, value: str }; if(typeof str === 'string') str = str.toLowerCase(); if(truthy.includes(str)) return { error: false, value: true }; if(falsey.includes(str)) return { error: false, value: false }; return { error: true }; } } return await types[type](str); } } module.exports = CommandHandler;