diff --git a/bot/engine.js b/bot/engine.js index f4a6958..195bbf8 100644 --- a/bot/engine.js +++ b/bot/engine.js @@ -5,6 +5,7 @@ let { getShortestPrefix } = require('./utility'); let help = require('./module/help'); let message = require('./message'); let utility = require('./utility'); +let { initModule } = require('./module/abstract'); let sentinelValue = '!'; @@ -20,20 +21,28 @@ class Engine { } initModules() { - this.modules.forEach((module) => { - logger.info("Loading module: %s", module.name); - this.moduleMap.set(module.command, module); - this.commandMap.set(module.command, module); - this.commandRadixTree.addWord(module.command); + this.modules.forEach((mod) => { + logger.info("Loading module: %s", mod.name); + initModule(mod); + console.log("Recognized commands: %s", mod.getRecognizedCommands()) + this.moduleMap.set(mod.command, mod); + this.commandMap.set(mod.command, mod); + this.commandRadixTree.addWord(mod.command); }); - this.modules.forEach((module) => { - let shortCommand = getShortestPrefix(this.commandRadixTree, module.command, 3); - logger.info("Adding short command %s for module: %s", shortCommand, module.name); - this.commandMap.set(shortCommand, module); + this.modules.forEach((mod) => { + let shortCharCommand = getShortestPrefix(this.commandRadixTree, mod.command, 1); + let short3CharCommand = getShortestPrefix(this.commandRadixTree, mod.command, 3); + let shortCommandAliases = [shortCharCommand, short3CharCommand]; + logger.info("Adding short command %s for module: %s", shortCommandAliases, mod.name); + shortCommandAliases.forEach( (commandAlias) => { + this.commandMap.set(commandAlias, mod); + }) }); this.helpModule = help.create(this.moduleMap) + initModule(this.helpModule); + this.moduleMap.set(this.helpModule.command, this.helpModule); this.commandMap.set('help', this.helpModule); this.commands = Array.from(this.commandMap.keys()).sort() diff --git a/bot/logging.js b/bot/logging.js index ca3eea6..06e43ac 100644 --- a/bot/logging.js +++ b/bot/logging.js @@ -22,7 +22,7 @@ if (process.env.NODE_ENV !== 'production') { })); } -if ('LOG_LEVE' in process.env) { +if ('LOG_LEVEL' in process.env) { logger.info('LOG_LEVEL:', process.env.LOG_LEVEL) logger.level = process.env.LOG_LEVEL } diff --git a/bot/module/abstract.js b/bot/module/abstract.js index e98fba4..afb3187 100644 --- a/bot/module/abstract.js +++ b/bot/module/abstract.js @@ -4,30 +4,132 @@ let { logger } = require('../logging'); let message = require('../message'); +let { isFunction, getObjectKeysToPrototype } = require('../utility'); class AbstractModule { - name = "AbstractModule" - description = "Base Module That All Other Modules Extend" - command = "abstract_module" + /* + Name of the module used in help documentation and logging. + */ + name = "AbstractModule"; + + /* + Short description of the module functionality. + */ + description = "Base Module That All Other Modules Extend"; + + /* + The exported command used to invoke the module directly. + */ + command = "abstract_module"; + + /* + The default method to call when a command word is not recognized. + */ + defaultCommand = null; + /* + The module should be hidden from help and command dialogs. + */ + hidden = false; + /* + This module should receive all messages, regardless of whether + the module was directly referenced with a command. + */ + canHandleIndirectMessages = false; constructor(name, description, command) { this.name = name; this.description = description; this.command = command; + this._recognizedCommands = new Map(); + } + + addRecognizedCommand(command, methodName) { + this._recognizedCommands.set(command, methodName) + } + + getRecognizedCommands() { + return this._recognizedCommands.keys(); } + /** + * Default functionality for receiving and processing a message. + * + * Override this if the module needs to do more complicated message processing. + */ handleMessage(event, room, callback) { logger.debug("[%s] [%s] [%s]", this.name, room.name, event.event.content.body); + + let messageBody = event.event.content.body; + let bodyParts = messageBody.split(' '); + let trigger = bodyParts[0]; + let command = bodyParts[1]; + var args = []; + if (bodyParts.length > 2) { + args = bodyParts.slice(2); + } + + logger.debug("Attempting to call %s with %s", command, args); + let responseMessage = this.processMessage(command, args); + callback( room, - message.createBasic(this.name + " processed the message") + responseMessage ); } - help(event, room) { + /* + Call the command method with the args + */ + processMessage(command, ...args) { + if (command in this._recognizedCommands) { + logger.debug("Calling %s with %s", this._recognizedCommands.get(command), args); + return this[this._recognizedCommands.get(command)](...args); + } else { + if (this.defaultCommand != null) { + logger.debug("Attempting to use default command %s", this.defaultCommand); + try { + let newArgs = [command].concat(...args); + logger.debug("Calling %s with %s", this._recognizedCommands.get(this.defaultCommand), newArgs); + return this[this._recognizedCommands.get(this.defaultCommand)](...newArgs); + } catch (e) { + logger.error("Error while calling default command %s %s", this.defaultCommand, e); + return this.cmd_help(); + } + } else { + logger.debug("Unrecognized command %s", command); + return this.cmd_help(); + } + } + } + + /* Basic cmd methods */ + + /* + return basic help information,. + */ + cmd_help(...args) { return message.createBasic(this.name + " HELP!"); } +} + +let abstractModulePrototype = Object.getPrototypeOf(new AbstractModule('', '', '')); +/* +Initialization of a module. +*/ +function init(mod) { + logger.debug("Initializing module %s", mod.name) + let commandMethods = getObjectKeysToPrototype(mod, abstractModulePrototype, (key) => { + return key.startsWith('cmd_') && isFunction(mod[key]); + }) + // let commandMethods = objectKeys.filter(); + logger.debug("Identified command methods: %s", commandMethods); + commandMethods.forEach((commandMethodName) => { + let command = commandMethodName.substring(4); + mod.addRecognizedCommand(command, commandMethodName); + }) + logger.debug("Bound command methods for %s as %s", mod.name, mod.getRecognizedCommands()); } -exports.AbstractModule = AbstractModule \ No newline at end of file +exports.AbstractModule = AbstractModule +exports.initModule = init; \ No newline at end of file diff --git a/bot/module/help.js b/bot/module/help.js index 9816ff4..5685b84 100644 --- a/bot/module/help.js +++ b/bot/module/help.js @@ -12,20 +12,34 @@ class HelpModule extends AbstractModule { "Provide helpful information about other modules.", "help" ); - this.commandMap = commandMap; + this._commandMap = commandMap; + this._commandList = Array.from(commandMap.keys()).sort(); + this.defaultCommand = 'help'; } - addCommand(command, module) { - logger.debug("Adding help for command %s against module %s", command, module.name); - this.commands.push(command); + _default_help_message() { + let help = `!help `; + for (let command of this._commandList) { + help += "\n!help " + command + " : " + this._commandMap.get(command).description; + } + return help; } - help() { - let help = `!help `; - for (let command of Array.from(this.commandMap.keys()).sort()) { - help += "\n!help " + command + " : " + this.commandMap.get(command).description; + cmd_help(...args) { + logger.debug("%o", args) + if (args.length < 1) { + return this._default_help_message(); + } else { + let command = args[0]; + logger.debug("Looking up help for %s from %o", command, this._commandMap); + if (this._commandList.includes(command)) { + return this._commandMap.get(command).cmd_help(); + } else { + let help = command + " is an unrecognized module\n"; + help += this._default_help_message(); + return help; + } } - return help; } } @@ -33,5 +47,4 @@ function create(commandMap) { return new HelpModule(commandMap); } -exports.create = create; -exports.module = new HelpModule(); \ No newline at end of file +exports.create = create; \ No newline at end of file diff --git a/bot/utility.js b/bot/utility.js index 67ddeb9..049dfd2 100644 --- a/bot/utility.js +++ b/bot/utility.js @@ -45,11 +45,41 @@ function sleep(ms) { } function isString(s) { - return typeof(s) === 'string' || s instanceof String; + return typeof (s) === 'string' || s instanceof String; +} + +function isFunction(f) { + return f && {}.toString.call(f) === '[object Function]'; +} + +/** + * Parse the prototype tree to return all accessible properties till + * reaching a sentinelPrototype. + * + * Optionally provide a filtering function to return only the names that match. + * + * @param {*} initialObj The starting object to derive the from + * @param {*} sentinelPrototype The prototype that represents the end of the line + * @param {*} filterFunc A fioltering function for the return names + */ +function getObjectKeysToPrototype(initialObj, sentinelPrototype, filterFunc = (e) => true) { + let prototypeChain = [] + var targetPrototype = initialObj; + while (Object.getPrototypeOf(targetPrototype) && targetPrototype !== sentinelPrototype) { + targetPrototype = Object.getPrototypeOf(targetPrototype); + prototypeChain.push(targetPrototype); + } + // console.log("Prototype chain: %s", prototypeChain); + let completePropertyNames = prototypeChain.map((obj) => { + return Object.getOwnPropertyNames(obj); + }) + return [Object.getOwnPropertyNames(initialObj)].concat.apply([], completePropertyNames).filter(filterFunc); } exports.getShortestPrefix = getShortestPrefix; exports.toISODateString = toISODateString; exports.getBuildInfo = getBuildInfo; exports.sleep = sleep; -exports.isString = isString; \ No newline at end of file +exports.isString = isString; +exports.isFunction = isFunction; +exports.getObjectKeysToPrototype = getObjectKeysToPrototype; \ No newline at end of file