From 827f9eb3ba46ca00f88b5d57b5d50b49cc2c8cec Mon Sep 17 00:00:00 2001 From: Drew Short Date: Mon, 30 Dec 2019 01:47:28 -0600 Subject: [PATCH] Implemented a working Giphy module * Added example required config * Refactored config tools for more generic config loading * Added NODE_PATH to entrypoint.sh * NODE_PATH environment variable is now required to run Baphoment-JS * Added Axios library * Added config handling to AbstractModule and init process * Added basic working Giphy module --- bot/config.js | 25 ++++++++++-------- bot/engine.js | 21 ++++++++------- bot/module/abstract.js | 35 ++++++++++++++++++++++++- bot/module/admin.js | 2 +- bot/module/giphy.js | 29 ++++++++++++++++++++- bot/module/help.js | 6 ++--- data/giphy-config.json.example | 4 +++ entrypoint.sh | 3 +++ index.js | 2 +- package-lock.json | 47 ++++++++++++++++++++++++++++------ package.json | 1 + 11 files changed, 140 insertions(+), 35 deletions(-) create mode 100644 data/giphy-config.json.example diff --git a/bot/config.js b/bot/config.js index 9a00593..ad3e471 100644 --- a/bot/config.js +++ b/bot/config.js @@ -1,22 +1,25 @@ let { logger } = require('./logging'); -var configPath = null; -var config = null; +let loadedConfigs = new Map(); -function sanitizeConfig(config) { +function sanitizeConfig(config, fields=[]) { let clonedConfig = { ...config }; - clonedConfig.accessToken = "******" + fields.forEach((field) => { + clonedConfig[field] = '******' + }) return clonedConfig; } -function getConfig(configFile) { - if (config === null) { - configPath = configFile; - config = require(configFile); - logger.info("Loaded config:"); - logger.debug("%o", sanitizeConfig(config)); +function getConfig(configFile, sanitizedFields=[]) { + if (!loadedConfigs.has(configFile)) { + let config = require(configFile); + logger.info("Loaded config: %s", configFile); + logger.debug("%o", sanitizeConfig(config, sanitizedFields)); + loadedConfigs.set(configFile, config); + return config; + } else { + return loadedConfigs.get(configFile); } - return config; } exports.getConfig = getConfig \ No newline at end of file diff --git a/bot/engine.js b/bot/engine.js index 195bbf8..2d5a3c9 100644 --- a/bot/engine.js +++ b/bot/engine.js @@ -35,7 +35,7 @@ class Engine { 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) => { + shortCommandAliases.forEach((commandAlias) => { this.commandMap.set(commandAlias, mod); }) }); @@ -116,14 +116,17 @@ class Engine { * @param {*} responseMessage */ function sendResponseMessage(bot, room, responseMessage) { - logger.debug("Responding to room: %s", room.roomId); - if (responseMessage instanceof Object) { - bot.client.sendMessage(room.roomId, responseMessage); - } else if (utility.isString(responseMessage)) { - bot.client.sendMessage(room.roomId, message.createBasic(responseMessage)); - } else { - logger.error("Unable to process response message: %s", responseMessage); - } + logger.debug("Responding to room: %s with %o", room.roomId, responseMessage); + Promise.resolve(responseMessage).then((promisedMessage) => { + logger.debug("Sending message: %s", promisedMessage); + if (promisedMessage instanceof Object) { + bot.client.sendMessage(room.roomId, promisedMessage); + } else if (utility.isString(promisedMessage)) { + bot.client.sendMessage(room.roomId, message.createBasic(promisedMessage)); + } else { + logger.error("Unable to process response message: %s", promisedMessage); + } + }) } /** diff --git a/bot/module/abstract.js b/bot/module/abstract.js index de16bac..38d2266 100644 --- a/bot/module/abstract.js +++ b/bot/module/abstract.js @@ -5,6 +5,7 @@ let { logger } = require('../logging'); let message = require('../message'); let { isFunction, getObjectKeysToPrototype } = require('../utility'); +let { getConfig } = require('../config'); class AbstractModule { /* @@ -32,16 +33,30 @@ class AbstractModule { 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; + /* + Indicates if the module requires a readable config file. + */ + needConfig = false; + + /* internal */ + + /* + The loaded config file, if it exists. + */ + _config = null; + constructor(name, description, command) { this.name = name; this.description = description; @@ -59,6 +74,14 @@ class AbstractModule { return this._recognizedCommandMap.keys(); } + getConfigFilePath() { + return process.env.NODE_PATH + '/data/' + this.name.toLowerCase().replace(' ', '_') + '-config.json'; + } + + getConfigSensitiveFields() { + return []; + } + /** * Default functionality for receiving and processing a message. * @@ -126,7 +149,17 @@ let abstractModulePrototype = Object.getPrototypeOf(new AbstractModule('', '', ' Initialization of a module. */ function init(mod) { - logger.debug("Initializing module %s", mod.name) + logger.debug("Initializing module %s", mod.name); + if (mod.needConfig) { + logger.debug("Loading config file %s", mod.getConfigFilePath()); + try { + mod._config = getConfig(mod.getConfigFilePath(), mod.getConfigSensitiveFields()); + } catch (e) { + logger.error("Module %s needs a valid config file at %s", mod.name, mod.getConfigFilePath()); + process.exit(1); + } + } + logger.debug("Detecting command methods."); let commandMethods = getObjectKeysToPrototype(mod, abstractModulePrototype, (key) => { return key.startsWith('cmd_') && isFunction(mod[key]); }) diff --git a/bot/module/admin.js b/bot/module/admin.js index fafd4bd..7b560e9 100644 --- a/bot/module/admin.js +++ b/bot/module/admin.js @@ -12,7 +12,7 @@ class AdminModule extends AbstractModule { "admin" ); this.helpAndUsage = `Usage: admin - ...` + ...`; } } diff --git a/bot/module/giphy.js b/bot/module/giphy.js index 5fb6fb8..2015b4e 100644 --- a/bot/module/giphy.js +++ b/bot/module/giphy.js @@ -3,6 +3,8 @@ */ let { AbstractModule } = require('./abstract'); +let axios = require('axios'); +let { logger } = require('../logging'); class GiphyModule extends AbstractModule { constructor() { @@ -12,7 +14,32 @@ class GiphyModule extends AbstractModule { "giphy" ); this.helpAndUsage = `Usage: !giphy itsworking - ...` + ...`; + this.needConfig = true; + this.defaultCommand = 'search'; + } + + getConfigSensitiveFields() { + return ["apiKey"]; + } + + getGiphySearch(term) { + let url = this._config.endpoint + '/gifs/search?api_key=' + this._config.apiKey + '&q=' + term + '&limit=1'; + logger.debug("Requesting: %s", url.replace(this._config.apiKey, '******')); + return axios.get(url); + } + + /** + * Return the top item for the search terms. + * + * @param {...any} args + */ + cmd_search(...args) { + return this.getGiphySearch(args[0]) + .then((response) => { + // logger.debug("Giphy response: %o", response.data.data[0].url); + return response.data.data[0].embed_url; + }) } } diff --git a/bot/module/help.js b/bot/module/help.js index 5685b84..0ad96be 100644 --- a/bot/module/help.js +++ b/bot/module/help.js @@ -19,9 +19,9 @@ class HelpModule extends AbstractModule { _default_help_message() { let help = `!help `; - for (let command of this._commandList) { - help += "\n!help " + command + " : " + this._commandMap.get(command).description; - } + for (let command of this._commandList) { + help += "\n!help " + command + " : " + this._commandMap.get(command).description; + } return help; } diff --git a/data/giphy-config.json.example b/data/giphy-config.json.example new file mode 100644 index 0000000..ff1f1dd --- /dev/null +++ b/data/giphy-config.json.example @@ -0,0 +1,4 @@ +{ + "endpoint": "api.giphy.com/v1", + "apiKey": "" +} \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index 02827e1..47ea9d7 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,5 +1,8 @@ #! /usr/bin/env sh +DIR="$( cd "$( dirname "${0}" )" >/dev/null 2>&1 && pwd )" +export NODE_PATH="${DIR}" + case $1 in run) node index.js diff --git a/index.js b/index.js index 56f2eaa..1e1f832 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ let bot = require('./bot/bot'); let { getConfig } = require('./bot/config'); let engine = require('./bot/engine'); -let config = getConfig("../data/config.json") +let config = getConfig(process.env.NODE_PATH + "/data/config.json", ['accessToken']) engine.create( config, diff --git a/package-lock.json b/package-lock.json index 3765bb2..b11c55b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,6 +86,15 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.0.tgz", "integrity": "sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==" }, + "axios": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -440,9 +449,9 @@ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fast-safe-stringify": { "version": "2.0.7", @@ -472,6 +481,29 @@ "is-buffer": "~2.0.3" } }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -607,8 +639,7 @@ "is-buffer": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", - "dev": true + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" }, "is-callable": { "version": "1.1.4", @@ -962,9 +993,9 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "psl": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.6.0.tgz", - "integrity": "sha512-SYKKmVel98NCOYXpkwUqZqh0ahZeeKfmisiLIcEZdsb+WbLv02g/dI5BUmZnIyOe7RzZtLax81nnb2HbvC2tzA==" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" }, "punycode": { "version": "2.1.1", diff --git a/package.json b/package.json index 1c09411..528dff5 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "test": "mocha" }, "dependencies": { + "axios": "^0.19.0", "matrix-js-sdk": "2.4.5", "trie-prefix-tree": "^1.5.1", "winston": "^3.2.1"