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) { //const time1 = new Date().getTime(); 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; await this.handleCommand(message, newArgs); //const time2 = new Date().getTime(); //console.log(`${time2-time1}ms`); } 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); const regex = new RegExp(`([0-9]*)(${Object.keys(longFlags).map(k=>escapeRegex(k)).join('|')})([0-9]*)`, 'i'); console.log(regex); let currentArgument = null; let params = []; for(let i=0; i 0 && ['INTEGER', 'FLOAT'].includes(currentArgument.type)) { //15 pts console.log("asgsaiughasiguassag") const lastItem = params[params.length-1]; const beforeError = await this._handleTypeParsing(currentArgument, lastItem); if(beforeError) { continue; } else { params.pop(); currentArgument.value = lastItem; parsedArguments.push(currentArgument); currentArgument = null; continue; } } const value = match[1] || match[3]; const error = await this._handleTypeParsing(currentArgument, value); if(value) { if(error) { if(currentArgument.required) { console.error(`Argument ${currentArgument.name} is required and failed to meet requirements.`); return undefined; } else { parsedArguments.push(currentArgument); currentArgument = null; continue; } } else { currentArgument.value = value; parsedArguments.push(currentArgument); currentArgument = null; continue; } } else { continue; } } else { if(currentArgument) { const error = await this._handleTypeParsing(currentArgument, word); if(error) { if(currentArgument.default) { params.push(word); currentArgument.value = currentArgument.default; parsedArguments.push(currentArgument); currentArgument = null; continue; } if(currentArgument.required) { if(currentArgument.infinite) { if(currentArgument.value.length === 0) { console.error(`1 Argument ${currentArgument.name} is required and failed to meet requirements.`); return undefined; } else { parsedArguments.push(currentArgument); currentArgument = null; params.push(word); continue; } } else { console.error(`2 Argument ${currentArgument.name} is required and failed to meet requirements.`); return undefined; } } else { currentArgument = null; params.push(word); continue; } } else { if(currentArgument.infinite) continue; parsedArguments.push(currentArgument); currentArgument = null; continue; } } else { params.push(word); continue; } } } } await message.channel.send(stripIndents`**arguments:** ${parsedArguments.map(a=>`${a.name}: ${a.value}`).join(' | ')} **words:** ${params.join(', ')}`); } async _handleTypeParsing(argument, string) { const parse = async (argument, string) => { const { error, value } = await this.constructor.parseType(argument.type, string); //Cannot access static functions through "this". if(error) return { error: true }; if(['INTEGER', 'FLOAT'].includes(argument.type)) { const { min, max } = argument; if(value > max && max !== null) { return { error: true }; } if(value < min && min !== null) { return { error: true }; } } return { error: false, value }; } const { error, value } = await parse(argument, string); if(!error) { argument.infinite ? argument.value.push(value) : argument.value = value; } return error; } 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;