├── .gitignore ├── app.json ├── gitter.js ├── package.json ├── bin.js ├── readme.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | testenv -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitter-irc-bot", 3 | "description": "A bot for synchronising messages from gitter and irc", 4 | "repository": "https://github.com/finnp/gitter-irc-bot", 5 | "keywords": [ 6 | "gitter", 7 | "irc", 8 | "synchronization", 9 | "bot" 10 | ], 11 | "env": { 12 | "GITTERBOT_APIKEY": "get it from https://developer.gitter.im/apps", 13 | "GITTERBOT_GITTER_ROOM": "user/room", 14 | "GITTERBOT_IRC_CHANNEL": "#", 15 | "GITTERBOT_IRC_NICK": "", 16 | "GITTERBOT_IRC_SERVER": "irc.freenode.net", 17 | "GITTERBOT_IRC_ADMIN": "", 18 | "GITTERBOT_IRC_OPTS": "{}", 19 | "HEROKU_URL": "http://.herokuapp.com" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /gitter.js: -------------------------------------------------------------------------------- 1 | var faye = require('faye') 2 | 3 | module.exports = getClient 4 | 5 | function getClient (token) { 6 | // Authentication extension 7 | 8 | var ClientAuthExt = function () {} 9 | 10 | ClientAuthExt.prototype.outgoing = function (message, callback) { 11 | if (message.channel === '/meta/handshake') { 12 | if (!message.ext) { message.ext = {} } 13 | message.ext.token = token 14 | } 15 | 16 | callback(message) 17 | } 18 | 19 | // faye client 20 | 21 | var client = new faye.Client('https://ws.gitter.im/faye', {timeout: 60, retry: 5, interval: 1}) 22 | 23 | // Add Client Authentication extension 24 | client.addExtension(new ClientAuthExt()) 25 | 26 | // keep alive, but we don't care about the answer 27 | setInterval(function () { 28 | client.publish('/api/v1/ping2', {reason: 'ping'}) 29 | }, 60000) 30 | 31 | return client 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitter-irc-bot", 3 | "version": "1.6.0", 4 | "description": "Bot that synchronises messages from a gitter room and an irc channel.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "./bin.js", 8 | "test": "standard ." 9 | }, 10 | "bin": { 11 | "gitter-irc-bot": "./bin.js" 12 | }, 13 | "author": "Finn Pauls", 14 | "license": "MIT", 15 | "dependencies": { 16 | "faye": "^1.1.2", 17 | "irc": "^0.4.0", 18 | "request": "^2.67.0", 19 | "xtend": "^4.0.0" 20 | }, 21 | "devDependencies": { 22 | "standard": "^12.0.1" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/finnp/gitter-irc-bot" 27 | }, 28 | "keywords": [ 29 | "gitter", 30 | "irc" 31 | ], 32 | "bugs": { 33 | "url": "https://github.com/finnp/gitter-irc-bot/issues" 34 | }, 35 | "homepage": "https://github.com/finnp/gitter-irc-bot" 36 | } 37 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var gitterBot = require('./') 3 | 4 | function getIrcOpts () { 5 | var ircOpts = process.env['GITTERBOT_IRC_OPTS'] 6 | 7 | if (ircOpts) { 8 | try { 9 | ircOpts = JSON.parse(process.env['GITTERBOT_IRC_OPTS']) 10 | } catch (err) { 11 | console.error('Invalid JSON in GITTERBOT_IRC_OPTS') 12 | process.exit(1) 13 | } 14 | } 15 | 16 | return ircOpts || {} 17 | } 18 | 19 | var opts = { 20 | ircServer: "irc.freenode.net", 21 | ircChannel: "#vimberlin", 22 | ircNick: "vimberlinbot", 23 | ircAdmin: "wikimatze", 24 | ircOpts: getIrcOpts(), 25 | gitterApiKey: "20abaec6f4c30ff5f2799ca58e305dc6e6a991b4", 26 | gitterRoom: "vimberlin/vimberlin.de" 27 | } 28 | 29 | if (!((opts.ircChannel || opts.ircOpts.channels) && 30 | opts.gitterApiKey && opts.gitterRoom && opts.ircNick)) { 31 | console.error('You need to set the config env variables (see readme.md)') 32 | process.exit(1) 33 | } 34 | 35 | var herokuURL = process.env.HEROKU_URL 36 | if (herokuURL) { 37 | var request = require('request') 38 | require('http').createServer(function (req, res) { 39 | res.end('ping heroku\n') 40 | }).listen(process.env.PORT) 41 | setInterval(function () { 42 | request(herokuURL).pipe(process.stdout) 43 | }, 5 * 60 * 1000) 44 | } 45 | 46 | gitterBot(opts) 47 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # gitter-irc-bot 2 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 3 | 4 | Bot that synchronises messages from a gitter room and an irc channel. 5 | 6 | Install with `npm install gitter-irc-bot`. 7 | 8 | If you want to test this bot, go to [the gitter channel](https://gitter.im/finnp/gitter-irc-bot) and 9 | join `#gitterircbot` on Freenode. 10 | 11 | ## How to use 12 | 13 | You need to set the following env variables 14 | 15 | * `GITTERBOT_APIKEY` Log in with your github/gitter bot [here](https://developer.gitter.im/apps) and take the `personal access token`. **Important:** This shouldn't be your personal GitHub account, but a designated bot account. 16 | * `GITTERBOT_GITTER_ROOM` The Gitter Room, e.g. `datproject/discussions` 17 | * `GITTERBOT_IRC_CHANNEL` IRC Channel name, e.g. `#dat` 18 | * `GITTERBOT_IRC_NICK` The IRC user nick of the bot 19 | 20 | The following options are optional: 21 | * `GITTERBOT_IRC_SERVER` IRC Server name, e.g. `irc.freenode.net` 22 | * `GITTERBOT_IRC_OPTS` JSON string with options passed to [node-irc](https://node-irc.readthedocs.org/en/latest/API.html) 23 | * `GITTERBOT_IRC_ADMIN` If specified this person receives error logs via pm. 24 | 25 | Then start the bot with `npm start`, or if you install globally run `gitter-irc-bot`. 26 | 27 | ### Authentication 28 | 29 | To authenticate to the IRC server, you will need to supply `password` in `GITTERBOT_IRC_OPTS`. 30 | For example: `GITTERBOT_IRC_OPTS='{"password": "hunter2"}'`. 31 | 32 | Using SASL is recommended if the IRC server supports it. You can enable it by supplying 33 | `"sasl": true` and the appropriate `userName`. 34 | Example: `GITTERBOT_IRC_OPTS='{"userName": "gitter-example", "password": "hunter2", "sasl": true}'` 35 | 36 | ## Deploy on heroku 37 | 38 | When deploying to heroku you need to set `HEROKU_URL` to the url of your heroku app. 39 | Otherwise heroku will spin down your free heroku instance after a few minutes. 40 | 41 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/finnp/gitter-irc-bot.git) 42 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var irc = require('irc') 2 | var request = require('request') 3 | var xtend = require('xtend') 4 | var gitterClient = require('./gitter.js') 5 | 6 | function escapeName (name) { 7 | var t = '`' 8 | while (name.indexOf(t) >= 0) t += '`' 9 | return t + ' ' + name + ' ' + t 10 | } 11 | 12 | module.exports = function (opts) { 13 | var gitter = gitterClient(opts.gitterApiKey) 14 | var headers = { 15 | 'Accept': 'application/json', 16 | 'Authorization': 'Bearer ' + opts.gitterApiKey 17 | } 18 | 19 | var ircOpts = xtend({ 20 | channels: [opts.ircChannel], 21 | autoConnect: false, 22 | retryCount: 20 23 | }, opts.ircOpts) 24 | 25 | var ircClient = new irc.Client( 26 | opts.ircServer || 'irc.freenode.net', 27 | opts.ircNick, 28 | ircOpts 29 | ) 30 | 31 | function log (message) { 32 | console.error(message) 33 | if (opts.ircAdmin) ircClient.say(opts.ircAdmin, message) 34 | } 35 | 36 | ircClient.on('error', function (message) { 37 | console.error('IRC Error:', message) 38 | }) 39 | 40 | console.log('Connecting to IRC..') 41 | ircClient.connect(function () { 42 | log('Connected to IRC, joined ' + opts.ircChannel) 43 | request.post({ url: 'https://api.gitter.im/v1/rooms', headers: headers, json: {uri: opts.gitterRoom} }, function (err, req, json) { 44 | if (err) return log(err) 45 | if (json.error) { 46 | return log('Error while communicating with the gitter API: ' + json.error); 47 | } 48 | 49 | var gitterRoomId = json.id 50 | var postGitterMessageUrl = 'https://api.gitter.im/v1/rooms/' + gitterRoomId + '/chatMessages' 51 | 52 | request({url: 'https://api.gitter.im/v1/user', headers: headers, json: true}, function (err, res, json) { 53 | if (err) return log(err) 54 | var gitterName = json[0].username 55 | var gitterUserId = json[0].id 56 | log('Gitterbot ' + gitterName + ' on channel ' + opts.gitterRoom + '(' + gitterRoomId + ')') 57 | 58 | gitter.subscribe('/api/v1/rooms/' + gitterRoomId + '/chatMessages', gitterMessage, {}) 59 | 60 | function gitterMessage (data) { 61 | if (data.operation !== 'create') return 62 | var message = data.model 63 | if (!message.fromUser) return 64 | var userName = message.fromUser.username 65 | if (userName === gitterName) return 66 | 67 | var lines = message.text.split('\n') 68 | if (lines.length > 4) { 69 | lines.splice(3) 70 | lines.push('[full message: https://gitter.im/' + opts.gitterRoom + '?at=' + message.id + ']') 71 | } 72 | 73 | var text = lines.map(function (line) { 74 | return '(' + userName + ') ' + line 75 | }).join('\n') 76 | 77 | // mark message as read by bot 78 | request.post({ 79 | url: 'https://api.gitter.im/v1/user/' + gitterUserId + '/rooms/' + gitterRoomId + '/unreadItems', 80 | headers: headers, 81 | json: {chat: [ message.id ]} 82 | }) 83 | console.log('gitter:', text) 84 | ircClient.say(opts.ircChannel, text) 85 | } 86 | 87 | ircClient.on('message' + opts.ircChannel, function (from, message) { 88 | if (from === ircClient.nick) return 89 | var text = escapeName(from) + ' ' + message 90 | console.log('irc:', text) 91 | request.post({url: postGitterMessageUrl, headers: headers, json: {text: text}}) 92 | }) 93 | ircClient.on('action', function (from, to, message) { 94 | if (to !== opts.ircChannel || from === ircClient.nick) return 95 | var text = '— ' + escapeName(from) + ' ' + message 96 | request.post({url: postGitterMessageUrl, headers: headers, json: {text: text}}) 97 | }) 98 | ircClient.on('pm', function (from, message) { 99 | if (from !== opts.ircAdmin) return ircClient.say('Your are not my master.') 100 | var commands = [ 101 | 'kill' 102 | ] 103 | if (message === commands[0]) { 104 | ircClient.say(from, 'Shutting down systems...') 105 | process.exit() 106 | } else { 107 | ircClient.say(from, 'Hi! I only understand: ' + commands.join(', ')) 108 | } 109 | }) 110 | }) 111 | }) 112 | }) 113 | } 114 | --------------------------------------------------------------------------------