├── .editorconfig ├── .gitignore ├── LICENSE.md ├── README.md ├── package.json ├── run_server.sh └── src ├── bot.js ├── commands ├── about.js ├── enterprisejs-tips.js ├── excuse.js ├── time.js └── weather.js ├── config_template.js ├── cronbot.js ├── index.js ├── servicehooks ├── groupme.js └── travis.js └── utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/config.js 2 | 3 | # JetBrains Directory-based project format 4 | .idea/ 5 | 6 | # Logs 7 | logs 8 | *.log 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # Commenting this out is preferred by some people, see 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 30 | node_modules 31 | 32 | # Users Environment Variables 33 | .lock-wscript 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 [Neelabh Gupta](http://neelabhgupta.com/) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | groupme-bot 2 | =========== 3 | A bot for [GroupMe](https://groupme.com/) (https://dev.groupme.com/tutorials/bots). 4 | Based on the [Sample GroupMe NodeJS Callback Bot](https://github.com/groupme/bot-tutorial-nodejs). 5 | 6 | This bot can respond to commands sent on multiple groups, and can also be used to publish messages on a regular schedule. 7 | A lot of the commands are setup for my groups, but it is easy to extend this bot and add your own commands. 8 | Feel free to [fork](https://help.github.com/articles/fork-a-repo/) this repository. 9 | Consider [contributing](https://guides.github.com/activities/contributing-to-open-source/#contributing) your changes/additional commands. 10 | 11 | I recommend taking a look at [Hubot](https://hubot.github.com/) - it is written by GitHub, Inc., supports multiple 12 | chat-backends, is very extensible, and has a huge community contributing lots of useful scripts. It does not officially support 13 | GroupMe, but [there is an adapter available](https://github.com/AdamEdgett/hubot-groupme). 14 | 15 | Configuring 16 | ----------- 17 | 1. [Create a GroupMe Bot](https://github.com/groupme/bot-tutorial-nodejs#next-create-a-groupme-bot). 18 | For every group you want to interact with, you need to register a new GroupMe bot. For the callback URL, pass the URL of the server where this application is hosted. 19 | 20 | 2. [Find your Bot ID](https://github.com/groupme/bot-tutorial-nodejs#find-your-bot-id) and save for later. Also save the group ID and optionally the group name. 21 | 22 | 3. Copy [`config_template.js`](src/config_template.js) to `config.js`. Make sure to keep `config.js` private/hidden, as it will hold secret data and should not be publicly visible. 23 | If you are using Git, it is already added to the [`.gitignore`](.gitignore). DO NOT ADD CONFIGURATION DATA TO [`config_template.js`](src/config_template.js), which is meant to be in source control. 24 | It should only be used to create `config.js`. 25 | 26 | 4. Open `config.js` and edit the `config.bots` array. For every group you have a bot for, add the following to the array: 27 | ```js 28 | { 29 | botID: 'xxxxxxxxxx', 30 | groupID: 'xxxxxxxx', 31 | groupLocalID: '1', // an identifier for this group used in this application only 32 | botName: 'mybot', 33 | groupName: 'the-group-name' // optional 34 | } 35 | ``` 36 | * Give every bot a `groupLocalID`. This can be any identifier, which can later be used to send messages to the group. 37 | The advantage of this approach is that all the GroupMe specific configuration and secrets remain in the `config.js` file, 38 | and the source code only contains the `groupLocalID`, which is meaningless outside this application. 39 | * The `botName` is the name which the bot responds to. All commands to the bot in the group must start with the given `botName`. 40 | * You can optionally store other data along with the bot, such as the group name. 41 | 42 | 5. Add any other configuration/secret API keys to `config.js`, and access them in your scripts. 43 | For example, one of the commands is for the bot to tell the current weather. 44 | The weather is fetched from the Yahoo Weather API, and its secret App ID is stored in `config.js` as `config.yahooWeatherAppId`. 45 | 46 | Running 47 | ------- 48 | 1. Install [node.js](https://nodejs.org/). Node's package manager ([npm](https://www.npmjs.org/)) comes bundled. 49 | 2. The file [`run_server.sh`](run_server.sh) is a simple [shell script](https://en.wikipedia.org/wiki/Shell_script) which sets up the project directory and runs the server. 50 | Make sure to read its contents carefully before executing it. You might also want to edit it to suit your needs. Once ready, 51 | execute the run script (it must be run from the project's root directory): 52 | ```sh 53 | $ cd 54 | $ ./run_server.sh 55 | ``` 56 | 57 | Adding your own commands 58 | ------------------------ 59 | Commands are automatically picked up from the [`commands/`](src/commands/) directory. To add your own command, create a JS module and place it in that directory. 60 | The module should export a single function which accepts as its parameter a command registration callback. This callback should be called with the 61 | command name, its description and the command function to register the command. 62 | 63 | The command function is called when the bot receives a message with the given command name. The command function will be called with four parameters: 64 | * the group's local ID (see item 4 in the [Configuring](#configuring) section) 65 | * the sending user's display name (may be empty - such as when a command is triggered as a scheduled message and not from a message sent by a user) 66 | * an array of words in the message following the command (used as command arguments) 67 | * a callback function, which should be called with the response 68 | 69 | A template for a command module is: 70 | ```js 71 | module.exports = function (registerCommand) { 72 | registerCommand( 73 | 'command', 74 | 'description', 75 | function (groupLocalID, userDisplayName, msgTokens, callback) { 76 | var response = '...'; 77 | callback(response); 78 | } 79 | ); 80 | }; 81 | ``` 82 | Look at existing modules in the [`commands/`](src/commands/) directory for examples. 83 | 84 | License 85 | ------- 86 | MIT Licensed. See [LICENSE.md](LICENSE.md). 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "groupme-bot", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "A bot for GroupMe (https://dev.groupme.com/tutorials/bots)", 6 | "main": "src/index.js", 7 | "scripts": { 8 | "start": "node src/index.js" 9 | }, 10 | "dependencies": { 11 | "director": "", 12 | "notevil": "^1.0.0", 13 | "weather": "^0.1.0" 14 | }, 15 | "engines": { 16 | "node": "0.10.x" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/neelabhg/groupme-bot" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /run_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Script to run the server. Should be run from the project's root directory. 3 | 4 | # Get latest code from repository (optional, comment out if not needed) 5 | git pull 6 | 7 | # Install/update dependencies 8 | npm install 9 | 10 | # Create the logs/ directory 11 | mkdir -p -v logs/ 12 | 13 | # Start the server. The default port used is 5000. If you want to use another port, 14 | # set the PORT environment variable to the desired port number. 15 | # 16 | # nohup is used to keep the process running even after the terminal/ssh session is closed. 17 | # 18 | # Any non-error output (including console.log) will be output in logs/server-stdout.txt, 19 | # and errors will be output in logs/server-stderr.txt 20 | nohup node src/index.js 1> logs/server-stdout.txt 2> logs/server-stderr.txt & 21 | -------------------------------------------------------------------------------- /src/bot.js: -------------------------------------------------------------------------------- 1 | var HTTPS = require('https'); 2 | var path = require('path'); 3 | var config = require('./config'); 4 | var bot = {}; 5 | 6 | var groupIdToBotMap = {}, groupLocalIdToBotMap = {}; 7 | config.bots.forEach(function (bot) { 8 | groupIdToBotMap[bot.groupID] = bot; 9 | groupLocalIdToBotMap[bot.groupLocalID] = bot; 10 | }); 11 | 12 | var commands = []; 13 | var registerCommand = function (command, description, func) { 14 | commands[command] = [description, func]; 15 | }; 16 | 17 | // http://stackoverflow.com/a/5365577 (node.js require all files in a folder?) 18 | // And http://stackoverflow.com/questions/5364928/node-js-require-all-files-in-a-folder#comment25520686_5365577 19 | var normalizedPath = path.join(__dirname, "commands"); 20 | require("fs").readdirSync(normalizedPath).forEach(function (file) { 21 | if (path.extname(file) === '.js') { 22 | require("./commands/" + file)(registerCommand); 23 | } 24 | }); 25 | 26 | bot.respond = function (request) { 27 | var botConfig, msg, botName, groupID; 28 | if (!(typeof request === 'object' && request)) { 29 | console.log('Invalid bot request:', request); 30 | return; 31 | } 32 | 33 | groupID = request.group_id; 34 | if (!(typeof groupID === 'string' && groupID)) { 35 | console.log('Invalid bot request:', request); 36 | return; 37 | } 38 | 39 | botConfig = groupIdToBotMap[groupID]; 40 | if (!(typeof botConfig === 'object' && botConfig)) { 41 | console.log('New message received, but no bot configured for Group ID ' + groupID); 42 | return; 43 | } 44 | 45 | botName = botConfig.botName.toLowerCase(); 46 | msg = request.text; 47 | console.log('New message from %s in group \'%s\': %s', request.name, botConfig.groupLocalID, msg); 48 | 49 | if (typeof msg === 'string') { 50 | msg = msg.toLowerCase(); 51 | if (msg === botName) { 52 | msg = botName + ' help'; 53 | } 54 | if (msg.substring(0, botName.length + 1) === botName + ' ') { 55 | msg = msg.substring(botName.length + 1); 56 | this.processCommand(botConfig.groupLocalID, request.name, msg, function (response) { 57 | if (response !== null) { 58 | this.postMessageWithBotID(botConfig.botID, response); 59 | } 60 | }.bind(this)); 61 | } 62 | } 63 | }; 64 | 65 | bot.processCommand = function (groupLocalID, userDisplayName, message, cb) { 66 | var tokens = message.split(' '), 67 | commandString = tokens.shift(); 68 | 69 | if (!commandString) { 70 | cb(null); 71 | return; 72 | } 73 | 74 | if (commandString === 'help') { 75 | var helpText = 'Usage: bot [optional arguments]\n' + 76 | 'Available commands:\nhelp: Display this text\n'; 77 | for (var key in commands) { 78 | if (commands.hasOwnProperty(key)) { 79 | helpText += commands[key][0] + '\n'; 80 | } 81 | } 82 | cb(helpText); 83 | return; 84 | } 85 | 86 | var command = commands[commandString]; 87 | if (!command) { 88 | cb('Command \'' + commandString + '\' not supported. Type \'bot help\' for available commands.'); 89 | return; 90 | } 91 | 92 | command[1](groupLocalID, userDisplayName, tokens, cb); 93 | }; 94 | 95 | bot.postMessageWithGroupLocalID = function (groupLocalID, text) { 96 | var botID = (groupLocalIdToBotMap[groupLocalID] || {}).botID; 97 | this.postMessageWithBotID(botID, text); 98 | }; 99 | 100 | bot.postMessageWithBotID = function (botID, text) { 101 | var options, body, botReq; 102 | 103 | options = { 104 | hostname: 'api.groupme.com', 105 | path: '/v3/bots/post', 106 | method: 'POST' 107 | }; 108 | 109 | body = { 110 | "bot_id" : botID, 111 | "text" : text 112 | }; 113 | 114 | console.log('sending ' + text + ' to ' + botID); 115 | 116 | botReq = HTTPS.request(options, function (res) { 117 | if (res.statusCode === 202) { 118 | //neat 119 | } else { 120 | console.log('rejecting bad status code ' + res.statusCode); 121 | } 122 | }); 123 | 124 | botReq.on('error', function(err) { 125 | console.log('error posting message ' + JSON.stringify(err)); 126 | }); 127 | botReq.on('timeout', function(err) { 128 | console.log('timeout posting message ' + JSON.stringify(err)); 129 | }); 130 | botReq.end(JSON.stringify(body)); 131 | }; 132 | 133 | module.exports = bot; 134 | -------------------------------------------------------------------------------- /src/commands/about.js: -------------------------------------------------------------------------------- 1 | module.exports = function (registerCommand) { 2 | registerCommand('about', 'about: About me!', function (groupLocalID, userDisplayName, msgTokens, callback) { 3 | callback('I\'m a bot. https://github.com/neelabhg/groupme-bot'); 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /src/commands/enterprisejs-tips.js: -------------------------------------------------------------------------------- 1 | var getHttp = require('../utils').getHttp; 2 | var safeEval = require('notevil'); 3 | 4 | var getRandomEnterpriseJsTip = (function () { 5 | var tips = []; 6 | 7 | // get the tips from https://github.com/bentruyman/enterprise-js 8 | // and save them in this closure 9 | getHttp({ 10 | hostname: 'raw.githubusercontent.com', 11 | port: 443, 12 | path: '/bentruyman/enterprise-js/master/tips.js', 13 | method: 'GET' 14 | }, function onResult(statusCode, data) { 15 | var safeEvalContext = { module: {} }; 16 | if (statusCode == 200) { 17 | safeEval(data, safeEvalContext); 18 | // the tips.js file exports an array 19 | tips = safeEvalContext.module.exports.map(function (tip) { 20 | var msg = '@' + tip.author + ' says Enterprise JavaScript is: '; 21 | // remove html tags from the message (taken from http://stackoverflow.com/a/5002161) 22 | msg += tip.message.replace(/<\/?[^>]+(>|$)/g, ""); 23 | if (tip.example) { 24 | msg += '\nFor example:\n' + tip.example.join('\n'); 25 | } 26 | return msg; 27 | }); 28 | } 29 | }, function onError() { 30 | }); 31 | 32 | return function (cb) { 33 | cb(tips[Math.floor(Math.random() * tips.length)]); 34 | }; 35 | })(); 36 | 37 | module.exports = function (registerCommand) { 38 | registerCommand( 39 | 'enterprisejs', 40 | 'enterprisejs: Get a random funny tip for writing "enterprise" quality JS', 41 | function (groupLocalID, userDisplayName, msgTokens, callback) { 42 | getRandomEnterpriseJsTip(function (tip) { 43 | if (tip) { 44 | callback(tip); 45 | } else { 46 | callback('error getting data'); 47 | } 48 | }); 49 | } 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/commands/excuse.js: -------------------------------------------------------------------------------- 1 | var getHttp = require('../utils').getHttp; 2 | 3 | var getRandomDeveloperExcuse = function (cb) { 4 | getHttp({ 5 | hostname: 'developerexcuses.com', 6 | path: '/', 7 | method: 'GET' 8 | }, function onResult(statusCode, data) { 9 | // adapted from https://github.com/github/hubot-scripts/blob/master/src/scripts/excuse.coffee#L55 10 | var matches = data.match(/]+>(.+)<\/a>/i); 11 | if (matches && matches[1]) { 12 | cb(matches[1]); 13 | } else { 14 | cb(''); 15 | } 16 | }, function onError() { 17 | cb(''); 18 | }); 19 | }; 20 | 21 | var getRandomDesignerExcuse = (function () { 22 | // Taken from http://designerexcuses.com/js/excuses.js 23 | var quotes = [ 24 | "That won’t fit the grid.", 25 | "That’s not in the wireframes.", 26 | "That’s a developer thing.", 27 | "I didn’t mock it up that way.", 28 | "The developer must have changed it.", 29 | "Did you try hitting refresh?", 30 | "No one uses IE anyway.", 31 | "That’s not how I designed it.", 32 | "That’s way too skeuomorphic.", 33 | "That’s way too flat.", 34 | "Just put a long shadow on it.", 35 | "It wasn’t designed for that kind of content.", 36 | "Josef Müller-Brockmann.", 37 | "That must be a server thing.", 38 | "It only looks bad if it’s not on Retina.", 39 | "Are you looking at it in IE or something?", 40 | "That’s not a recognised design pattern.", 41 | "It wasn’t designed to work with this content.", 42 | "The users will never notice that.", 43 | "The users might not notice it, but they’ll feel it.", 44 | "These brand guidelines are shit.", 45 | "You wouldn’t get it, it's a design thing.", 46 | "Jony wouldn’t do it like this.", 47 | "That’s a dark pattern.", 48 | "I don’t think that’s very user friendly.", 49 | "That’s not what the research says.", 50 | "I didn’t get a change request for that.", 51 | "No, that would break the vertical rhythm.", 52 | "Why’s this type so ugly? Did a developer do it?", 53 | "Because that’s not my design style.", 54 | "If the user can’t figure this out, they’re an idiot.", 55 | "Ever heard of apostrophes?", 56 | "It looked fine in the mockups.", 57 | "Just put some gridlines on it.", 58 | "No, I didn’t test it on Firefox. I don’t install that trash on my Mac.", 59 | "If they don’t have JavaScript turned on, it’s their own damn fault.", 60 | "I don’t care if they don’t have a recent browser, this is 2013!", 61 | "It’s a responsive layout, of course it has widows." 62 | ]; 63 | 64 | return function (cb) { 65 | cb(quotes[Math.floor(Math.random() * quotes.length)]); 66 | }; 67 | })(); 68 | 69 | module.exports = function (registerCommand) { 70 | registerCommand( 71 | 'excuse', 72 | 'excuse [designer]: Get a random developer or designer excuse', 73 | function (groupLocalID, userDisplayName, msgTokens, callback) { 74 | if (msgTokens[0] === 'designer') { 75 | getRandomDesignerExcuse(function (excuse) { 76 | if (excuse) { 77 | callback('Designer excuse: ' + excuse); 78 | } 79 | }); 80 | } else { 81 | getRandomDeveloperExcuse(function (excuse) { 82 | if (excuse) { 83 | callback('Developer excuse: ' + excuse); 84 | } 85 | }); 86 | } 87 | } 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /src/commands/time.js: -------------------------------------------------------------------------------- 1 | module.exports = function (registerCommand) { 2 | registerCommand('time', 'time: Get the current time', function (groupLocalID, userDisplayName, msgTokens, callback) { 3 | callback('The current time is ' + (new Date()).toString()); 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /src/commands/weather.js: -------------------------------------------------------------------------------- 1 | var weather = require('weather'); 2 | var yahooWeatherAppId = require('../config').yahooWeatherAppId; 3 | 4 | var getWeatherForCity = function (city, callback) { 5 | weather.error = function (msg) { 6 | console.log('Error getting weather for city %s: %s', city, msg); 7 | callback({ 8 | status: 'error', 9 | errorMsg: msg 10 | }); 11 | }; 12 | weather({location: city, appid: yahooWeatherAppId, logging: true}, function(data) { 13 | data['status'] = 'success'; 14 | callback(data); 15 | }); 16 | }; 17 | 18 | module.exports = function (registerCommand) { 19 | registerCommand( 20 | 'weather', 21 | 'weather : Get the current weather for city', 22 | function (groupLocalID, userDisplayName, msgTokens, callback) { 23 | var city = msgTokens[0]; 24 | if (city) { 25 | getWeatherForCity(city, function (data) { 26 | if (data.status == 'success') { 27 | callback('The current temperature in ' + city + ' is ' + data.temp); 28 | } else { 29 | callback('Cannot get weather at this time'); 30 | } 31 | }); 32 | } else { 33 | callback('Please provide a city for weather.'); 34 | } 35 | } 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/config_template.js: -------------------------------------------------------------------------------- 1 | var config = {}; 2 | 3 | config.bots = [ 4 | { 5 | botID: 'xxxxxxxxxx', 6 | groupID: 'xxxxxxxx', 7 | groupLocalID: '1', // an identifier for this group used in this application only 8 | botName: 'mybot', 9 | groupName: 'the-group-name' // optional 10 | } 11 | ]; 12 | 13 | config.yahooWeatherAppId = ''; 14 | 15 | // Travis CI user token 16 | config.travisUserToken = 'xxxxxxxxxxxxxx'; 17 | 18 | module.exports = config; 19 | -------------------------------------------------------------------------------- /src/cronbot.js: -------------------------------------------------------------------------------- 1 | // Messages sent on a regular schedule 2 | var bot = require('./bot'); 3 | 4 | var send = function (groupLocalID, msg) { 5 | bot.processCommand(groupLocalID, '', msg, function (response) { 6 | if (response) { 7 | bot.postMessageWithGroupLocalID(groupLocalID, 'Today\'s random ' + response); 8 | } 9 | process.exit(); 10 | }); 11 | }; 12 | 13 | if (Math.random() < 0.5) { 14 | send('1', 'excuse'); 15 | } else { 16 | send('1', 'excuse designer'); 17 | } 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var http, director, path, router, server, port; 2 | 3 | // http://stackoverflow.com/a/15303236/2193410 (Node.js - check if module is installed without actually requiring it) 4 | try { 5 | require.resolve('./config'); 6 | } catch (e) { 7 | console.error('Configuration file config.js not found.'); 8 | process.exit(e.code); 9 | } 10 | 11 | http = require('http'); 12 | director = require('director'); 13 | path = require('path'); 14 | 15 | router = new director.http.Router({ 16 | '/' : { 17 | get: function () { 18 | this.res.writeHead(200); 19 | this.res.end('

