diff --git a/README.md b/README.md index 3bafc65..083216f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ projects and events. ## Quick Start 1. Fork (optional) and clone: - `git clone https://github.com/FabricLabs/doorman.git` + `git clone https://github.com/FabricLabs/doorman.git` 2. Run `cp config/index.json.example config/index.json` & edit to your liking 2. Run `cp config/auth.json.example config/auth.json` & add your [auth tokens](#auth) 3. Run `npm install` @@ -57,8 +57,8 @@ in `config/index.json` (without the `doorman-` prefix): ```js { - // slice of larger JSON file - "plugins": ["catfacts", "misc", "wikipedia", "urbandictionary"] + // slice of larger JSON file + "plugins": ["catfacts", "misc", "wikipedia", "urbandictionary"] } ``` @@ -67,13 +67,13 @@ file: ```js { - // slice of larger JSON file - "dependencies": { - "doorman-urbandictionary": "FabricLabs/doorman-urbandictionary", - "doorman-wikipedia": "FabricLabs/doorman-wikipedia", - "doorman-misc": "FabricLabs/doorman-misc", - "doorman-catfact": "FabricLabs/doorman-catfact" - }, + // slice of larger JSON file + "dependencies": { + "doorman-urbandictionary": "FabricLabs/doorman-urbandictionary", + "doorman-wikipedia": "FabricLabs/doorman-wikipedia", + "doorman-misc": "FabricLabs/doorman-misc", + "doorman-catfact": "FabricLabs/doorman-catfact" + }, } ``` @@ -104,85 +104,31 @@ List of external plugins for you to include with your installation (if you wish) - [datefact](https://github.com/FabricLabs/doorman-datefact) => spits out a random date fact - [remaeusfact](https://github.com/FabricLabs/doorman-remaeusfact) => spits out a random fact about [Remaeus](https://github.com/martindale) -### Writing Plugin +### Writing Plugins To write a Doorman plugin, create a new NPM module that exports an array named `commands` of triggers your bot will respond to. You can use a simple callback to display your message in both Slack and Discord, depending on the features you added: ```js module.exports = (Doorman) => { - return { - commands: [ - 'hello' - ], - hello: { - description: 'responds with hello!', - process: (msg, suffix, isEdit, cb) => { cb('hello!', msg); } - } - }; + return { + commands: [ + 'hello' + ], + hello: { + description: 'responds with hello!', + process: (msg, suffix, isEdit, cb) => { cb('hello!', msg); } + } + }; }; ``` If you think your plugin is amazing, please let us know! We'd love to add it to our list. Currently, the bot is configured to work with external repositories with the `doorman-` prefix. -## Installation - -Written in Node.JS. - -0. Install prereqs, see below for your OS. -1. Clone the repo. -2. Run `npm install` in the repo directory. - -For music playback (on Discord), you will need [ffmpeg](https://www.ffmpeg.org/download.html) installed and in your path variables. - -### Prereqs: - -#### On Unix - - * `python` (`v2.7` recommended, `v3.x.x` is __*not*__ supported) - * `make` - * A proper C/C++ compiler toolchain, like [GCC](https://gcc.gnu.org) - * `npm install -g node-gyp` - -#### On Mac OS X - - * `python` (`v2.7` recommended, `v3.x.x` is __*not*__ supported) (already installed on Mac OS X) - * [Xcode](https://developer.apple.com/xcode/download/) - * You also need to install the `Command Line Tools` via Xcode. You can find this under the menu `Xcode -> Preferences -> Downloads` - * This step will install `gcc` and the related toolchain containing `make` - * `npm install -g node-gyp` - -#### On Windows - -##### Option 1 - -Install all the required tools and configurations using Microsoft's [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) using `npm install --global --production windows-build-tools` from an elevated PowerShell or CMD.exe (run as Administrator). - -##### Option 2 - -Install tools and configuration manually: - * Visual C++ Build Environment: - * Option 1: Install [Visual C++ Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools) using the **Default Install** option. - - * Option 2: Install [Visual Studio 2015](https://www.visualstudio.com/products/visual-studio-community-vs) (or modify an existing installation) and select *Common Tools for Visual C++* during setup. This also works with the free Community and Express for Desktop editions. - - > :bulb: [Windows Vista / 7 only] requires [.NET Framework 4.5.1](http://www.microsoft.com/en-us/download/details.aspx?id=40773) - - * Install [Python 2.7](https://www.python.org/downloads/) (`v3.x.x` is not supported), and run `npm config set python python2.7` (or see below for further instructions on specifying the proper Python version and path.) - * Launch cmd, `npm config set msvs_version 2015` - - If the above steps didn't work for you, please visit [Microsoft's Node.js Guidelines for Windows](https://github.com/Microsoft/nodejs-guidelines/blob/master/windows-environment.md#compiling-native-addon-modules) for additional tips. - -Then, install node-gyp using `npm install -g node-gyp` - -### Customization -The `/examples/` directory contains example files for the configs.! These files need to be renamed, without the .example extension, and placed in the `/config/` folder. - ## Running Before first run you will need to create an `auth.json` file. A bot token is required for the bot to connect to the different services. The other credentials are not required for the bot to run, but highly recommended as commands that depend on them will malfunction. See `auth.json.example`. -To start the bot just run -`node start`. +To start the bot, just run `npm start`. ## Updates If you update the bot, please run `npm update` before starting it again. If you have issues with this, you can try deleting your node_modules folder and then running -`npm install` again. Please see [Installation](#Installation). +`npm install` again. diff --git a/config/index.js b/config/index.js deleted file mode 100644 index 54f8105..0000000 --- a/config/index.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = { - "name": "doorman", - "debug": false, - "commandPrefix": "!", - "plugins": [ - //"catfacts", - "admin", - "beer-lookup", - "xkcd", - "urbandictionary", - "wikipedia", - "cocktail-lookup", - "dice", - "dictionary", - "misc", - "translator" - ], - "discord": { - "enabled": true, - "serverName": "insert your server name", - "welcomeChannel": "insert channel id of channel where you want welcome to be", - "defaultEmbedColor": 5592405, - "pruneInterval": 10, - "pruneMax": 100 - }, - "slack": { - "enabled": true, - "teamName": "friendly team name", - "welcomeChannel": "general" - } -} diff --git a/doorman.js b/doorman.js index c3055ed..df6bc97 100644 --- a/doorman.js +++ b/doorman.js @@ -1,52 +1,11 @@ 'use strict'; -const fs = require('fs'); -const path = require('path'); -const chalk = require('chalk'); +const config = require('./config'); +const Doorman = require('./lib/doorman'); -console.log(chalk.green(`Starting Doorman...`)); -console.log(chalk.green(`Node version: ${process.version}`)); +function main () { + let doorman = new Doorman(config); + doorman.start(); +} -// Helpers -exports.require = function (filePath) { - delete require.cache[path.join(path.dirname(require.main.filename), filePath)]; - return require(path.join(path.dirname(require.main.filename), filePath))(this); -}; - -exports.getFileContents = function (filePath) { - try { - return fs.readFileSync(path.join(path.dirname(require.main.filename), filePath), 'utf-8'); - } catch (err) { - return ''; - } -}; - -exports.getFileArray = function (srcPath) { - try { - srcPath = path.join(path.dirname(require.main.filename), srcPath); - return fs.readdirSync(srcPath).filter(file => fs.statSync(path.join(srcPath, file)).isFile()); - } catch (err) { - return []; - } -}; - -exports.getJsonObject = function (filePath) { - return JSON.parse(exports.getFileContents(filePath)); -}; - -exports.resolveMention = function (usertxt) { - let userid = usertxt; - if (usertxt.startsWith('<@!')) { - userid = usertxt.substr(3, usertxt.length - 4); - } else if (usertxt.startsWith('<@')) { - userid = usertxt.substr(2, usertxt.length - 3); - } - return userid; -}; - -exports.config = require('./config'); -exports.auth = require('./config/auth'); - -// Now for the good stuff! -require('./lib/maki')(this); -require('./lib/doorman')(this); +main(); diff --git a/lib/discord/index.js b/lib/discord/index.js index 4fde67e..4c4ae84 100644 --- a/lib/discord/index.js +++ b/lib/discord/index.js @@ -1,18 +1,22 @@ 'use strict'; const path = require('path'); +const util = require('util'); const chalk = require('chalk'); -const commandDirectory = './plugins'; -module.exports = doorman => { - const commandFiles = doorman.getFileArray(commandDirectory); - const Discord = require('discord.js'); - doorman.Discord = new Discord.Client(); +const DiscordJS = require('discord.js'); +const Plugin = require('../plugin'); + +const helpers = require('../helpers'); +const commandFiles = helpers.getFileArray('./plugins'); + +function Discord (config) { + doorman.discord = new DiscordJS.Client(); console.log(chalk.magenta(`Discord Enabled... Starting.\nDiscord.js version: ${Discord.version}`)); if (doorman.auth.discord && doorman.auth.discord.bot_token) { console.log('Logging in to Discord...'); - doorman.Discord.login(doorman.auth.discord.bot_token); + doorman.discord.login(doorman.auth.discord.bot_token); // TODO: make these configurable... require('./onEvent/disconnected')(doorman); @@ -52,3 +56,7 @@ module.exports = doorman => { console.log(`Loaded ${doorman.commandCount()} base commands`); console.log(`Loaded ${Object.keys(doorman.discordCommands).length} Discord commands`); }; + +util.inherits(Discord, Plugin); + +module.exports = Discord; diff --git a/lib/disk.js b/lib/disk.js new file mode 100644 index 0000000..e0c432c --- /dev/null +++ b/lib/disk.js @@ -0,0 +1,13 @@ +'use strict'; + +const fs = require('fs'); + +function Disk () { + this.type = 'Disk'; +} + +Disk.prototype.exists = function (path) { + return fs.existsSync(path); +}; + +module.exports = Disk; diff --git a/lib/doorman.js b/lib/doorman.js index 9242f03..0fa77b6 100644 --- a/lib/doorman.js +++ b/lib/doorman.js @@ -1,104 +1,126 @@ 'use strict'; -const chalk = require('chalk'); -const path = require('path'); -const commandDirectory = '/plugins'; - -module.exports = doorman => { - const commandFiles = doorman.getFileArray(commandDirectory); - - // Helpers - doorman.addCommand = (commandName, commandObject) => { - try { - doorman.Commands[commandName] = commandObject; - } catch (err) { - console.log(err); - } - }; - - doorman.commandCount = () => { - return Object.keys(doorman.Commands).length; - }; - - doorman.setupCommands = () => { - doorman.Commands = {}; - - // Load command files - commandFiles.forEach(commandFile => { - try { - commandFile = require(`${path.join(commandDirectory, commandFile)}`)(doorman); - } catch (err) { - console.log(chalk.red(`Improper setup of the '${commandFile}' command file. : ${err}`)); - } - - if (commandFile) { - if (commandFile.commands) { - commandFile.commands.forEach(command => { - if (command in commandFile) { - doorman.addCommand(command, commandFile[command]); - } - }); - } - } - }); +const Plugin = require('./plugin'); +const Router = require('./router'); +const Scribe = require('./scribe'); + +/** + * General-purpose bot framework. + * @param {Object} config Overall configuration object. + * @constructor + */ +function Doorman (config) { + let self = this; + + self.config = config || {}; + self.services = {}; + self.triggers = {}; + + self.router = new Router(); + self.scribe = new Scribe({ + namespace: 'doorman' + }); +} + +require('util').inherits(Doorman, require('events').EventEmitter); + +Doorman.prototype.start = function configure () { + let self = this; + + if (self.config.triggers) { + Object.keys(self.config.triggers).forEach(name => { + let route = { + name: name, + value: self.config.triggers[name] + }; - // Load simple commands from json file - let jsonCommands = []; - try { - // TODO: refactor how external data is loaded - // I/O should not take place inside of the module - jsonCommands = doorman.getJsonObject('/config/commands.json'); - } catch (err) { } - - jsonCommands.forEach(jsonCommand => { - const command = jsonCommand.command; - const description = jsonCommand.description; - const response = jsonCommand.response; - - doorman.addCommand(command, { - description, - process: (msg, suffix, isEdit, cb) => { - cb({ - embed: { - color: doorman.config.discord.defaultEmbedColor, - description: response - } - }, msg); - } - }); + self.router.use(route); }); + } + + if (self.config.plugins && Array.isArray(self.config.plugins)) { + self.config.plugins.forEach(module => self.use(module)); + } + + if (self.config.services && Array.isArray(self.config.services)) { + self.config.services.forEach(module => self.enable(module)); + } + + if (self.config.debug) { + this.scribe.log('[DEBUG]', 'triggers:', Object.keys(self.triggers)); + } + + Object.keys(self.services).forEach(function launcher (name) { + self.services[name].connect(); + }); + + this.scribe.log('started!'); + + return this; +}; + +Doorman.prototype.enable = function enable (name) { + let Service = require(`../services/${name}`); + let service = new Service(this.config[name]); + + console.log('dat service:', service); - // Load external commands - if (doorman.config.plugins && Array.isArray(doorman.config.plugins)) { - doorman.config.plugins.forEach(module => { - if (doorman.Commands[module]) { - return; - } - try { - module = require(`doorman-${module}`)(doorman); - } catch (err) { - console.log(chalk.red(`Improper setup of the '${module}' command file. : ${err}`)); - return; - } - if (module && module.commands) { - module.commands.forEach(command => { - if (command in module) { - doorman.addCommand(command, module[command]); - } - }); - } - }); + service.on('message', function (msg) { + console.log('magic handler, incoming magic message:', msg); + }); + + this.services[name] = service; + this.services[name].connect(); + + return this; +}; + +Doorman.prototype.use = function assemble (plugin) { + this.scribe.log(`importing ${plugin}...`); + + let handler = Plugin.fromName(plugin); + this.scribe.log('handler:', handler); + + if (handler) { + let trigger = this.register(handler); + + if (!trigger) { + this.scribe.warn(`Could not successfully configure ${plugin}. Plugin disabled.`); } - }; + } - doorman.setupCommands(); + return this; +}; - // TODO: implement service modules (#8) - // https://github.com/FabricLabs/doorman/issues/8 +Doorman.prototype.register = function configure (handler) { + if (!handler.name) return false; + if (!handler.value) return false; - // Discord - require('./discord')(doorman); + this.triggers[handler.name] = handler.value; - // Slack - require('./slack')(doorman); + return this; }; + +Doorman.prototype.parse = function interpret (msg) { + let answers = this.router.route(msg); + let message = null; + + if (answers.length) { + switch (answers.length) { + case 1: + message = answers[0]; + break; + default: + message = answers.join('\n\n'); + break; + } + } + + if (message) { + this.emit('message', message); + } + + return message || null; +}; + +module.exports = Doorman; diff --git a/lib/helpers.js b/lib/helpers.js new file mode 100644 index 0000000..c0c8f1b --- /dev/null +++ b/lib/helpers.js @@ -0,0 +1,36 @@ +// Helpers +exports.require = function (filePath) { + delete require.cache[path.join(path.dirname(require.main.filename), filePath)]; + return require(path.join(path.dirname(require.main.filename), filePath))(this); +}; + +exports.getFileContents = function (filePath) { + try { + return fs.readFileSync(path.join(path.dirname(require.main.filename), filePath), 'utf-8'); + } catch (err) { + return ''; + } +}; + +exports.getFileArray = function (srcPath) { + try { + srcPath = path.join(path.dirname(require.main.filename), srcPath); + return fs.readdirSync(srcPath).filter(file => fs.statSync(path.join(srcPath, file)).isFile()); + } catch (err) { + return []; + } +}; + +exports.getJsonObject = function (filePath) { + return JSON.parse(exports.getFileContents(filePath)); +}; + +exports.resolveMention = function (usertxt) { + let userid = usertxt; + if (usertxt.startsWith('<@!')) { + userid = usertxt.substr(3, usertxt.length - 4); + } else if (usertxt.startsWith('<@')) { + userid = usertxt.substr(2, usertxt.length - 3); + } + return userid; +}; diff --git a/lib/plugin.js b/lib/plugin.js new file mode 100644 index 0000000..4cad82b --- /dev/null +++ b/lib/plugin.js @@ -0,0 +1,34 @@ +'use strict'; + +const util = require('util'); +const Disk = require('./disk'); + +function Plugin (doorman) { + this.doorman = doorman; +} + +util.inherits(Plugin, require('events').EventEmitter); + +Plugin.fromName = function (name) { + let disk = new Disk('../'); + let path = `plugins/${name}`; + let plugin = null; + + if (disk.exists(path)) { + plugin = require(path); + } else { + try { + plugin = require(`doorman-${name}`); + } catch (E) { + console.error('could not load:', E); + } + } + + return plugin; +}; + +Plugin.prototype.register = function attach (doorman) { + this.doorman = doorman; +}; + +module.exports = Plugin; diff --git a/lib/router.js b/lib/router.js new file mode 100644 index 0000000..357d4e6 --- /dev/null +++ b/lib/router.js @@ -0,0 +1,46 @@ +'use strict'; + +/** + * Maintains a list of triggers ("commands") and their behaviors. + * @param {Object} map Map of command names => behaviors. + * @constructor + */ +function Router (map) { + this.handlers = map || {}; +} + +/** + * Assembles an array of responses to the triggers contained in a particular message. + * @param {String} msg Input message to route. + * @return {Array} List of outputs generated from the input string. + */ +Router.prototype.route = async function handle (msg) { + if (typeof msg !== 'string') return; + let parts = msg.split(/\s+/g); + let output = []; + + for (var token in parts) { + let command = token.toLowerCase(); + if (this.handlers[command]) { + let result = await this.handlers[command].apply({}, msg); + if (result) { + output.push(result); + } + } + } + + return output; +}; + +/** + * Attaches a new handler to the router. + * @param {Plugin} plugin Instance of the plugin. + * @param {Plugin.name} name Name of the plugin. + * @return {Router} Configured instance of the router. + */ +Router.prototype.use = function configure (plugin) { + this.handlers[plugin.name] = plugin; + return this; +}; + +module.exports = Router; diff --git a/lib/scribe.js b/lib/scribe.js new file mode 100644 index 0000000..a15aea0 --- /dev/null +++ b/lib/scribe.js @@ -0,0 +1,23 @@ +'use strict'; + +function Scribe (config) { + this.type = 'Scribe'; + this.config = config || { namespace: 'scribe' }; +} + +Scribe.prototype.log = function append (...inputs) { + inputs.unshift(['[', this.config.namespace.toUpperCase(), ']'].join('')); + console.log.apply(null, inputs); +}; + +Scribe.prototype.throw = function exit (...inputs) { + inputs.unshift(['[', this.config.namespace.toUpperCase(), ']'].join('')); + console.error.apply(null, inputs); +}; + +Scribe.prototype.warn = function exit (...inputs) { + inputs.unshift(['[', this.config.namespace.toUpperCase(), ']'].join('')); + console.warn.apply(null, inputs); +}; + +module.exports = Scribe; diff --git a/lib/service.js b/lib/service.js new file mode 100644 index 0000000..b0eb241 --- /dev/null +++ b/lib/service.js @@ -0,0 +1,29 @@ +'use strict'; + +const util = require('util'); +const stream = require('stream'); + +function Service (config) { + this.config = config || {}; + this.connection = null; + this.map = {}; +} + +util.inherits(Service, require('events').EventEmitter); + +Service.prototype.connect = function initialize () { + // TODO: implement a basic Stream + this.connection = { + status: 'active' + }; +}; + +Service.prototype.handler = function route (message) { + this.emit('message', message.text); +}; + +Service.prototype.send = function send (channel, message) { + console.log('[SERVICE]', 'send:', channel, message); +}; + +module.exports = Service; diff --git a/lib/slack/index.js b/lib/slack/index.js index 46722e6..7163649 100644 --- a/lib/slack/index.js +++ b/lib/slack/index.js @@ -10,12 +10,12 @@ module.exports = doorman => { console.log('Logging in to Slack...'); // initialize Slack client - doorman.Slack = new RTMClient(doorman.auth.slack.bot_token); - doorman.Slack.c_events = Slack.CLIENT_EVENTS; - doorman.Slack.rtm_events = Slack.RTM_EVENTS; + doorman.slack = new RTMClient(doorman.auth.slack.bot_token); + doorman.slack.c_events = Slack.CLIENT_EVENTS; + doorman.slack.rtm_events = Slack.RTM_EVENTS; // TODO: ask naterchrdsn why this is before plugins in the control flow? - doorman.Slack.start(); + doorman.slack.start(); // load event handlers? require('./onEvent/auth-and-connect')(doorman); diff --git a/lib/slack/onEvent/auth-and-connect.js b/lib/slack/onEvent/auth-and-connect.js index 6d508ab..fdece9c 100644 --- a/lib/slack/onEvent/auth-and-connect.js +++ b/lib/slack/onEvent/auth-and-connect.js @@ -1,9 +1,9 @@ -module.exports = function (Doorman) { - Doorman.Slack.on('ready', rtmStartData => { +module.exports = function (doorman) { + doorman.slack.on('ready', rtmStartData => { //console.log(`Logged into Slack! Name: ${rtmStartData.self.name}, Team:${rtmStartData.team.name}`); console.log(`Logged into Slack!`); }); - Doorman.Slack.on('connected', () => { + doorman.slack.on('connected', () => { console.log(`Connection to Slack Successful!`); }); }; diff --git a/lib/slack/onEvent/message.js b/lib/slack/onEvent/message.js index 97e1970..072e3a7 100644 --- a/lib/slack/onEvent/message.js +++ b/lib/slack/onEvent/message.js @@ -1,6 +1,6 @@ -module.exports = function (Doorman) { +module.exports = function (doorman) { function currentChannelHasHook (channelId) { - if (Doorman.Auth.slack.webhooks[Doorman.Slack.dataStore.getChannelById(channelId).name]) { + if (doorman.Auth.slack.webhooks[doorman.slack.dataStore.getChannelById(channelId).name]) { return true; } return false; @@ -57,7 +57,7 @@ module.exports = function (Doorman) { reformatted.attachments[0].pretext = `<@${output.reply}>`; } if (currentChannelHasHook(msg.channel)) { - const url = Doorman.Auth.slack.webhooks[Doorman.Slack.dataStore.getChannelById(msg.channel).name]; + const url = doorman.Auth.slack.webhooks[doorman.slack.dataStore.getChannelById(msg.channel).name]; require('request').post({ uri: url, json: true, @@ -75,12 +75,12 @@ module.exports = function (Doorman) { if (delCalling) { return msg.channel.send(output).then(() => msg.delete()); } */ - Doorman.Slack.sendMessage(output, msg.channel); + doorman.slack.sendMessage(output, msg.channel); } function checkMessageForCommand (msg, isEdit) { - // TODO: re-bind Doorman.config to Doorman.config - //const bot = Doorman.Slack.dataStore.getBotByName(Doorman.config.name); + // TODO: re-bind doorman.config to doorman.config + //const bot = doorman.slack.dataStore.getBotByName(doorman.config.name); msg.author = `<@${msg.user}>`; // Drop our own messages to prevent feedback loops @@ -88,44 +88,46 @@ module.exports = function (Doorman) { return; } - if (Doorman.config.debug) { + if (doorman.config.debug) { console.log('message received:', msg.type, msg.subtype, 'interpreting...'); } // Check for mention /* if (msg.text.split(' ')[0] === `<@${bot.id}>`) { - if (Doorman.config.elizaEnabled) { + if (doorman.config.elizaEnabled) { // If Eliza AI is enabled, respond to @mention const message = msg.text.replace(`<@${bot.id}> `, ''); - Doorman.Slack.sendMessage(Doorman.Eliza.transform(message), msg.channel); + doorman.slack.sendMessage(doorman.Eliza.transform(message), msg.channel); return; } - Doorman.Slack.sendMessage('Yes?', msg.channel); + doorman.slack.sendMessage('Yes?', msg.channel); return; }*/ // Check for IM - /*if (Doorman.Slack.dataStore.getDMById(msg.channel)) { - if (msg.text.startsWith(Doorman.config.commandPrefix) && msg.text.split(' ')[0].substring(Doorman.config.commandPrefix.length).toLowerCase() === 'reload') { - Doorman.setupCommands(); - Doorman.setupSlackCommands(); - Doorman.Slack.sendMessage(`Reloaded ${Doorman.commandCount()} Base Commands`, msg.channel); - Doorman.Slack.sendMessage(`Reloaded ${Object.keys(Doorman.slackCommands).length} Slack Commands`, msg.channel); + /*if (doorman.slack.dataStore.getDMById(msg.channel)) { + if (msg.text.startsWith(doorman.config.commandPrefix) && msg.text.split(' ')[0].substring(doorman.config.commandPrefix.length).toLowerCase() === 'reload') { + doorman.setupCommands(); + doorman.setupSlackCommands(); + doorman.slack.sendMessage(`Reloaded ${doorman.commandCount()} Base Commands`, msg.channel); + doorman.slack.sendMessage(`Reloaded ${Object.keys(doorman.slackCommands).length} Slack Commands`, msg.channel); return; } - Doorman.Slack.sendMessage(`I don't respond to direct messages.`, msg.channel); + doorman.slack.sendMessage(`I don't respond to direct messages.`, msg.channel); return; }*/ // Check if message is a command - if (msg.text.startsWith(Doorman.config.commandPrefix)) { - const allCommands = Object.assign(Doorman.Commands, Doorman.slackCommands); - const cmdTxt = msg.text.split(' ')[0].substring(Doorman.config.commandPrefix.length).toLowerCase(); - const suffix = msg.text.substring(cmdTxt.length + Doorman.config.commandPrefix.length + 1); // Add one for the ! and one for the space + if (msg.text.startsWith(doorman.config.commandPrefix)) { + const allCommands = Object.assign(doorman.Commands, doorman.slackCommands); + const cmdTxt = msg.text.split(' ')[0].substring(doorman.config.commandPrefix.length).toLowerCase(); + const suffix = msg.text.substring(cmdTxt.length + doorman.config.commandPrefix.length + 1); // Add one for the ! and one for the space const cmd = allCommands[cmdTxt]; + + console.log('MESSAGE:', msg); if (cmdTxt === 'help') { - const DM = Doorman.Slack.dataStore.getDMByUserId(msg.user).id; + const DM = doorman.slack.dataStore.getDMByUserId(msg.user).id; if (suffix) { const cmds = suffix.split(' ').filter(cmd => { return allCommands[cmd]; @@ -134,7 +136,7 @@ module.exports = function (Doorman) { if (cmds.length > 0) { cmds.forEach(cmd => { // TODO: add permissions check back here - info += `**${Doorman.config.commandPrefix + cmd}**`; + info += `**${doorman.config.commandPrefix + cmd}**`; const usage = allCommands[cmd].usage; if (usage) { info += ` ${usage}`; @@ -148,17 +150,17 @@ module.exports = function (Doorman) { } info += '\n'; }); - Doorman.Slack.sendMessage(info, DM); + doorman.slack.sendMessage(info, DM); return; } - Doorman.Slack.sendMessage('I can\'t describe a command that doesn\'t exist', msg.channel); + doorman.slack.sendMessage('I can\'t describe a command that doesn\'t exist', msg.channel); } else { - Doorman.Slack.sendMessage('**Available Commands:**', DM); + doorman.slack.sendMessage('**Available Commands:**', DM); let batch = ''; const sortedCommands = Object.keys(allCommands).sort(); for (const i in sortedCommands) { const cmd = sortedCommands[i]; - let info = `**${Doorman.config.commandPrefix + cmd}**`; + let info = `**${doorman.config.commandPrefix + cmd}**`; const usage = allCommands[cmd].usage; if (usage) { info += ` ${usage}`; @@ -172,14 +174,14 @@ module.exports = function (Doorman) { } const newBatch = `${batch}\n${info}`; if (newBatch.length > (1024 - 8)) { // Limit message length - Doorman.Slack.sendMessage(batch, DM); + doorman.slack.sendMessage(batch, DM); batch = info; } else { batch = newBatch; } } if (batch.length > 0) { - Doorman.Slack.sendMessage(batch, DM); + doorman.slack.sendMessage(batch, DM); } } } else if (cmd) { @@ -189,16 +191,16 @@ module.exports = function (Doorman) { cmd.process(msg, suffix, isEdit, slackMessageCb); } catch (err) { let msgTxt = `Command ${cmdTxt} failed :disappointed_relieved:`; - if (Doorman.config.debug) { + if (doorman.config.debug) { msgTxt += `\n${err.stack}`; } - Doorman.Slack.sendMessage(msgTxt, msg.channel); + doorman.slack.sendMessage(msgTxt, msg.channel); } } else { - Doorman.Slack.sendMessage(`${cmdTxt} not recognized as a command!`, msg.channel); + doorman.slack.sendMessage(`${cmdTxt} not recognized as a command!`, msg.channel); } } } - Doorman.Slack.on('message', msg => checkMessageForCommand(msg, false)); + doorman.slack.on('message', msg => checkMessageForCommand(msg, false)); }; diff --git a/lib/slack/onEvent/team-join.js b/lib/slack/onEvent/team-join.js index 6ffe0ad..ccb0879 100644 --- a/lib/slack/onEvent/team-join.js +++ b/lib/slack/onEvent/team-join.js @@ -1,5 +1,5 @@ -module.exports = function (Doorman) { - Doorman.Slack.on('team_join', event => { - Doorman.Slack.sendMessage(`@here, please Welcome ${event.user.name} to ${Doorman.config.slack.teamName}!`, Doorman.Slack.dataStore.getChannelByName(Doorman.config.slack.welcomeChannel).id); +module.exports = function (doorman) { + doorman.slack.on('team_join', event => { + doorman.slack.sendMessage(`@here, please Welcome ${event.user.name} to ${doorman.config.slack.teamName}!`, doorman.slack.dataStore.getChannelByName(doorman.config.slack.welcomeChannel).id); }); }; diff --git a/package.json b/package.json index c5dfdef..8e2f978 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/fabriclabs/doorman.git" + "url": "git+https://github.com/FabricLabs/doorman.git" }, "keywords": [ "maki", @@ -20,16 +20,16 @@ "slackin", "invitations" ], - "author": "Nate Richardson (https://naterichardson.com)", + "author": "Fabric Labs (https://labs.fabric.pub)", "contributors": [ "Nate Richardson (https://naterichardson.com)", "Eric Martindale (https://www.ericmartindale.com)" ], "license": "MIT", "bugs": { - "url": "https://github.com/fabriclabs/doorman/issues" + "url": "https://github.com/FabricLabs/doorman/issues" }, - "homepage": "https://github.com/fabriclabs/doorman#readme", + "homepage": "https://github.com/FabricLabs/doorman#readme", "dependencies": { "@slack/client": "^4.1.0", "bufferutil": "^3.0.1", diff --git a/services/slack.js b/services/slack.js new file mode 100644 index 0000000..ce13871 --- /dev/null +++ b/services/slack.js @@ -0,0 +1,37 @@ +'use strict'; + +const util = require('util'); +const SlackSDK = require('@slack/client'); +const Service = require('../lib/service'); + +function Slack (config) { + this.config = config || {}; + this.connection = null; + this.map = {}; +} + +util.inherits(Slack, Service); + +Slack.prototype.connect = function initialize () { + if (this.config.token) { + this.connection = new SlackSDK.RTMClient(this.config.token); + this.connection.on('ready', this.ready); + this.connection.on('message', this.handler.bind(this)); + this.connection.start(); + } +}; + +Slack.prototype.ready = function (data) { + console.log('le data:', data); +}; + +Slack.prototype.handler = function route (message) { + this.emit('message', message.text); +}; + +Slack.prototype.send = function send (channel, message) { + console.log('[SLACK]', 'send:', channel, message); + this.connection.sendMessage(message, this.map[channel]); +}; + +module.exports = Slack; diff --git a/test/doorman.unit.js b/test/doorman.unit.js index def72ba..de05fbd 100644 --- a/test/doorman.unit.js +++ b/test/doorman.unit.js @@ -1,7 +1,7 @@ -var assert = require('assert'); -var expect = require('chai').expect; +const assert = require('assert'); +const expect = require('chai').expect; -var Doorman = require('../'); +const Doorman = require('../lib/doorman'); describe('Doorman', function () { it('should expose a constructor', function () { @@ -9,11 +9,19 @@ describe('Doorman', function () { }); it('can handle a message', function (done) { - let doorman = new Doorman(); + let doorman = new Doorman({ + triggers: { + 'debug': 'This is a debug response.' + } + }); + doorman.on('input', function (data) { assert.ok(data); done(); }); + + doorman.start(); + doorman.parse('Hello, world. This is a !debug trigger!'); }); it('can initialize a connection', function (done) {