diff --git a/.gitignore b/.gitignore index 0abd9d3..c273066 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ logs/ config.json node_modules/ config.json +.DS_Store diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..1b1cdf7 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node master.js diff --git a/README.md b/README.md index 928ad46..3de334e 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,16 @@ .d8888b. 888 888 d88P Y88b 888 888 Y88b. 888 888 - "Y888b. 888 d888b. .d8888b 888 .888 .d88b. 888d88b + "Y888b. 888 d888b. .d8888b 888 .888 .d88b. 888d88b "Y88b. 888 "88b d88P" 888 .88P d8P Y8b 888P" - "888 888 .d888888 888 888888K 88888888 888 + "888 888 .d888888 888 888888K 88888888 888 Y88b d88P 888 888 888 Y88b. 888 "88b Y8b. 888 "Y8888P" 888 "Y888888 "Y8888P 888 .888 "Y888P 888 ```` ### Slacker is a bot for [Slack](https://slack.com) built on the Node.js platform. - Slacker processes Slack WebHook requests and executes pre-defined actions before responding to Slack. It deals with the networking aspects of your bot so you can focus on functionality. + Slacker processes Slack Slash Command requests and executes pre-defined actions before responding to Slack. It deals with the networking aspects of your bot so you can focus on functionality. ## Setup @@ -25,25 +25,92 @@ 3. Set Slack Integrations. - * Create a Outgoing WebHook in Slack and point it to your instance of Slacker. + * Create a Slash Command integration in Slack and point it to your instance of Slacker. * Slacker expects to receive requests directly to the server's root URL. - * Be sure to include a valid `key` URL parameter on your outgoing-webhook URL when you set it in Slack. + * Be sure to note the token for this Slash Command, as you will need it for the configuration file. - * Create an Incoming WebHook in Slack. + * Get your admin user token. The easiest way is to use the (API explorer)[https://api.slack.com/methods/emoji.list/test], and grab it off the url. - * Be sure to update `hostname` and `path` in `config.json` in match your incoming-webhook. + 4. Create a `config.json` file. Note that a `sample-config.json` is included in this package. You can simply copy this, and replace the two token values with the tokens that you noted above. - 4. Start Slacker. + ````json + { + "port": 80, + "logs": "logs", + "token": { + "slashCommand": "[Enter your Slash Command token here.]", + "user": "[Enter your User token here.]" + }, + "timeout": 8000 + } + ```` + + 5. Start Slacker. + + ```` + ➜ ~ sh Slacker + ```` +## Using Slacker + + Slacker uses a command-line-like syntax. To interact with Slacker, use the Slash Command that you previously set up. For demonstration purposes, we'll assume that you named it `/slacker`. + + One of the most basic uses of Slacker is to get a list of commands. To do this, on an empty input line in Slack, type + + ```` + ➜ /slacker list + ```` + + Assuming that everything is set up correctly, you should now see a list of commands. Note that these are returned to you through Slackbot, which only you can see. + + Another useful action is __help__. If you wanted to learn how to use the `gif` action, for instance, you could use the following command. + + ```` + ➜ /slacker help gif + ```` + + This time, we sent 'gif' as an argument for __help__. Beware that different actions may use arguments differently, so pay attention to their help. If you want to send multiple words as single argument, simply wrap them in quotes like so. ```` - ➜ ~ sh Slacker + ➜ /slacker gif "Homer Simpson" ```` + Alternately, you can escape a space like so. + + ```` + ➜ /slacker gif Peter/ Griffin + ```` + + If you want to pass in quotes as part of an argument, you can escape them as well. + + ```` + ➜ /slacker echo "The worm looked up at me and said, \"I'd like to poison your mind.\"" + ```` + + You can also redirect where the output goes through the use of the `>` character, which is often used to redirect output on a terminal to a file. Valid targets include rooms, + + ```` + ➜ /slacker gif daily kitten > #general + ```` + + and users. + + ```` + ➜ /slacker btc > @stewie_griffin + ```` + + One of the more powerful features of Slacker is the user of pipes (`|`), where you can pipe the output of one action into the input of the next. + + ```` + ➜ /slacker btc | echo + ```` + + (Yes, I know that example is lame, but our actions catalogue is a bit light at the moment.) + ## Actions - Actions are script files located in the `actions/` directory. When Slacker receives a request from Slack it will check for and execute the appropriate action. Actions receive a `data` object with relevant information about the request from Slack, process the data and then return a response string. Slacker facilitates the response process. + Actions are script files located in the `actions/` directory. When Slacker receives a request from Slack it will check for and execute the appropriate action. Actions receive a `data` object with relevant information about the request from Slack and the command that triggered it, process the data and then return a response string. Slacker facilitates the response process. ### Creating an Action @@ -57,36 +124,38 @@ ### A Sample Action - ```` - var bot = require(__dirname + '/../bot.js') + ````javascript + // Loads all of the bot functionality. + var bot = require(__dirname + '/../bot.js'); var action = { + // This is the string that will name: 'echo', - trigger: /^echo \".*\"$/, - + // Used for the `list` action. description: 'Echo a string to Slack.', + // Used for the `help` action. + helpText: 'Echo a string to Slack.', + setup: function() { // This method will be run at server start up. }, execute: function(data, callback) { - callback(data.text.split('\"')[1]) + // If piped data is provided, send that, otherwise send any text passed in. + callback( data.pipedResponse || data.command.arguments.join(' ') ); } - } + }; - bot.addAction(action) + // Adds this action to the action list. + bot.addAction(action); ```` #### Name The `name` attribute defines your action. It is used by the __help__ and __list__ actions to inform users about your action. `name` attributes must be unique; Slacker will ignore actions if their `name` has already been defined by another action. This attribute is required on all actions. -#### Trigger - - Your action's `trigger` attribute is a regular expression which defines when it will be exectued. If a Slack user activates your bot and his message text matches your action's `trigger` then Slacker will perform your action. `trigger` attributes should be unique; Slacker will ignore actions if their `trigger` attributes have already been defined by another action. This attribute is required on all actions. - #### Description The `description` attribute is a string used to describe your action when a user triggers the __list__ action. This attribute is required on all actions. @@ -101,16 +170,26 @@ ##### The `data` Object - ```` - var data = { - team_id: 'T028JNZFM', - team_domain: 'mobdev', - channel_id: 'C02DCNQRN', - channel_name: 'slacker-testing', - timestamp: '1405215092.000210', - user_id: 'U02A1R3PM', - user_name: 'chris_young', - text: 'echo foo' + ````javascript + data === { + "channel_id": "D02DVPT67", + "channel_name": "directmessage", + "team_domain": "ustice", + "team_id": "T02DVPT63", + "text": "echo \"This is a test of the Emergency Broadcast System.\"", + "user_id": "U02DVPT65", + "user_name": "ustice", + "command": { + "name": "echo", + "id": "A230317A-82C7-4FA5-91A8-92DFEFB49C06", + "arguments": [ + "This is a test of the Emergency Broadcast System." + ], + "switches": [], + "pipe": false, + "redirectTo": [] + }, + "pipedResponse": null } ```` diff --git a/Slacker b/Slacker index aaac5aa..b42bd95 100755 --- a/Slacker +++ b/Slacker @@ -6,4 +6,4 @@ rm -rf logs/*.log clear -node master.js +node master.js $1 $2 $3 $4 $5 $6 $7 $8 $9 diff --git a/action_data/help.md b/action_data/help.md index 297eeb3..45250f5 100644 --- a/action_data/help.md +++ b/action_data/help.md @@ -1,3 +1 @@ -*Help* - -Try `list` to see available actions, or try `help action` for help with a specific action. +Try `list` to see available actions, or try `help ` for help with a specific action. diff --git a/actions/btc.js b/actions/btc.js index 1ae189b..e1553ff 100644 --- a/actions/btc.js +++ b/actions/btc.js @@ -21,16 +21,18 @@ var action = { }); response.on('end', function() { try { - callback(JSON.parse(responseText).ask + ' USD'); + callback('1 BTC = ' + JSON.parse(responseText).ask + ' USD'); } catch (exception) { throw exception; } }); - }).end(); + }); request.on('error', function(error) { throw error; }); + + request.end(); } }; diff --git a/actions/commitmsg.js b/actions/commitmsg.js new file mode 100644 index 0000000..a898ff3 --- /dev/null +++ b/actions/commitmsg.js @@ -0,0 +1,25 @@ +var bot = require(__dirname + '/../bot.js'); +var $ = require('jquerygo'); + +var action = { + name: "commitmsg", + + description: "Display a message from http://whatthecommit.com/", + + helpText: "Display a random message from http://whatthecommit.com/", + + execute: function(data, callback) { + $.visit("http://whatthecommit.com/", function() { + $("#content p:first-child").text(function(text) { + if (text) { + callback(text); + } else { + callback("Sorry. Something went wrong :disappointed:"); + } + $.close(); + }); + }); + } +}; + +bot.addAction(action); \ No newline at end of file diff --git a/actions/echo.js b/actions/echo.js index 49131c2..ac8cbfd 100644 --- a/actions/echo.js +++ b/actions/echo.js @@ -1,19 +1,25 @@ +// Loads all of the bot functionality. var bot = require(__dirname + '/../bot.js'); var action = { + // This is the string that will name: 'echo', - trigger: /^echo \".*\"$/, - + // Used for the `list` action. description: 'Echo a string to Slack.', + // Used for the `help` action. + helpText: 'Echo a string to Slack.', + setup: function() { // This method will be run at server start up. }, execute: function(data, callback) { - callback( data.pipedResponse || data.command.arguments.join(' ') ); + // If piped data is provided, send that, otherwise send any text passed in. + callback( data.pipedResponse || data.command.arguments.join(' ') ); } }; +// Adds this action to the action list. bot.addAction(action); diff --git a/actions/gif.js b/actions/gif.js index fb81d10..55242b7 100644 --- a/actions/gif.js +++ b/actions/gif.js @@ -6,9 +6,15 @@ var https = require('https') var action = { name: 'gif', - trigger: /^gif ".*"$/, + description: 'Displays animated GIFs from Giphy.', - description: 'Display a random GIF from Giphy.', + helpText: '' + + 'Displays animated GIFs from Giphy.\n' + + '```' + + '/slacker gif [tag] [...]' + + '```\n' + + 'If no tag is supplied, then `gif` will return a completely random gif. If one or more tags are provided, `gif` will return a random result from a search for all of the tags on Giphy.' + , apiKey: 'dc6zaTOxFJmzC', diff --git a/actions/help.js b/actions/help.js index 692bad5..61e5300 100644 --- a/actions/help.js +++ b/actions/help.js @@ -1,37 +1,57 @@ var bot = require(__dirname + '/../bot.js') var fs = require('fs') +var _ = require('lodash'); var action = { name: 'help', - trigger: /^help.*/, - description: 'Display information on how to user Slacker.', helpText: 'help.md has not loaded yet, try again.', setup: function() { fs.readFile(__dirname + '/../action_data/help.md', function(error, data) { - if (error) - throw error + if (error) { + throw error; + } - action.helpText = data.toString() + action.helpText = data.toString(); }) }, execute: function(data, callback) { - var components = data.text.split(' ') - - if (components.length === 1) - callback(this.helpText) - - for (var x = 0; x < bot.actions.length; x++) - if (bot.actions[x].name === components[1]) - callback(bot.actions[x].trigger) + var args, action, helpText, helpTitle; + + args = data.command.arguments; + helpText = ''; + helpTitle = ''; + + if (args.length === 0) { + + helpTitle = this.name; + helpText = this.helpText; + } else if (args.length >= 1) { + action = _.find(bot.actions, {name: data.command.arguments[0]}); + helpTitle = args[0]; + + if (!action) { + console.log('No action.'); + helpText = 'Command, `' + args[0] + '`, not found. Did you mistype it?'; + } else if (!action.helpText && !action.description) { + helpText = 'No help information found.'; + } else if (!action.helpText && action.description) { + helpText = action.description; + } else if (typeof action.helpText === 'string') { + helpText = action.helpText; + } else if (typeof action.helpText === 'function') { + helpText = action.helpText(); + } + } + + callback('*' + helpTitle.toUpperCase() + '*\n' + '> ' + helpText.replace(/\n/g, '\n> ')); - callback('Action "' + components[1] + '" not found.') } } -bot.addAction(action) +bot.addAction(action); diff --git a/actions/list.js b/actions/list.js index 16e03c8..b9e1272 100644 --- a/actions/list.js +++ b/actions/list.js @@ -3,8 +3,6 @@ var bot = require(__dirname + '/../bot.js') var action = { name: 'list', - trigger: /^list$/, - description: 'List all availble actions.', execute: function(data, callback) { @@ -14,6 +12,6 @@ var action = { callback(output) } -} +}; -bot.addAction(action) +bot.addAction(action); diff --git a/actions/talky.js b/actions/talky.js index ddea747..724088d 100644 --- a/actions/talky.js +++ b/actions/talky.js @@ -5,10 +5,16 @@ var id = require('../library/id.js'); var action = { name: 'talky', - trigger: /^talky\s*(['"]?)(.*?)(\1)$/, - description: 'Create a talky room for video conferencing and video chatting.', + helpText: '' + + 'Create a talky room for video conferencing and video chatting.\n' + + '```' + + '/slacker talky [room_name]' + + '```\n' + + 'If the optional `room_name` is inlcuded, then it will be used in the form of `http://talky.io/room_name`. If not a random one will be chosen.' + , + execute: function(data, callback) { var match = data.text.match(this.trigger); var room = (match && match[1]) ? encodeURI(data.text.replace(this.trigger, '$2')) : id(); @@ -18,7 +24,7 @@ var action = { text: 'Join the conference: .' }; - // Once bot supports formatted responses, replace the following with `callback(payload);` + // TODO: Once bot supports formatted responses, replace the following with `callback(payload);` callback(payload.text); } }; diff --git a/actions/weather.js b/actions/weather.js index cc435a4..33be6be 100644 --- a/actions/weather.js +++ b/actions/weather.js @@ -5,10 +5,11 @@ var http = require('http'); var action = { name: "weather", - trigger: /^weather ".*"$/, - description: "Display weather based on City or ZIP", + helpText: "Get weather from openweathermap.org by Zip or City and State + \n" + + "```" + "/slacker weather \"19102\" \n /slacker weather \"Philadelphia, PA\"" + "```", + execute: function(data, callback) { var query_param = data.text.substring(data.text.indexOf('\"') + 1, data.text.length - 1).replace(/ /g,"+"); @@ -61,11 +62,11 @@ var action = { callback(callback_data_str); } else { - callback("Something wasn't right :disappointed:"); + callback("Something wasn't right :disappointed: try again, please."); } } else { - callback("Where you at, bruh? Try `looking out the window` to get a read on the situation..."); + callback("Something wasn't right :disappointed: try again, please."); } }); }); diff --git a/actions/youtube.js b/actions/youtube.js index 7efa76f..13e95e4 100644 --- a/actions/youtube.js +++ b/actions/youtube.js @@ -5,48 +5,47 @@ var https = require('https'); var action = { name: "youtube", - trigger: /^youtube ".*"$/, - description: "Display a random YouTube video similar to the way the 'gif' command does.", api_key: "AIzaSyAHD4juIcaWTRC-sDhspzbPYh-GQ5BXkbs", execute: function(data, callback) { - - var query_param = data.text.substring(data.text.indexOf('\"') + 1, data.text.length - 1).replace(/ /g,"+"); - - var options = { - hostname: "www.googleapis.com", - path : "/youtube/v3/search?part=snippet&q=" + query_param + "&key=" + this.api_key, - port: 443, - method: "GET" - }; - - var request = https.request(options, function(response) { - var responseText = ""; - response.on("data", function(data) { - responseText += data.toString(); - }); - response.on("end", function() { - var videoArray = JSON.parse(responseText).items; - if (videoArray.length > 0) { - var random_int = Math.floor(Math.random() * videoArray.length); - var videoID = videoArray[random_int].id.videoId; - var videoURL = "http://www.youtube.com/watch?v=" + videoID; - callback(videoURL); - } - else { - callback("No Videos Today :disappointed:"); - } - }); + var input = data.pipedResponse || data.command.arguments.join(' '); + var query_param = input.replace(/ /g,"+"); + + var options = { + hostname: "www.googleapis.com", + path : "/youtube/v3/search?part=snippet&q=" + query_param + "&key=" + this.api_key, + port: 443, + method: "GET" + }; + + var request = https.request(options, function(response) { + var responseText = ""; + response.on("data", function(data) { + responseText += data.toString(); }); - - request.on("error", function(error) { - callback("Something wasn't right. :disappointed:"); - throw error; + response.on("end", function() { + var videoArray = JSON.parse(responseText).items; + if (videoArray.length > 0) { + var random_int = Math.floor(Math.random() * videoArray.length); + var videoID = videoArray[random_int].id.videoId; + var videoURL = "http://www.youtube.com/watch?v=" + videoID; + + callback(videoURL); + } + else { + callback("No Videos Today :disappointed:"); + } }); + }); + + request.on("error", function(error) { + callback("Something wasn't right. :disappointed:"); + throw error; + }); - request.end(); + request.end(); } }; diff --git a/bot.js b/bot.js index c9c87fb..33d68f2 100644 --- a/bot.js +++ b/bot.js @@ -5,31 +5,27 @@ var _ = require('lodash'); var fs = require('fs'); var https = require('https'); var querystring = require('querystring'); -var Slack = require('slack-node'); // Libraries -var config = require(__dirname + '/config.json'); +var config = require(__dirname + '/library/config'); var log = require(__dirname + '/library/log.js'); var parse = require(__dirname + '/library/parse.js'); -var slack = { - webhook: new Slack(config.token.webhook, config.domain), - api: new Slack(config.token.api) -}; - exports.actions = []; -exports.setup = function (callback) { +exports.setup = function setup (callback) { var setup; var x; + fs.readdir(__dirname + '/actions', function (error, files) { var errors = []; if (error) return callback(error, files); - + _.each(files, function (file) { - var action = require(__dirname + '/actions/' + file); - if (!action) return true; - // exports.actions.push(action); + require(__dirname + '/actions/' + file); + }); + + _.each(exports.actions, function runSetup (action) { setup = action.setup; if (setup && _.isFunction(setup)) setup(function (error, data) { console.log('Running setup of ' + file); @@ -40,6 +36,7 @@ exports.setup = function (callback) { log.info('Success running setup function of action' + data.name); } }); + }); log.info('bot setup complete'); @@ -47,12 +44,24 @@ exports.setup = function (callback) { }); }; -exports.processRequest = function (request, response) { - var actionFound, commands, input, outgoingData, pipedResponse, regex, requestText, responseMethod, responseText, VARIABLES; - +exports.processRequest = function processRequest (request, response) { + var + actionFound, + commands, + input, + outgoingData, + pipedResponse, + regex, + requestText, + responseMethod, + responseText, + VARIABLES + ; + input = request.body.text; - // The keys on this object will + // The keys on this object will be replaced with their corresponding values + // at runtime. VARIABLES = { 'HERE': '#' + request.body.channel_name, 'ME': '@' + request.body.user_name, @@ -143,6 +152,11 @@ exports.processRequest = function (request, response) { pipedResponse = null; } + // User is ending their command with the `>`. Assume current room. + if (command.redirects && !command.redirectTo.length) { + command.redirectTo.push({ type: 'channel', name: request.body.channel_name }); + } + // If the response should be redirected, then do so if (command.redirectTo.length > 0) { _.each(command.redirectTo, function (redirect) { @@ -152,11 +166,11 @@ exports.processRequest = function (request, response) { break; case 'channel': - exports.sendMessage(responseText, '#' + redirect.name); + exports.sendMessage(responseText, '#' + redirect.name); break; case 'group': - exports.sendMessage(responseText, '#' + redirect.name); + exports.sendMessage(responseText, '#' + redirect.name); break; case 'file': @@ -182,7 +196,7 @@ exports.processRequest = function (request, response) { function formatResponse (response) { return (request.body.trigger_word) ? JSON.stringify({text: response}) : response; - } + } }; exports.addAction = function (action) { @@ -206,14 +220,19 @@ exports.addAction = function (action) { exports.sendMessage = function (message, channel, callback) { callback = callback || function () {}; var messageData = { - token: config.token.robot, + token: config.token.user, channel: channel, text: message }; - https.get('https://slack.com/api/chat.postMessage?' + querystring.stringify(messageData), function (response) { + var url = 'https://slack.com/api/chat.postMessage?' + querystring.stringify(messageData); + https.get(url, function (response) { response.on('end', function () { - callback(response.error, response); + callback(response.error, response); }); + + response.on('error', function (error) { + console.error(error); + }) }).end(); }; diff --git a/config.json b/config.json deleted file mode 100644 index e848e53..0000000 --- a/config.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "port": 8421, - "logs": "logs", - "token": { - "api": "Set your API token here", - "robot": "Set your access token for your robot user here.", - "webhook": "Set your incoming webhook token here" - }, - "domain": "SlackTeam", - "hostname": "https://SlackTeam.slack.com", - "path": "/services/hooks/incoming-webhook?token=[REDACTED]", - "keys": [ - "Create a unique access key and make sure to set it as a URL parameter in your Slack outgoing-webhook URL" - ], - "timeout": 8000 -} diff --git a/library/config.js b/library/config.js new file mode 100644 index 0000000..0b820f5 --- /dev/null +++ b/library/config.js @@ -0,0 +1,22 @@ +"use strict"; + +var fs = require('fs'); +var config; + +var configExists = fs.existsSync(__dirname + '/../config.json'); + +if (!configExists) { + config = { + "port": 8421, + "logs": "logs", + "token": { + "slashCommand": process.env.TOKEN_SLASH_COMMAND, + "user": process.env.TOKEN_USER + }, + "timeout": 8000 + }; +} else { + config = require(__dirname + '/../config.json'); +} + +module.exports = config; diff --git a/library/log.js b/library/log.js index 8a56f38..5b410a8 100644 --- a/library/log.js +++ b/library/log.js @@ -1,7 +1,7 @@ var cluster = require('cluster') var fs = require('fs') -var config = require(__dirname + '/../config.json') +var config = require(__dirname + '/config') function format(type, message, data, id) { var speaker = cluster.isMaster ? 'master' : 'worker' + process.pid @@ -25,7 +25,7 @@ function format(type, message, data, id) { stringifiedData = JSON.stringify(dataObject); } - return JSON.stringify(stringifiedData) + return stringifiedData } function writeToFile(fileName, dataString) { diff --git a/library/parse.js b/library/parse.js index 7c4d6a0..57fbe84 100644 --- a/library/parse.js +++ b/library/parse.js @@ -15,7 +15,7 @@ var slackEncoding = { } var slackChars = Object.keys(slackEncoding) -exports.httpParameters = function(parameters) { +exports.httpParameters = function httpParameters (parameters) { var parameters = parameters.split('&') var parametersObject = {} @@ -27,7 +27,7 @@ exports.httpParameters = function(parameters) { return parametersObject } -exports.slackText = function(encodedText) { +exports.slackText = function slackText (encodedText) { var decodedText = '' while (encodedText) { @@ -45,7 +45,7 @@ exports.slackText = function(encodedText) { if (encodedText[0] === '%') { try { decodedText += decodeURIComponent(encodedText.substring(0, 3)) - encodedText = encodedText.substring(3, encodedText.length) + encodedText = encodedText.substring(3, encodedText.length) } catch (exception) { decodedText += encodedText[0] encodedText = encodedText.substring(1, encodedText.length) @@ -66,7 +66,7 @@ exports.slackText = function(encodedText) { * @param {String} input String of commands * @return {Array} Array of tokens, including commands, arguments, switches, resources, etc. */ -function commands (tokens, input) { + exports.commands = function commands (tokens, input) { var commandId = ''; if (typeof tokens === 'string') { input = tokens; @@ -89,7 +89,7 @@ function commands (tokens, input) { } else if (quotes === 'single') { boundary = true; quotes = ''; - } + } break; case '"': @@ -98,7 +98,7 @@ function commands (tokens, input) { } else if (quotes === 'double') { boundary = true; quotes = ''; - } + } break; case '\\': @@ -125,8 +125,9 @@ function commands (tokens, input) { break; case '-': - boundary = (state !== 'switch'); - state = 'switch'; + if (boundary) { + state = 'switch'; + } break; default: @@ -135,7 +136,7 @@ function commands (tokens, input) { if (tokens.length === 0) state = 'command'; else if(tokens[tokens.length - 1].value === '>') state = 'resource'; else state = 'command'; - } + } break; } @@ -152,7 +153,7 @@ function commands (tokens, input) { index++; } return normalize(tokens); -} +}; function normalize (commands) { var normalizedCommands = []; @@ -174,9 +175,10 @@ function normalize (commands) { arguments: [], switches: [], pipe: false, + redirects: false, redirectTo: [] }; - + normalizedCommands.push(command); thisCommand = _.find(normalizedCommands, {id: token.id}); break; @@ -198,12 +200,13 @@ function normalize (commands) { case 'operator': if (token.value === '|') thisCommand.pipe = true; + if (token.value === '>') thisCommand.redirects = true; break; case 'resource': if (resourceTypes[token.value.substring(0,1)]) thisCommand.redirectTo.push({type: resourceTypes[token.value.substring(0,1)] , name: token.value.substring(1)}); - else + else thisCommand.redirectTo.push({type: 'file' , name: token.value}); break; @@ -216,7 +219,7 @@ function normalize (commands) { return normalizedCommands; } -/** +/** * Creates a parsing Error and associated formatted message * @param {String} error Error message to be displayed to the user * @param {String} input String that was to be parsed when the error was thrown @@ -238,5 +241,3 @@ function throwParseError (error, input, index) { throw new Error(errorText); } - -exports.commands = commands; diff --git a/master.js b/master.js index ca3da7e..05bf903 100644 --- a/master.js +++ b/master.js @@ -1,7 +1,7 @@ var cluster = require('cluster') var os = require('os') -var config = require(__dirname + '/config.json') +var config = require(__dirname + '/library/config') var worker = require(__dirname + '/worker.js') var log = require(__dirname + '/library/log.js') @@ -14,7 +14,7 @@ if (cluster.isMaster) { } log.info('application started') - console.log('application started on port:', config.port) + console.log('application started on port:', process.env.PORT || config.port) var cores = os.cpus().length for (var x = 0; x < cores; x++) diff --git a/package.json b/package.json index 5a8cb78..2981b45 100644 --- a/package.json +++ b/package.json @@ -7,30 +7,35 @@ "node": "~0.10.28" }, "scripts": { - "test": "mocha tests" + "start": "node master.js" }, "author": { "name": "Chris Young", + "github": "https://github.com/chris--young", "email": "cyoung@mobiquityinc.com" }, "contributors": [ { "name": "Jason Kleinberg", - "email": "jkleinberg@mobiquityinc.com" + "github": "https://github.com/Ustice", + "email": "ustice@gmail.com" }, { "name": "Firoze Rakib", + "github": "https://github.com/firoze", "email": "frakib@mobiquityinc.com" }, { "name": "Andrew Crites", + "github": "https://github.com/ajcrites", "email": "acrites@mobiquityinc.com" } ], "license": "ISC", "dependencies": { "lodash": "~2.4.1", - "slack-node": "0.0.9" + "slack-node": "0.0.9", + "jquerygo": "0.0.16" }, "devDependencies": { "mocha": "~1.20.1" diff --git a/router.js b/router.js index b78a1c4..a0702bb 100644 --- a/router.js +++ b/router.js @@ -25,7 +25,7 @@ module.exports = function(request, response) { default: response.statusCode = 404 response.end() - + log.error('resource not found', request.url.pathname, request.id) } } diff --git a/sample-config.json b/sample-config.json new file mode 100644 index 0000000..ef94d9b --- /dev/null +++ b/sample-config.json @@ -0,0 +1,9 @@ +{ + "port": 80, + "logs": "logs", + "token": { + "slashCommand": "[Enter your Slash Command token here.]", + "user": "[Enter your User token here.]" + }, + "timeout": 8000 +} diff --git a/worker.js b/worker.js index a718988..45f5f14 100644 --- a/worker.js +++ b/worker.js @@ -3,7 +3,7 @@ var http = require('http') var domain = require('domain') var url = require('url') -var config = require(__dirname + '/config.json') +var config = require(__dirname + '/library/config') var router = require(__dirname + '/router.js') var bot = require(__dirname + '/bot.js') @@ -26,6 +26,7 @@ module.exports = function() { serverDomain.on('error', function(error) { log.error('uncaught exception', error, response.id) + console.error(error.stack); try { var kill = setTimeout(function() { @@ -39,6 +40,7 @@ module.exports = function() { response.end() } catch (exception) { log.error('failed to respond after uncaught exception', exception, response.id) + console.error(exception.stack); } }) @@ -48,6 +50,7 @@ module.exports = function() { request.on('error', function(error) { log.error('request error', error, request.id) + console.error(error); response.statusCode = 500 response.end() @@ -69,54 +72,40 @@ module.exports = function() { log.info('headers', request.headers, request.id) - var key = request.url.parameters.key; - if (key) { - if (~config.keys.indexOf(key)) { - if (request.body) { - try { - request.body = parse.httpParameters(request.body) - log.info('body', request.body, request.id) - } catch (exception) { - log.error('failed to parse request body', exception, request.id) - response.statusCode = 415 - response.end() - return - } - } - request.data = {} - - if (request.body.token) { - if ( - (request.body.trigger_word && request.body.token === config.token.webhook ) || - (request.body.command && request.body.token === config.token.api) - ) - router(request, response) - else { - log.error('invalid token', request.body.token, request.id) - response.statusCode = 403 - response.end() - } - } else { - log.error('missing token', {}, request.id) - response.statusCode = 403 - response.end() - } + if (request.body) { + try { + request.body = parse.httpParameters(request.body) + log.info('body', request.body, request.id) + } catch (exception) { + log.error('failed to parse request body', exception, request.id) + response.statusCode = 415 + response.end() + return + } + } else if(request.method.toLowerCase() === 'get') { + response.end('Slacker is running.'); + } + request.data = {} + + if (request.body.token) { + if (request.body.command && request.body.token === config.token.slashCommand) { + router(request, response); } else { - log.error('invalid key', key, request.id) - response.statusCode = 401 + log.error('invalid token', request.body.token, request.id) + response.statusCode = 403 response.end() } } else { - log.error('missing key', {}, request.id) - response.statusCode = 401 + log.error('missing token', {}, request.id) + response.statusCode = 403 response.end() } }) }) }) - - server.listen(config.port, function() { - log.info('listening on port ' + config.port) + + server.listen(process.env.PORT || config.port, function() { + log.info('listening on port ' + process.env.PORT || config.port) }) }) }