I\'m a bot. https://github.com/neelabhg/groupme-bot

'); 20 | } 21 | } 22 | }); 23 | 24 | var registerRoute = function (httpVerb, route, handler) { 25 | if (['get', 'post'].indexOf(httpVerb) === -1) { 26 | console.log('registerRoute: Incorrect router method \'%s\' for route \'%s\'', httpVerb, route); 27 | return; 28 | } 29 | router[httpVerb](route, function () { 30 | this.res.writeHead(200); 31 | handler(this.req.headers, this.req.body || this.req.chunks.join('')); 32 | this.res.end(); 33 | }); 34 | }; 35 | 36 | // http://stackoverflow.com/a/5365577 (node.js require all files in a folder?) 37 | // And http://stackoverflow.com/questions/5364928/node-js-require-all-files-in-a-folder#comment25520686_5365577 38 | var normalizedPath = path.join(__dirname, "servicehooks"); 39 | require("fs").readdirSync(normalizedPath).forEach(function (file) { 40 | if (path.extname(file) === '.js') { 41 | require("./servicehooks/" + file)(registerRoute); 42 | } 43 | }); 44 | 45 | server = http.createServer(function (req, res) { 46 | req.chunks = []; 47 | req.on('data', function (chunk) { 48 | req.chunks.push(chunk.toString()); 49 | }); 50 | 51 | router.dispatch(req, res, function(err) { 52 | res.writeHead(err.status, {"Content-Type": "text/plain"}); 53 | res.end(err.message); 54 | }); 55 | }); 56 | 57 | port = Number(process.env.PORT || 5000); 58 | server.listen(port); 59 | -------------------------------------------------------------------------------- /src/servicehooks/groupme.js: -------------------------------------------------------------------------------- 1 | var bot = require('../bot'); 2 | 3 | module.exports = function (registerRoute) { 4 | registerRoute('post', '/groupme', function (headers, requestBody) { 5 | bot.respond(requestBody); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /src/servicehooks/travis.js: -------------------------------------------------------------------------------- 1 | // http://docs.travis-ci.com/user/notifications/#Webhook-notification 2 | 3 | var bot = require('../bot'); 4 | var config = require('../config'); 5 | var crypto = require('crypto'); 6 | var util = require('util'); 7 | 8 | var isAuthorizedRequest = function (headers) { 9 | var digest = crypto.createHash('sha256').update(headers['travis-repo-slug'] + config.travisUserToken).digest('hex'); 10 | return digest === headers['authorization']; 11 | }; 12 | 13 | var getChatMessageText = function (payload) { 14 | var commit_details; 15 | if (payload.type === 'pull_request') { 16 | commit_details = util.format('pull-request #%d by %s', payload.pull_request_number, payload.author_name); 17 | } else { 18 | commit_details = util.format('commit to %s by %s', payload.branch, payload.author_name); 19 | } 20 | return util.format('Travis CI - %s - build #%d (%s): %s.\nBuild url: %s', 21 | payload.repository.name, payload.number, commit_details, payload.status_message, payload.build_url); 22 | }; 23 | 24 | module.exports = function (registerRoute) { 25 | registerRoute('post', '/travisci', function (headers, requestBody) { 26 | var payload; 27 | if (!(typeof headers === 'object' && headers && typeof requestBody === 'object' && requestBody)) { 28 | console.log('Invalid POST request for route /travisci'); 29 | return; 30 | } 31 | 32 | if (!isAuthorizedRequest(headers)) { 33 | console.log('Travis CI hook: unauthorized payload request for repository', headers['travis-repo-slug']); 34 | return; 35 | } 36 | 37 | try { 38 | payload = JSON.parse(requestBody.payload); 39 | } catch (e) { 40 | console.log('Travis hook error while parsing JSON:', e); 41 | console.log('Travis CI notification hook request body:', requestBody); 42 | return; 43 | } 44 | if (!payload) { 45 | return; 46 | } 47 | 48 | bot.postMessageWithGroupLocalID('2', getChatMessageText(payload)); 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var https = require('https'); 3 | 4 | var utils = {}; 5 | 6 | // http://stackoverflow.com/a/9577651 (HTTP GET Request in Node.js Express) 7 | utils.getHttp = function(options, onResult, onError) { 8 | var protocol = options.port == 443 ? https : http; 9 | var req = protocol.request(options, function(res) { 10 | var output = ''; 11 | //console.log(options.host + ':' + res.statusCode); 12 | res.setEncoding('utf8'); 13 | 14 | res.on('data', function (chunk) { 15 | output += chunk; 16 | }); 17 | 18 | res.on('end', function() { 19 | //var obj = JSON.parse(output); 20 | onResult(res.statusCode, output); 21 | }); 22 | }); 23 | 24 | req.on('error', function(err) { 25 | onError(err.message); 26 | }); 27 | 28 | req.end(); 29 | }; 30 | 31 | module.exports = utils; 32 | --------------------------------------------------------------------------------