├── .gitignore
├── Procfile
├── README.md
├── env.sample
├── heroku-sample.env
├── lib
├── pair_db.js
├── pair_db_memory.js
├── pair_db_mongo.js
├── pair_list.js
└── pair_list_mongo.js
├── package.json
└── web.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | .DS_Store
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: node web.js
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | ## slack commands for communicating about pairing / coworking with your team
3 |
4 | So you can find teammates to pair on the company website or a bite to eat, for example:
5 |
6 |
7 |
8 |
9 | ### Usage
10 |
11 | Use "/pair" alone to list the status of all teammates:
12 |
13 | > `/pair`
14 |
15 | > Yes! Someone should come find me now. Let's pair:
16 | >
17 | > * Jeremia: "Want to work on design today! Open to other ideas."
18 | > * Giselle: "Would love to do learn some JS today, or teach design"
19 | >
20 | > OK. I'm working now but feel free to interrupt me:
21 | >
22 | > * Molly
23 | > * Tom
24 | >
25 | > Nope. Do Not Disturb:
26 | >
27 | > * Jason: travelling
28 | > * Maksim
29 | > * Peter: deadlines!
30 |
31 | Use "/pair [yes/ok/no]" to set your status
32 |
33 | > `/pair yes`
34 |
35 | > Yes! You want to pair.
36 | > Use "/pair yes [subject]" to specify the [subject] you want to pair on.
37 |
38 | or
39 |
40 | > `/pair ok`
41 | >
42 | > OK! You're working but are OK with occassional interruptions for brief pairing.
43 |
44 | or
45 |
46 | > `/pair no`
47 | >
48 | > Bummer! You're too busy for pairing.
49 |
50 | ### Setup & Run
51 |
52 | 1. get a copy of the source: `git clone https://github.com/techieshark/slack-pair.git && cd slack-pair`
53 | 2. you can start it by just running `npm start`, but first:
54 | 3. follow the [instructions for configuring the Slack integration](https://github.com/techieshark/slack-pair/issues/14).
55 | 4. If you want notifications sent to a channel (e.g. "Samantha says yes to pairing (kernel debugging)"), configure an incoming webhook (name=pair, description="pair with buddies", channel = whatever channel you want things sent to), then copy the channel & webhook url to your config file and uncomment the lines for SLACK_PAIR_CHANNEL and SLACK_WEBHOOK_URL. Make sure to `$ source your-slack-domain.env` after you've copied and edited the env.sample.
56 | 5. By default, `pair` will run using an in-memory data store, which works for testing purposes but as soon as the app restarts (which could be more than once a day on Heroku), the list of users wanting to pair will be wiped. To prevent that, set up a MongoDB database, update `MONGO_URL` in your environment (see `env.sample`) and switch `DB_PROVIDER` from `memory` to `mongo` (again, see `env.sample`).
57 |
58 | ### Deploying to Heroku
59 |
60 | 1. Follow the setup instructions above. You should have `pair` up and running on your development machine, and connected to Slack through an `ngrok`-provided URL.
61 | 2. Create a heroku app: `heroku create your-app`.
62 | 3. Copy `heroku-sample.env` to `your-app.heroku.env`. Put your environment settings in that file.
63 | 4. If you want to user MongoDB, create that addon: `heroku addons:create mongolab:sandbox`. You'll need to create a new user / password and update the MONGO_URL in your environment settings. If you don't do this, you'l just run in memory and the pair list will reset fairly often (ok for testing, a bummer for production).
64 | 5. Push your confit to heroku: `heroku config:push -e your-app.heroku.env`.
65 | 6. Deploy to Heroku: `git push heroku master`
66 | 7. Update Slack's URL setting in the Custom Integration you set up in `Setup & Run` above. Your URL should look like https://your-app.herokuapp.com.
67 | 8. Enjoy!
68 |
69 |
70 | ### Contributing
71 |
72 | [Pull requests](https://help.github.com/articles/using-pull-requests/) are welcome and encouraged! You'll need
73 |
74 | 1. A slack account and the [ability to add slash commands](http://YOURTEAMNAME.slack.com/services/new/slash-commands)
75 | 2. [ngrok](https://ngrok.com/) or some other method of exposing a local port through a public URL
76 |
77 | Once you pull down the project, simply run `npm install` to set up the dependencies. There is a required `PAIRBOT_URL` environment variable but you can `source env.sample` to set it. This is used so that the bot pings itself to keep the Heroku dynos up.
78 |
79 | Then you should be able to just `npm start` (or `node web.js`) and be off to the races.
80 |
81 | You'll be wanting a slack command integration and supply a publicly accessible URL along with a testing command. Slack uses these commands to trigger the integration. To test out your app you'll tell slack to `/ ok test all the things`.
82 |
83 | Of course, if you run into any problems you can always open an [issue](https://github.com/techieshark/slack-pair/issues).
84 |
85 |
86 | ### Credits
87 |
88 | This is a collaborative project. We welcome your contributitions (see above). Ping [@techieshark](https://twitter.com/techieshark) on twitter if you want to get involved.
89 |
90 | Code originally by [@jeremiak](https://github.com/jeremiak) & [@techieshark](https://github.com/techieshark). Other collaborators listed here: https://github.com/techieshark/slack-pair/graphs/contributors.
91 |
92 | Special thanks to Ainsley ([@ainsleywagon](https://github.com/ainsleywagon)) for designing such a cool pair icon (https://thenounproject.com/term/pair/19161/).
93 |
94 | ---
95 |
96 | **happy pairing :)**
97 |
--------------------------------------------------------------------------------
/env.sample:
--------------------------------------------------------------------------------
1 | export PAIRBOT_URL=http://0.0.0.0:5000
2 | export SLACK_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXX
3 | #export SLACK_WEBHOOK_URL=https://hooks.slack.com/services/TXXXXXXXX/BXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX
4 | #export SLACK_PAIR_CHANNEL=#pair
5 | export MONGO_URL="mongodb://localhost:27017/test"
6 | export DB_PROVIDER=memory
7 | #export DB_PROVIDER=mongo
8 | #export DB_PROVIDER=redis
9 |
--------------------------------------------------------------------------------
/heroku-sample.env:
--------------------------------------------------------------------------------
1 | PAIRBOT_URL=http://your-app.herokuapp.com
2 | SLACK_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXX
3 | #SLACK_WEBHOOK_URL=https://hooks.slack.com/services/TXXXXXXXX/BXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX
4 | #SLACK_PAIR_CHANNEL=#pair
5 | MONGO_URL="mongodb://user:password@xyz123.mongolab.com:35965/heroku_xyz123"
6 | DB_PROVIDER=memory
7 | #DB_PROVIDER=mongo
8 | #DB_PROVIDER=redis
9 |
--------------------------------------------------------------------------------
/lib/pair_db.js:
--------------------------------------------------------------------------------
1 | var PairDbMemory = require('./pair_db_memory.js'),
2 | PairDbMongo = require('./pair_db_mongo.js');
3 | // PairDbRedis = require('./pair_db_redis.js');
4 |
5 | function PairDb () { }
6 |
7 | // factory function:
8 | // Decides which PairDb to return based on env var DB_PROVIDER
9 | //
10 | // returns a PairDb with functions:
11 | // .connect(callback) -> connects to database, calls callback when done
12 | // .getPairList() -> fetches pairlist from database
13 | //
14 | PairDb.build = function () {
15 |
16 | var provider = process.env.DB_PROVIDER;
17 | if (!provider || provider === '') {
18 | console.error("ERROR: environment variable DB_PROVIDER must be set to `memory`, `mongo`, or `redis`");
19 | process.exit(1);
20 | }
21 |
22 | if (provider === 'memory') {
23 | console.error("WARNING: While using in-memory storage, list won't save across app restarts.");
24 | Db = PairDbMemory;
25 |
26 | } else if (provider === 'mongo') {
27 | Db = PairDbMongo;
28 |
29 | } else if (provider === 'redis') {
30 | throw "Sorry, Redis isn't actually supported yet.";
31 | }
32 |
33 | try {
34 | return new Db();
35 | } catch (e) {
36 | console.error("ERROR: " + e);
37 | process.exit(1);
38 | }
39 |
40 | };
41 |
42 |
43 | module.exports = PairDb;
44 |
45 |
--------------------------------------------------------------------------------
/lib/pair_db_memory.js:
--------------------------------------------------------------------------------
1 | var PairList = require('./pair_list.js');
2 |
3 | function PairDbMemory() {
4 | this.pairList = new PairList();
5 | }
6 |
7 | // .connect(callback) -> connects to database, calls callback when done
8 | PairDbMemory.prototype.connect = function (callback) {
9 | // does nothing, we're already connected to memory
10 | callback(null);
11 | };
12 |
13 | // .getPairList() -> fetches pairlist from database
14 | PairDbMemory.prototype.getPairList = function () {
15 | return this.pairList;
16 | };
17 |
18 | module.exports = PairDbMemory;
19 |
--------------------------------------------------------------------------------
/lib/pair_db_mongo.js:
--------------------------------------------------------------------------------
1 | var MongoClient = require('mongodb').MongoClient;
2 | var PairListMongo = require('./pair_list_mongo.js');
3 |
4 |
5 | function PairDbMongo () {
6 | this.dbUrl = process.env.MONGO_URL;
7 | if (!this.dbUrl || this.dbUrl === '') {
8 | throw "Missing MONGO_URL environment variable needed by Pair to run on Mongo DB.";
9 | }
10 |
11 | this.slack_token = process.env.SLACK_TOKEN;
12 | if (!this.slack_token || this.slack_token === '') {
13 | throw "Missing SLACK_TOKEN environment variable needed by Pair to run on Mongo DB.";
14 | }
15 | }
16 |
17 |
18 | PairDbMongo.prototype.getPairList = function () {
19 | return new PairListMongo(this.database, this.slack_token);
20 | };
21 |
22 |
23 | PairDbMongo.prototype.connect = function(callback) {
24 | var that = this;
25 | MongoClient.connect(this.dbUrl, function(err, database) {
26 | that.database = database;
27 | callback(err);
28 | });
29 | };
30 |
31 |
32 | module.exports = PairDbMongo;
--------------------------------------------------------------------------------
/lib/pair_list.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash'),
2 | assert = require('assert'),
3 | debug = require('debug')('pair_list');
4 |
5 |
6 | // Simple PairList using an array
7 | function PairList () {
8 | this._pairs = [];
9 | }
10 |
11 | // Fetches (currently no-op), then calls callback
12 | // callback(err, pairList)
13 | PairList.prototype.fetch = function (callback) {
14 | callback(null, this);
15 | };
16 |
17 | function _update (users, username, status, comment) {
18 | var user = _.find(users, {'username': username});
19 | if (user) {
20 | user.status = status;
21 | user.comment = comment;
22 | }
23 | else {
24 | user = {'username': username, 'status': status, 'comment': comment};
25 | users.push(user);
26 | }
27 | debug("Updated pair list: ");
28 | debug(users);
29 | return user;
30 | }
31 |
32 |
33 | // update the pair list. Calls cb with the user & pairlist that has been updated.
34 | PairList.prototype.update = function (username, status, comment, callback) {
35 | assert(this._pairs instanceof Array);
36 | user = _update(this._pairs, username, status, comment);
37 | callback(null, {user: user, pairs: this._pairs});
38 | };
39 |
40 |
41 | function setof(status, users) {
42 | return _.filter(users, { 'status': status }).map(function(user) {
43 | return ">*" + user.username + "*: " + user.comment;
44 | }).join('\n');
45 | }
46 |
47 |
48 | PairList.prototype.toString = function () {
49 | var status = '', yes, no, ok;
50 |
51 | yes = setof('yes', this._pairs);
52 | ok = setof('ok', this._pairs);
53 | no = setof('no', this._pairs);
54 |
55 | if (yes.length > 0) {
56 | status = '*Yes! Let\'s pair. Come find me now:*\n';
57 | status += yes;
58 | }
59 | if (ok.length > 0 ) {
60 | status += '\n*Ok. I\'m working but you can interrupt:*\n';
61 | status += ok;
62 | }
63 | if (no.length > 0 ) {
64 | status += '\n*Nope. Do Not Disturb:*\n';
65 | status += no;
66 | }
67 | if (status === '') {
68 | status = 'No one up for pairing (yet!). Pair up, yo.\n';
69 | } else {
70 | status += '\n Pair up, yo. (Go find \'em!) \n';
71 | }
72 | return status;
73 | };
74 |
75 | module.exports = PairList;
76 |
77 |
--------------------------------------------------------------------------------
/lib/pair_list_mongo.js:
--------------------------------------------------------------------------------
1 |
2 | var PairList = require('./pair_list.js'),
3 | assert = require('assert'),
4 | debug = require('debug')('pair_list_mongo');
5 |
6 |
7 | var pairCollection = 'pair';
8 |
9 | // Create a pair list backed by a connected MongoDB database
10 | function PairListMongo (db, slack_token) {
11 | assert(db);
12 | assert(slack_token);
13 | this._db = db;
14 | this._token = slack_token;
15 |
16 | PairList.call(this);
17 | }
18 |
19 | PairListMongo.prototype = Object.create(PairList.prototype);
20 | PairListMongo.prototype.constructor = PairListMongo;
21 |
22 |
23 | // fetch list initially
24 | PairListMongo.prototype.fetch = function (callback) {
25 | var pairList = this;
26 | var cursor = this._db.collection(pairCollection).find({"slack_token": this._token}).limit(1);
27 | cursor.toArray(function(err, documents) {
28 | assert.equal(err, null);
29 | if (documents !== null) {
30 | assert(documents.length < 2);
31 | var list;
32 | if (documents.length === 1 && documents[0].users instanceof Array) {
33 | list = documents[0].users;
34 | debug("db found users list:");
35 | debug(list);
36 | debug("docs:");
37 | debug(documents);
38 | } else {
39 | list = [];
40 | }
41 | pairList._pairs = list;
42 | assert(pairList._pairs instanceof Array);
43 |
44 | callback(null, pairList);
45 | } else {
46 | callback("No userlist found.");
47 | }
48 | });
49 | };
50 |
51 |
52 | PairListMongo.prototype.update = function (username, status, comment, callback) {
53 | // call PairList update first, then save to database
54 | pairList = this;
55 | assert(this._pairs instanceof Array);
56 | Object.getPrototypeOf(PairListMongo.prototype).update.call(this,
57 | username, status, comment,
58 | function (err, data) {
59 | if (err) throw err;
60 | pairList._save(data, callback);
61 | });
62 | };
63 |
64 |
65 | PairListMongo.prototype._save = function (data, callback) {
66 | var list = data.pairs;
67 | var user = data.user;
68 | debug("saving data:");
69 | debug(data);
70 | this._db.collection(pairCollection).updateOne(
71 | { "slack_token": this._token },
72 | { // perhaps we could just update the one user, but for small lists this s/be ok
73 | "slack_token": this._token,
74 | "users" : list
75 | },
76 | { upsert: true },
77 | function (err, result) {
78 | assert.equal(err, null);
79 | debug("Updated MongoDB w/ slack pairs: ");
80 | debug(list);
81 | callback(null, data);
82 | }
83 | );
84 | };
85 |
86 |
87 | module.exports = PairListMongo;
88 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "slack-pair",
3 | "version": "0.0.1",
4 | "description": "a co-working (or pair programming) availability tracker for Slack",
5 | "main": "web.js",
6 | "scripts": {
7 | "start": "node web.js",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/techieshark/slack-pair.git"
13 | },
14 | "keywords": [
15 | "slack",
16 | "cowork",
17 | "pairing",
18 | "bot"
19 | ],
20 | "author": "techieshark",
21 | "contributors": [
22 | "jeremiak"
23 | ],
24 | "license": "GPL-3.0",
25 | "bugs": {
26 | "url": "https://github.com/techieshark/slack-pair/issues"
27 | },
28 | "homepage": "https://github.com/techieshark/slack-pair",
29 | "dependencies": {
30 | "body-parser": "^1.4.3",
31 | "debug": "^2.2.0",
32 | "express": "^4.4.4",
33 | "lodash": "^2.4.1",
34 | "logfmt": "^1.1.2",
35 | "mongodb": "^2.0.52",
36 | "request": "^2.67.0"
37 | },
38 | "devDependencies": {}
39 | }
40 |
--------------------------------------------------------------------------------
/web.js:
--------------------------------------------------------------------------------
1 | // web.js
2 | var _ = require('lodash');
3 | var express = require("express");
4 | var bodyParser = require('body-parser');
5 | var logfmt = require("logfmt");
6 | var request = require('request');
7 | var PairDb = require('./lib/pair_db.js');
8 | var debug = require('debug')('pair');
9 |
10 | var db;
11 | var app = express();
12 | app.use(bodyParser.urlencoded({extended: true}));
13 | app.use(bodyParser.json());
14 | app.use(logfmt.requestLogger());
15 |
16 | var help = 'Usage:\t `/pair [yes|ok|no ]` or `/pair` alone to see who is free.';
17 |
18 | function validToken(token) {
19 | if (token == process.env.SLACK_TOKEN) {
20 | debug('Slack token verified');
21 | return true;
22 | } else{
23 | debug('Slack token does not match stored token, if present.');
24 | return false;
25 | }
26 | }
27 |
28 | function notifyChannel(text) {
29 | payload = {
30 | "channel": process.env.SLACK_PAIR_CHANNEL,
31 | "username": "pair",
32 | "text": text,
33 | "icon_url": "http://s8.postimg.org/kmlmmglid/noun_19161_cc.png" // thx @ainsleywagon, https://thenounproject.com/term/pair/19161/
34 | };
35 |
36 | request.post({
37 | uri: process.env.SLACK_WEBHOOK_URL,
38 | body: JSON.stringify(payload),
39 | },
40 | function (error, response, body) {
41 | if (!error && response.statusCode == 200) {
42 | debug(body);
43 | } else if (error) {
44 | console.error("Error posting to channel: " + error);
45 | }
46 | }
47 | );
48 | }
49 |
50 |
51 | app.post('/', function(req, res) {
52 | var hasArgs = req.body.text.length > 0;
53 | var args = req.body.text.toLowerCase().split(' '),
54 | command = req.body.command,
55 | username = req.body.user_name,
56 | token = req.body.token,
57 | acceptable = ["yes", "ok", "no"];
58 | var user, status, notification;
59 | var pairList;
60 |
61 | debug('args');
62 | debug(args);
63 |
64 | if (!validToken(token)) {
65 | res.send('Invalid Slack token. Ensure that the correct Slack integration token is set as the SLACK_TOKEN env var.');
66 | } else {
67 | if (hasArgs) {
68 | if (args[0] === 'help') {
69 | // send help
70 | res.send(
71 | {
72 | "response_type": "ephemeral",
73 | "text": help + "\n" +
74 | "Full documentation .",
75 | });
76 | } else if (_.contains(acceptable, args[0])) {
77 | var comment = args.slice(1).join(' ');
78 | status = args[0];
79 |
80 | // get pairList from DB
81 | pairList = db.getPairList();
82 | pairList.fetch(function (err, pairList) {
83 | pairList.update(username, status, comment, function (err, data) {
84 | user = data.user; // users = data.users
85 |
86 | // People want confirmation that we got their status, so show it and everyone else's:
87 | notification = 'Your pairing status was set to: ' + status + '\n';
88 | notification += pairList.toString();
89 | res.send(notification);
90 |
91 | // notify pairing channel if environment vars are provided and status is yes/ok
92 | if (process.env.SLACK_WEBHOOK_URL && process.env.SLACK_PAIR_CHANNEL &&
93 | (status === 'yes' || status === 'ok')) {
94 | notifyChannel(user.username + " says '" + user.status + "' to pairing" + (user.comment ? " (" + user.comment + ")" : "" ) + "! Go pair!");
95 | }
96 | });
97 | });
98 | } else {
99 | res.send('Close but no cigar. What is this command, "' + args[0] + '", that you speak of?\n' + help);
100 | }
101 | }
102 | else {
103 | // get pairList from DB
104 | pairList = db.getPairList();
105 | pairList.fetch(function (err, pairList) {
106 | if(err) throw err;
107 | status = pairList.toString();
108 | debug(status);
109 | res.send(status);
110 | });
111 | }
112 | }
113 | });
114 |
115 | app.get('/keepalive', function (req, res) {
116 | debug('pong');
117 | res.send(Date.now()+'');
118 | });
119 |
120 | function keepalive() {
121 | request(process.env.PAIRBOT_URL + '/keepalive');
122 | }
123 |
124 | db = PairDb.build();
125 | db.connect(function (err) {
126 |
127 | if (err) throw err;
128 |
129 | // Start the application after the database connection is ready
130 | var port = Number(process.env.PORT || 5000);
131 | app.listen(port, function() {
132 | console.log("Listening on " + port);
133 | console.log("PAIR URL: " + process.env.PAIRBOT_URL);
134 | console.log("SLACK_TOKEN: " + process.env.SLACK_TOKEN);
135 | console.log("SLACK_WEBHOOK_URL: " + process.env.SLACK_WEBHOOK_URL);
136 | console.log("SLACK_PAIR_CHANNEL: " + process.env.SLACK_PAIR_CHANNEL);
137 | console.log("MONGO_URL: " + process.env.MONGO_URL);
138 | console.log("DB_PROVIDER: " + process.env.DB_PROVIDER);
139 | setInterval(keepalive, 60e3);
140 | });
141 |
142 | });
143 |
144 |
--------------------------------------------------------------------------------