From b57dc1a03c35f67919fe184ebd6fa210c9c94aad Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 01:11:41 -0400 Subject: [PATCH 01/59] initial commit --- package.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 0000000..1f23b74 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "slackbot", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Atticus White (http://atticuswhite.com/)", + "license": "MIT" +} From 76877fe8b5c8dd19000c5c2fa052d77eecfb6264 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 01:25:26 -0400 Subject: [PATCH 02/59] slack client --- .gitignore | 1 + package.json | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/package.json b/package.json index 1f23b74..a47926a 100644 --- a/package.json +++ b/package.json @@ -7,5 +7,8 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Atticus White (http://atticuswhite.com/)", - "license": "MIT" + "license": "MIT", + "dependencies": { + "slack-client": "^1.4.1" + } } From 85159d300064bc1861d7e14c277428c2ab402436 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 01:50:44 -0400 Subject: [PATCH 03/59] ignores and packages --- .gitignore | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 3c3629e..1f43471 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.config.env.json diff --git a/package.json b/package.json index a47926a..0007f44 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "author": "Atticus White (http://atticuswhite.com/)", "license": "MIT", "dependencies": { + "lodash": "^3.10.1", "slack-client": "^1.4.1" } } From 43e61917e5d0659858af1d23bd95b2b679065f76 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 02:33:11 -0400 Subject: [PATCH 04/59] inital structure --- README.md | 0 index.js | 8 ++++++++ src/bot.js | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 README.md create mode 100644 index.js create mode 100644 src/bot.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/index.js b/index.js new file mode 100644 index 0000000..965a92d --- /dev/null +++ b/index.js @@ -0,0 +1,8 @@ +var Slack = require('slack-client'), + Bot = require('./src/bot'), + config = require('./.config.env.json'); + +(function () { + var slack = new Slack(config.slack.key, true), + bot = new Bot(slack); +}).call(this); diff --git a/src/bot.js b/src/bot.js new file mode 100644 index 0000000..70df08f --- /dev/null +++ b/src/bot.js @@ -0,0 +1,35 @@ +var Slack = require('slack-client'), + _ = require('lodash'); + +module.exports = (function () { + + function Bot (slackClient) { + this.service = slackClient; + + + // this.service.on('open', this.initialize.bind(this)); + this.service.on('message', this.onMessage.bind(this)); + this.service.on('user_change', this.userJoined.bind(this)); + this.service.on('open', this.connected.bind(this)); + this.service.login(); + } + + Bot.prototype.onMessage = function (message) { + console.log('foobar', message); + }; + + Bot.prototype.connected = function () { + // leave all channels that the bot may have been added to + _.filter(this.service.channels, function (channel) { + return channel.is_member; + }).forEach(function (channel) { + channel.leave(); + }); + }; + + Bot.prototype.userJoined = function (event) { + + }; + + return Bot; +}).call(this); From 1bf5d7c12a3027a35d78f424b98840bd1dbf4ba7 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 02:41:06 -0400 Subject: [PATCH 05/59] message dispatch --- src/bot.js | 13 +++++++++---- src/conversation.js | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 src/conversation.js diff --git a/src/bot.js b/src/bot.js index 70df08f..9b51f43 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,21 +1,26 @@ var Slack = require('slack-client'), + Conversation = require('./conversation'), _ = require('lodash'); module.exports = (function () { function Bot (slackClient) { this.service = slackClient; + this.conversations = {}; // this.service.on('open', this.initialize.bind(this)); - this.service.on('message', this.onMessage.bind(this)); + this.service.on('message', this.dispatch.bind(this)); this.service.on('user_change', this.userJoined.bind(this)); this.service.on('open', this.connected.bind(this)); this.service.login(); } - Bot.prototype.onMessage = function (message) { - console.log('foobar', message); + Bot.prototype.dispatch = function (message) { + if (!(message.user in this.conversations)) { + this.conversations[message.user] = new Conversation(this, message.channel); + } + this.conversations[message.user].push(message); }; Bot.prototype.connected = function () { @@ -28,7 +33,7 @@ module.exports = (function () { }; Bot.prototype.userJoined = function (event) { - + }; return Bot; diff --git a/src/conversation.js b/src/conversation.js new file mode 100644 index 0000000..56c3f70 --- /dev/null +++ b/src/conversation.js @@ -0,0 +1,14 @@ +module.exports = (function () { + + function Conversation (delegate, channel) { + this.delegate = delegate; + this.channel = delegate.service.getDMByID(channel); + this.messages = []; + } + + Conversation.prototype.push = function (message) { + this.channel.send('response'); + }; + + return Conversation; +}).call(this); From e35e110f1db1ef4da4e1f043e2ab371e6a8442f4 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 03:38:38 -0400 Subject: [PATCH 06/59] dialog working --- src/conversation-nodes.js | 55 ++++++++++++++++++ src/conversation.js | 19 ++++++- src/dialog.js | 117 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 src/conversation-nodes.js create mode 100644 src/dialog.js diff --git a/src/conversation-nodes.js b/src/conversation-nodes.js new file mode 100644 index 0000000..075afac --- /dev/null +++ b/src/conversation-nodes.js @@ -0,0 +1,55 @@ +var states = require('./dialog'), + _ = require('lodash'); + +function Node (state, message, options, type) { + Node.nodes.push(this); + + this.state = state; + this.message = Array.isArray(message) ? message.join(' ') : message; + this.options = options; + this.type = type; +} + +Node.nodes = []; + +// static accessor +Node.get = function (state) { + return Node.nodes[state] || false; +}; + +Node.prototype.interact = function (message) { + if (message === "restart") { + console.log('rewinding!'); + return Node.get(0); + } + + var transitionNode = _.find(this.options, function (option) { + console.log('comparing', option.word, 'to', message, option.word === message); + return option.word.toLowerCase() === message.toLowerCase(); + }); + console.log('match', transitionNode); + if (transitionNode) { + return Node.get(transitionNode.state); + } else { + return false; + } +}; + +Node.prototype.prompt = function (channel) { + channel.send(this.message); +}; + +Node.prototype.retry = function (channel) { + channel.send('Try again, I did not understand'); +}; + + +var nodes = _.map(states, function (state, index) { + return new Node(index, state.message, state.options, state.type); +}); + +module.exports = { + getRootNode: function () { + return Node.get(0); + } +}; diff --git a/src/conversation.js b/src/conversation.js index 56c3f70..ba07b2d 100644 --- a/src/conversation.js +++ b/src/conversation.js @@ -1,13 +1,28 @@ +var ConversationNodes = require('./conversation-nodes'); + module.exports = (function () { function Conversation (delegate, channel) { this.delegate = delegate; this.channel = delegate.service.getDMByID(channel); - this.messages = []; + this.node = null; } Conversation.prototype.push = function (message) { - this.channel.send('response'); + var transitionNode; + + if (this.node === null) { + this.node = ConversationNodes.getRootNode(); + this.node.prompt(this.channel); + } else { + transition = this.node.interact(message.text); + if (transition) { + this.node = transition; + this.node.prompt(this.channel); + } else { + this.node.retry(this.channel); + } + } }; return Conversation; diff --git a/src/dialog.js b/src/dialog.js new file mode 100644 index 0000000..05ebec8 --- /dev/null +++ b/src/dialog.js @@ -0,0 +1,117 @@ +var channels = {}; + +var states = [ + // 0 - Initial Greeting + { + message: [ + "Hey, is this the first time you've been here?\n", + "Yes or No" + ], + type: Boolean, + options: [ + { + word: 'Yes', + state: 1, + regex: '' + }, { + word: 'No', + state: 2, + regex: '' + } + ] + }, + + // 1 - Newcomer + { + message: [ + "No problem, Slack is a great tool for teams. You can find a lot of documentation here: \n", + "To start, what are you interested in?\n", + "Development, Design, Managing, or Other" + ], + options: [ + { + word: 'Development', + state: 3, + regex: '' + }, { + word: 'Design', + state: 4, + regex: '' + }, { + word: 'Managing', + state: 5, + regex: '' + }, { + word: 'Other', + state: 6, + regex: '' + } + ], + type: String + }, + + // 2 - Experienced + { + message: [ + "Great, you know your way around here then\n", + "To start, what are you interested in?\n", + "Development, Design, Managing, or Other" + ], + type: String, + options: [ + { + word: 'Development', + state: 3, + regex: '' + }, { + word: 'Design', + state: 4, + regex: '' + }, { + word: 'Managing', + state: 5, + regex: '' + }, { + word: 'Other', + state: 6, + regex: '' + } + ] + }, + + // 3 - Developer + { + message: [ + "Awesome, we have plenty of projects for developers.", + "You can find some cool projects over in #sites-fors-sanders, #bernie-app" + ], + type: String + }, + + // 4 - Designer + { + message: [ + "Awesome, we have plenty of projects for designers", + "Off hand, #car-pool could use some help right now" + ], + type: String + }, + + // 5 - Management + { + message: [ + "Awesome, you'll want to coordinate with @jahaz. Give him a shout!" + ], + type: String + }, + + // 6 - Other + { + message: [ + "Cool, give @jahaz a shout to see where you can come in!" + ], + type: String + } +]; + +module.exports = states; From 2a3ed28c10f205961a95ebe7fc62598794cb31bd Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 03:41:23 -0400 Subject: [PATCH 07/59] readme --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index e69de29..51b979b 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,35 @@ +# Butler + +Our welcoming Slack bot. + + +## Conversation + +The conversation flow can be edited in `src/dialog.js` + +## Installation + +Install dependencies: +``` +npm install +``` + +Add environment configuration +``` +touch .config.env.json +``` + +Add a slack token +```json +{ + "slack" : { + "key": "XXX" + } +} +``` + +## Running + +``` +node . +``` From 39e550db40685d52c236ebe80a9713d2ebb74fc2 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 03:51:57 -0400 Subject: [PATCH 08/59] cleanup --- src/conversation-nodes.js | 9 --------- src/conversation.js | 17 +++++++---------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/conversation-nodes.js b/src/conversation-nodes.js index 075afac..5ab5b75 100644 --- a/src/conversation-nodes.js +++ b/src/conversation-nodes.js @@ -35,15 +35,6 @@ Node.prototype.interact = function (message) { } }; -Node.prototype.prompt = function (channel) { - channel.send(this.message); -}; - -Node.prototype.retry = function (channel) { - channel.send('Try again, I did not understand'); -}; - - var nodes = _.map(states, function (state, index) { return new Node(index, state.message, state.options, state.type); }); diff --git a/src/conversation.js b/src/conversation.js index ba07b2d..022564b 100644 --- a/src/conversation.js +++ b/src/conversation.js @@ -9,20 +9,17 @@ module.exports = (function () { } Conversation.prototype.push = function (message) { - var transitionNode; + var transitionNode, + response = 'Try again, I did not understand'; if (this.node === null) { this.node = ConversationNodes.getRootNode(); - this.node.prompt(this.channel); - } else { - transition = this.node.interact(message.text); - if (transition) { - this.node = transition; - this.node.prompt(this.channel); - } else { - this.node.retry(this.channel); - } + response = this.node.message; + } else if (transition = this.node.interact(message.text)) { + this.node = transition; + response = this.node.message; } + this.channel.send(response); }; return Conversation; From ddc58de2eee99549390ba51ea03aadd30fa9d8b9 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 04:12:02 -0400 Subject: [PATCH 09/59] regex --- src/conversation-nodes.js | 12 +++++++++--- src/dialog.js | 20 ++++++++++---------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/conversation-nodes.js b/src/conversation-nodes.js index 5ab5b75..688e4c2 100644 --- a/src/conversation-nodes.js +++ b/src/conversation-nodes.js @@ -18,15 +18,21 @@ Node.get = function (state) { }; Node.prototype.interact = function (message) { + var transitionNode; if (message === "restart") { console.log('rewinding!'); return Node.get(0); } - var transitionNode = _.find(this.options, function (option) { - console.log('comparing', option.word, 'to', message, option.word === message); - return option.word.toLowerCase() === message.toLowerCase(); + transitionNode = _.find(this.options, function (option) { + console.log('comparing', option.word, 'to', message); + if (option.regex) { + return option.regex.test(message); + } else { + option.word.toLowerCase === message.toLowerCase(); + } }); + console.log('match', transitionNode); if (transitionNode) { return Node.get(transitionNode.state); diff --git a/src/dialog.js b/src/dialog.js index 05ebec8..ef1943f 100644 --- a/src/dialog.js +++ b/src/dialog.js @@ -12,11 +12,11 @@ var states = [ { word: 'Yes', state: 1, - regex: '' + regex: /[y]+(es|e|a)*$/i }, { word: 'No', state: 2, - regex: '' + regex: /[n]/i } ] }, @@ -32,19 +32,19 @@ var states = [ { word: 'Development', state: 3, - regex: '' + regex: /[d]+(ev)+/i }, { word: 'Design', state: 4, - regex: '' + regex: /[d]+(es)+/i }, { word: 'Managing', state: 5, - regex: '' + regex: /(man)+/i }, { word: 'Other', state: 6, - regex: '' + regex: /(other)+/i } ], type: String @@ -62,19 +62,19 @@ var states = [ { word: 'Development', state: 3, - regex: '' + regex: /[d]+(ev)+/i }, { word: 'Design', state: 4, - regex: '' + regex: /[d]+(es)+/i }, { word: 'Managing', state: 5, - regex: '' + regex: /(man)+/i }, { word: 'Other', state: 6, - regex: '' + regex: /(other)+/i } ] }, From fe4a8a1b870d9b6c1be46b7f6f624221472f857d Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 04:27:05 -0400 Subject: [PATCH 10/59] connection logged --- src/bot.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bot.js b/src/bot.js index 9b51f43..291fa83 100644 --- a/src/bot.js +++ b/src/bot.js @@ -25,6 +25,7 @@ module.exports = (function () { Bot.prototype.connected = function () { // leave all channels that the bot may have been added to + console.log('connection successful'); _.filter(this.service.channels, function (channel) { return channel.is_member; }).forEach(function (channel) { From 23bc457f7801dbdefbea8d4ef1d6d40bc8bfe460 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 04:27:16 -0400 Subject: [PATCH 11/59] regex support --- src/conversation-nodes.js | 14 ++++++++++++++ src/conversation.js | 4 ++-- src/dialog.js | 9 +++------ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/conversation-nodes.js b/src/conversation-nodes.js index 688e4c2..bba5ee4 100644 --- a/src/conversation-nodes.js +++ b/src/conversation-nodes.js @@ -41,6 +41,20 @@ Node.prototype.interact = function (message) { } }; +Node.prototype.getValue = function () { + var output = _.map(this.options, function (option) { + return option.word; + }); + if (output.length > 1) { + output[output.length - 1] = 'or ' + output[output.length - 1]; + } + return [ + this.message, + '\n', + output.join(output.length > 2 ? ', ' : ' ') + ].join(' '); +}; + var nodes = _.map(states, function (state, index) { return new Node(index, state.message, state.options, state.type); }); diff --git a/src/conversation.js b/src/conversation.js index 022564b..05f1431 100644 --- a/src/conversation.js +++ b/src/conversation.js @@ -14,10 +14,10 @@ module.exports = (function () { if (this.node === null) { this.node = ConversationNodes.getRootNode(); - response = this.node.message; + response = this.node.getValue(); } else if (transition = this.node.interact(message.text)) { this.node = transition; - response = this.node.message; + response = this.node.getValue(); } this.channel.send(response); }; diff --git a/src/dialog.js b/src/dialog.js index ef1943f..2e8bd45 100644 --- a/src/dialog.js +++ b/src/dialog.js @@ -4,8 +4,7 @@ var states = [ // 0 - Initial Greeting { message: [ - "Hey, is this the first time you've been here?\n", - "Yes or No" + "Hey, is this the first time you've been here?" ], type: Boolean, options: [ @@ -25,8 +24,7 @@ var states = [ { message: [ "No problem, Slack is a great tool for teams. You can find a lot of documentation here: \n", - "To start, what are you interested in?\n", - "Development, Design, Managing, or Other" + "To start, what are you interested in?" ], options: [ { @@ -54,8 +52,7 @@ var states = [ { message: [ "Great, you know your way around here then\n", - "To start, what are you interested in?\n", - "Development, Design, Managing, or Other" + "To start, what are you interested in?" ], type: String, options: [ From 13118eaf1e886b4298aadb932bcd9e7366c64e16 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 04:30:05 -0400 Subject: [PATCH 12/59] comments --- src/conversation.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/conversation.js b/src/conversation.js index 05f1431..0868eb0 100644 --- a/src/conversation.js +++ b/src/conversation.js @@ -2,23 +2,38 @@ var ConversationNodes = require('./conversation-nodes'); module.exports = (function () { + /** + * A conversation object tracking the current dialog node with a given user + */ function Conversation (delegate, channel) { + // the bot this.delegate = delegate; + // the DM channel this.channel = delegate.service.getDMByID(channel); + // the current dialog node this.node = null; } + /** + * Receive a message from the user and interact with the current node + */ Conversation.prototype.push = function (message) { var transitionNode, response = 'Try again, I did not understand'; if (this.node === null) { + // the converation has not yet started, grab the root (welcome) node this.node = ConversationNodes.getRootNode(); + // respond with the root node message response = this.node.getValue(); } else if (transition = this.node.interact(message.text)) { + // the message has transitioned to a new node in the conversation this.node = transition; + // respond with the next node message response = this.node.getValue(); } + + // send the node's message to the user this.channel.send(response); }; From 759f688546953105215b4e2387043a0e005fec2e Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 04:37:22 -0400 Subject: [PATCH 13/59] node comments --- src/conversation-nodes.js | 62 +++----------------------------- src/node.js | 74 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 57 deletions(-) create mode 100644 src/node.js diff --git a/src/conversation-nodes.js b/src/conversation-nodes.js index bba5ee4..463f69e 100644 --- a/src/conversation-nodes.js +++ b/src/conversation-nodes.js @@ -1,61 +1,9 @@ -var states = require('./dialog'), - _ = require('lodash'); +var Node = require('./node'), + states = require('./dialog'), + _ = require('lodash'), + nodes; -function Node (state, message, options, type) { - Node.nodes.push(this); - - this.state = state; - this.message = Array.isArray(message) ? message.join(' ') : message; - this.options = options; - this.type = type; -} - -Node.nodes = []; - -// static accessor -Node.get = function (state) { - return Node.nodes[state] || false; -}; - -Node.prototype.interact = function (message) { - var transitionNode; - if (message === "restart") { - console.log('rewinding!'); - return Node.get(0); - } - - transitionNode = _.find(this.options, function (option) { - console.log('comparing', option.word, 'to', message); - if (option.regex) { - return option.regex.test(message); - } else { - option.word.toLowerCase === message.toLowerCase(); - } - }); - - console.log('match', transitionNode); - if (transitionNode) { - return Node.get(transitionNode.state); - } else { - return false; - } -}; - -Node.prototype.getValue = function () { - var output = _.map(this.options, function (option) { - return option.word; - }); - if (output.length > 1) { - output[output.length - 1] = 'or ' + output[output.length - 1]; - } - return [ - this.message, - '\n', - output.join(output.length > 2 ? ', ' : ' ') - ].join(' '); -}; - -var nodes = _.map(states, function (state, index) { +nodes = _.map(states, function (state, index) { return new Node(index, state.message, state.options, state.type); }); diff --git a/src/node.js b/src/node.js new file mode 100644 index 0000000..31d34d5 --- /dev/null +++ b/src/node.js @@ -0,0 +1,74 @@ +var _ = require('lodash'); + +module.exports = (function () { + function Node (state, message, options) { + // append the node to the static tree + Node.nodes.push(this); + + // the state value of the node + this.state = state; + // the node's message + this.message = Array.isArray(message) ? message.join(' ') : message; + // the options available to the node + this.options = options; + } + + // static collection of nodes + Node.nodes = []; + + // static node accessor + Node.get = function (state) { + return Node.nodes[state] || null; + }; + + /** + * Handle an interaction with the node. + * - Return the next state (node) based on the input message + * - Return the root node in the case of a rewinding + * - Return `null` if there is no transition + */ + Node.prototype.interact = function (message) { + var transitionNode; + if (message === "restart") { + return Node.get(0); + } + + // find the node to transition to based on the options + transitionNode = _.find(this.options, function (option) { + if (option.regex) { + // if the transition has a regex value, match it + return option.regex.test(message); + } else { + // otherwise check if the input matches the option value + return option.word.toLowerCase() === message.toLowerCase(); + } + }); + + if (transitionNode) { + // if we have a new node to transition to, return it + return Node.get(transitionNode.state); + } else { + // if no inputs are satisfied, there is no transition + return null; + } + }; + + /** + * Gets the formatted message value of the node. If there are options, they are formatted into a comma delimited list + */ + Node.prototype.getValue = function () { + var output = _.map(this.options, function (option) { + return option.word; + }); + if (output.length > 1) { + output[output.length - 1] = 'or ' + output[output.length - 1]; + } + return [ + this.message, + '\n', + output.join(output.length > 2 ? ', ' : ' ') + ].join(' '); + }; + + return Node; +}).call(this); From a2eb5bb49af33fd902d0e7993966b8078d3cbdb7 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 04:39:01 -0400 Subject: [PATCH 14/59] naming --- src/conversation.js | 4 ++-- src/{conversation-nodes.js => node-factory.js} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename src/{conversation-nodes.js => node-factory.js} (100%) diff --git a/src/conversation.js b/src/conversation.js index 0868eb0..9fae2c0 100644 --- a/src/conversation.js +++ b/src/conversation.js @@ -1,4 +1,4 @@ -var ConversationNodes = require('./conversation-nodes'); +var NodeFactory = require('./node-factory'); module.exports = (function () { @@ -23,7 +23,7 @@ module.exports = (function () { if (this.node === null) { // the converation has not yet started, grab the root (welcome) node - this.node = ConversationNodes.getRootNode(); + this.node = NodeFactory.getRootNode(); // respond with the root node message response = this.node.getValue(); } else if (transition = this.node.interact(message.text)) { diff --git a/src/conversation-nodes.js b/src/node-factory.js similarity index 100% rename from src/conversation-nodes.js rename to src/node-factory.js From 64ec11eb5a8e1f392515883616b606828f3e2b24 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 04:56:00 -0400 Subject: [PATCH 15/59] dialog --- src/dialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dialog.js b/src/dialog.js index 2e8bd45..401d8c6 100644 --- a/src/dialog.js +++ b/src/dialog.js @@ -23,7 +23,7 @@ var states = [ // 1 - Newcomer { message: [ - "No problem, Slack is a great tool for teams. You can find a lot of documentation here: \n", + "No problem, Slack is a great tool for teams. You can find a lot of documentation here on slack.com\n", "To start, what are you interested in?" ], options: [ @@ -80,7 +80,7 @@ var states = [ { message: [ "Awesome, we have plenty of projects for developers.", - "You can find some cool projects over in #sites-fors-sanders, #bernie-app" + "You can find some cool projects over in #sites-for-sanders, #bernie-app" ], type: String }, From 873d364b91dd290fbdb0929ca471374859f2cad7 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 05:01:33 -0400 Subject: [PATCH 16/59] heroku --- Procfile | 1 + 1 file changed, 1 insertion(+) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..1da0cd6 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node index.js From e0afbffc27d8240bb227ebef56b41ae73feb286a Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 05:05:06 -0400 Subject: [PATCH 17/59] enviornment var --- index.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 965a92d..feb0fd6 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,15 @@ var Slack = require('slack-client'), - Bot = require('./src/bot'), - config = require('./.config.env.json'); + Bot = require('./src/bot'); (function () { - var slack = new Slack(config.slack.key, true), - bot = new Bot(slack); + var token = process.env.SLACK_TOKEN, + slack, + bot; + + if (!token) { + throw new Error('Slack token is required. Please provide SLACK_TOKEN as an environment variable'); + } + + slack = new Slack(config.slack.key, true); + bot = new Bot(slack); }).call(this); From ff50620aa31248df727311acacc76583711f263a Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 05:05:56 -0400 Subject: [PATCH 18/59] token --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index feb0fd6..fc1bff7 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,6 @@ var Slack = require('slack-client'), throw new Error('Slack token is required. Please provide SLACK_TOKEN as an environment variable'); } - slack = new Slack(config.slack.key, true); + slack = new Slack(token, true); bot = new Bot(slack); }).call(this); From d9120715908be3558531ac439a6b5f68582a813a Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 05:13:33 -0400 Subject: [PATCH 19/59] logging --- package.json | 1 + src/bot.js | 4 +++- src/conversation.js | 6 +++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 0007f44..d10db74 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "lodash": "^3.10.1", + "log4js": "^0.6.26", "slack-client": "^1.4.1" } } diff --git a/src/bot.js b/src/bot.js index 291fa83..95a1a5d 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,5 +1,6 @@ var Slack = require('slack-client'), Conversation = require('./conversation'), + logger = require('log4js').getLogger('bot'), _ = require('lodash'); module.exports = (function () { @@ -18,6 +19,7 @@ module.exports = (function () { Bot.prototype.dispatch = function (message) { if (!(message.user in this.conversations)) { + logger.info('new user conversation', message.user); this.conversations[message.user] = new Conversation(this, message.channel); } this.conversations[message.user].push(message); @@ -25,7 +27,7 @@ module.exports = (function () { Bot.prototype.connected = function () { // leave all channels that the bot may have been added to - console.log('connection successful'); + logger.info('connection successful'); _.filter(this.service.channels, function (channel) { return channel.is_member; }).forEach(function (channel) { diff --git a/src/conversation.js b/src/conversation.js index 9fae2c0..6a93519 100644 --- a/src/conversation.js +++ b/src/conversation.js @@ -1,7 +1,7 @@ -var NodeFactory = require('./node-factory'); +var NodeFactory = require('./node-factory'), + logger = require('log4js').getLogger('conversation'); module.exports = (function () { - /** * A conversation object tracking the current dialog node with a given user */ @@ -21,6 +21,7 @@ module.exports = (function () { var transitionNode, response = 'Try again, I did not understand'; + logger.info(message.user, 'sent', message.text, 'in response to node', this.node ? this.node.state : 'initial node'); if (this.node === null) { // the converation has not yet started, grab the root (welcome) node this.node = NodeFactory.getRootNode(); @@ -32,7 +33,6 @@ module.exports = (function () { // respond with the next node message response = this.node.getValue(); } - // send the node's message to the user this.channel.send(response); }; From 47b2b7952f26a83d4778da79d8a4bf9fa338d1a9 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 05:31:20 -0400 Subject: [PATCH 20/59] required http listener for heroku --- index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index fc1bff7..f92f211 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ var Slack = require('slack-client'), - Bot = require('./src/bot'); + Bot = require('./src/bot'), + http = require('http'); (function () { var token = process.env.SLACK_TOKEN, @@ -12,4 +13,8 @@ var Slack = require('slack-client'), slack = new Slack(token, true); bot = new Bot(slack); + + http.createServer(function (req, res) { + res.end('Talk to me through slack'); + }).listen(process.env.PORT || 5000); }).call(this); From 768fbe3bd6f8a1226463b72254c6b7c66f678850 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 05:38:02 -0400 Subject: [PATCH 21/59] ignore any non DMs --- src/bot.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/bot.js b/src/bot.js index 95a1a5d..8e12168 100644 --- a/src/bot.js +++ b/src/bot.js @@ -18,11 +18,17 @@ module.exports = (function () { } Bot.prototype.dispatch = function (message) { - if (!(message.user in this.conversations)) { - logger.info('new user conversation', message.user); - this.conversations[message.user] = new Conversation(this, message.channel); + var messageObject = this.service.getChannelGroupOrDMByID(message.channel); + if (messageObject.getType() === 'DM') { + if (!(message.user in this.conversations)) { + logger.info('new user conversation', message.user); + this.conversations[message.user] = new Conversation(this, message.channel); + } + this.conversations[message.user].push(message); + } else if (_.contains(message.text, '<@U09J43MQ9>')) { + logger.info(message.user, 'pinged from', messageObject.getType(), 'with message', messageObject.text); + messageObject.send('I only respond to DMs right now'); } - this.conversations[message.user].push(message); }; Bot.prototype.connected = function () { From 8a6bb4b49a00ed097825adfa8b1f03afeeee00fd Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 05:38:50 -0400 Subject: [PATCH 22/59] undefined --- src/bot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot.js b/src/bot.js index 8e12168..83cd885 100644 --- a/src/bot.js +++ b/src/bot.js @@ -26,7 +26,7 @@ module.exports = (function () { } this.conversations[message.user].push(message); } else if (_.contains(message.text, '<@U09J43MQ9>')) { - logger.info(message.user, 'pinged from', messageObject.getType(), 'with message', messageObject.text); + logger.info(message.user, 'pinged from', messageObject.getType(), 'with message', message.text); messageObject.send('I only respond to DMs right now'); } }; From cb6d2f08ad62ec0ec3894a990ecf4269d4d33b0e Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 05:39:18 -0400 Subject: [PATCH 23/59] log --- src/bot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot.js b/src/bot.js index 83cd885..a58276d 100644 --- a/src/bot.js +++ b/src/bot.js @@ -26,7 +26,7 @@ module.exports = (function () { } this.conversations[message.user].push(message); } else if (_.contains(message.text, '<@U09J43MQ9>')) { - logger.info(message.user, 'pinged from', messageObject.getType(), 'with message', message.text); + logger.info(message.user, 'pinged from', messageObject.getType(), 'by user', message.user, 'with message', message.text); messageObject.send('I only respond to DMs right now'); } }; From 8040bb5d67e91dfa3ec25b1a7a53e8c23cabaa49 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 05:45:06 -0400 Subject: [PATCH 24/59] dialog --- src/dialog.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/dialog.js b/src/dialog.js index 401d8c6..e2ce467 100644 --- a/src/dialog.js +++ b/src/dialog.js @@ -4,7 +4,8 @@ var states = [ // 0 - Initial Greeting { message: [ - "Hey, is this the first time you've been here?" + "Hey, I'm Mr. Butler. I'm currently under development, but I'm going to be greeting new members and helping them find their way around here.\n", + "Let's get started! Is this the first time you've used Slack?" ], type: Boolean, options: [ @@ -80,7 +81,8 @@ var states = [ { message: [ "Awesome, we have plenty of projects for developers.", - "You can find some cool projects over in #sites-for-sanders, #bernie-app" + "You can find some cool projects over in #sites-for-sanders, #bernie-app\n", + "You can say 'restart' to start our conversation over again" ], type: String }, @@ -89,7 +91,8 @@ var states = [ { message: [ "Awesome, we have plenty of projects for designers", - "Off hand, #car-pool could use some help right now" + "Off hand, #car-pool could use some help right now\n", + "You can say 'restart' to start our conversation over again" ], type: String }, @@ -97,7 +100,8 @@ var states = [ // 5 - Management { message: [ - "Awesome, you'll want to coordinate with @jahaz. Give him a shout!" + "Awesome, you'll want to coordinate with @jahaz. Give him a shout!\n", + "You can say 'restart' to start our conversation over again" ], type: String }, @@ -105,7 +109,8 @@ var states = [ // 6 - Other { message: [ - "Cool, give @jahaz a shout to see where you can come in!" + "Cool, give @jahaz a shout to see where you can come in!\n", + "You can say 'restart' to start our conversation over again" ], type: String } From 800a4ffb8a7fa86d3ccad436b2caf5623ffa7928 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 11:12:34 -0400 Subject: [PATCH 25/59] :lipstick: --- src/bot.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/bot.js b/src/bot.js index a58276d..f6a7105 100644 --- a/src/bot.js +++ b/src/bot.js @@ -4,12 +4,13 @@ var Slack = require('slack-client'), _ = require('lodash'); module.exports = (function () { + var CHANNEL_TYPE_DM = 'DM', + BOT_ID = 'U09J43MQ9'; // this will later be dynamic function Bot (slackClient) { this.service = slackClient; this.conversations = {}; - // this.service.on('open', this.initialize.bind(this)); this.service.on('message', this.dispatch.bind(this)); this.service.on('user_change', this.userJoined.bind(this)); @@ -19,13 +20,13 @@ module.exports = (function () { Bot.prototype.dispatch = function (message) { var messageObject = this.service.getChannelGroupOrDMByID(message.channel); - if (messageObject.getType() === 'DM') { + if (messageObject.getType() === CHANNEL_TYPE_DM) { if (!(message.user in this.conversations)) { logger.info('new user conversation', message.user); this.conversations[message.user] = new Conversation(this, message.channel); } this.conversations[message.user].push(message); - } else if (_.contains(message.text, '<@U09J43MQ9>')) { + } else if (_.contains(message.text, '<@' + BOT_ID + '>')) { logger.info(message.user, 'pinged from', messageObject.getType(), 'by user', message.user, 'with message', message.text); messageObject.send('I only respond to DMs right now'); } From fde5f4971d8cbba1890df33531da351ae7f67bd9 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 19:48:41 -0400 Subject: [PATCH 26/59] readme installation --- README.md | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 51b979b..6f04410 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,6 @@ Our welcoming Slack bot. -## Conversation - -The conversation flow can be edited in `src/dialog.js` ## Installation @@ -14,22 +11,8 @@ Install dependencies: npm install ``` -Add environment configuration -``` -touch .config.env.json -``` - -Add a slack token -```json -{ - "slack" : { - "key": "XXX" - } -} -``` - ## Running - +Start the application with `SLACK_TOKEN` as an environment variable ``` -node . +SLACK_TOKEN=xxx node . ``` From b00e94d96c3ffb4e2afcda45111fc637be88aa64 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Tue, 25 Aug 2015 20:07:58 -0400 Subject: [PATCH 27/59] readme --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 6f04410..c61b60d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Our welcoming Slack bot. +This bot will welcome new users to the Slack team by entering a dialog to determine their skillset and direct them to the proper resources. + ## Installation @@ -16,3 +18,17 @@ Start the application with `SLACK_TOKEN` as an environment variable ``` SLACK_TOKEN=xxx node . ``` + +## How it works + +The bot uses a basic finite state machine to enter a dialog with the user. + +The configuration of the state machine is defind in [dialog.js](src/dialog.js) as an array of JSON objects. + +[node-factory.js](src/node-factory.js) will construct all the objects into [nodes](src/node.js). + +The [bot](src/bot.js) will dispatch messages to specific [conversations](src/conversation.js). A [conversation](src/conversation.js) tracks the current state of a slack conversation with a user. + +As of current (800a4ffb8a7fa86d3ccad436b2caf5623ffa7928), the state machine looks as follows: + +![fsm](https://cloud.githubusercontent.com/assets/656630/9482684/7fb0be3e-4b64-11e5-98db-9da4496c74b8.jpg) From 19971e94a24c4a5d5b01efad4479a9fed27e4c2a Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 3 Sep 2015 21:03:39 -0400 Subject: [PATCH 28/59] initiate conversation when new user joins --- src/bot.js | 18 ++++++++++++++++-- src/conversation.js | 5 +++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/bot.js b/src/bot.js index f6a7105..8cbf91b 100644 --- a/src/bot.js +++ b/src/bot.js @@ -11,9 +11,8 @@ module.exports = (function () { this.service = slackClient; this.conversations = {}; - // this.service.on('open', this.initialize.bind(this)); this.service.on('message', this.dispatch.bind(this)); - this.service.on('user_change', this.userJoined.bind(this)); + this.service.on('userChange', this.userJoined.bind(this)); this.service.on('open', this.connected.bind(this)); this.service.login(); } @@ -43,7 +42,22 @@ module.exports = (function () { }; Bot.prototype.userJoined = function (event) { + var userId = event.id; + if (event.deleted || event.is_bot) { + // user account deactivation, ignore + return; + } + this.service.openDM(userId, function (response) { + var channel; + if (!response.ok) { + return; + } + channel = response.channel; + this.conversations[userId] = new Conversation(this, channel.id); + this.conversations[userId].introduce(); + logger.info(userId, 'joined - conversation initialized', channel.id); + }.bind(this)); }; return Bot; diff --git a/src/conversation.js b/src/conversation.js index 6a93519..07476da 100644 --- a/src/conversation.js +++ b/src/conversation.js @@ -37,5 +37,10 @@ module.exports = (function () { this.channel.send(response); }; + Conversation.prototype.introduce = function () { + this.node = NodeFactory.getRootNode(); + this.channel.send(this.node.getValue()); + }; + return Conversation; }).call(this); From aa3973a593e1d08625b9c793d119a0c73bf18db6 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 3 Sep 2015 21:27:20 -0400 Subject: [PATCH 29/59] the bot has a lot more to say now --- src/bot.js | 6 ++++- src/dialog.js | 61 ++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/bot.js b/src/bot.js index 8cbf91b..ee089e0 100644 --- a/src/bot.js +++ b/src/bot.js @@ -18,7 +18,11 @@ module.exports = (function () { } Bot.prototype.dispatch = function (message) { - var messageObject = this.service.getChannelGroupOrDMByID(message.channel); + var messageObject; + if (!message.user || !message.channel) { + return; + } + messageObject = this.service.getChannelGroupOrDMByID(message.channel); if (messageObject.getType() === CHANNEL_TYPE_DM) { if (!(message.user in this.conversations)) { logger.info('new user conversation', message.user); diff --git a/src/dialog.js b/src/dialog.js index e2ce467..36cff1d 100644 --- a/src/dialog.js +++ b/src/dialog.js @@ -1,21 +1,21 @@ -var channels = {}; - var states = [ // 0 - Initial Greeting { message: [ - "Hey, I'm Mr. Butler. I'm currently under development, but I'm going to be greeting new members and helping them find their way around here.\n", - "Let's get started! Is this the first time you've used Slack?" + "Hey! :wave:\n", + "Welcome to our Slack group :simple_smile:\n", + "I'm here to help get you started and point you to the right resources and people.\n", + "So let's get right to it -- have you used Slack before?" ], type: Boolean, options: [ { word: 'Yes', - state: 1, + state: 2, regex: /[y]+(es|e|a)*$/i }, { word: 'No', - state: 2, + state: 1, regex: /[n]/i } ] @@ -24,8 +24,10 @@ var states = [ // 1 - Newcomer { message: [ - "No problem, Slack is a great tool for teams. You can find a lot of documentation here on slack.com\n", - "To start, what are you interested in?" + "Not a problem! Slack is a great tool for teams. You can find a lot information on their website in how it works https://slack.com.\n", + "On the left sidebar is where you'll different channels and people. Most of our channels are made up of groups working on different projects.\n", + "Feel free to poke around and drop by different channels to see what people are working on.\n", + "In the meantime, I can help point you in the right direction. First, what are you interested in?" ], options: [ { @@ -52,8 +54,9 @@ var states = [ // 2 - Experienced { message: [ - "Great, you know your way around here then\n", - "To start, what are you interested in?" + "Great, you know your way around here then!\n", + "As you can guess, we have a handful of channels. Most of them are split up by project. Feel free to poke around and drop by different channels to see what people are working on.\n", + "In the meantime, I can help point you in the right direction. First, what are you interested in?" ], type: String, options: [ @@ -81,8 +84,20 @@ var states = [ { message: [ "Awesome, we have plenty of projects for developers.", - "You can find some cool projects over in #sites-for-sanders, #bernie-app\n", - "You can say 'restart' to start our conversation over again" + "Here's a few that I can think of broken down by platform:\n", + "*Android, iOS, and other mobile platform development*\n", + "- #bernie-app - The bernie app\n", + "\n", + "*Web application development*\n", + "- #berniestrap - Bernie Sanders theme of the Twitter Bootstrap fork\n", + "- #bus-rsvp - Reserve seats in busses to travel to a Bernie event\n", + "- #carpool-app - Organize rides to travel to a Bernie event\n", + "- #elastic-searches4bs - ElasticSearch API for all our Bernie apps\n", + "- #map-berniesanders - The map on https://map.berniesanders.com\n", + "- #sites-for-bernie - The http://forberniesanders.com project\n", + "\n\n", + "Those are just a few of the many projects we have going on.\n", + "If you'd like to learn more, feel free to reach out to @jahaz, @atticusw, @schneidmaster, @rcas, @jonculver, or @validatorian!" ], type: String }, @@ -91,8 +106,20 @@ var states = [ { message: [ "Awesome, we have plenty of projects for designers", - "Off hand, #car-pool could use some help right now\n", - "You can say 'restart' to start our conversation over again" + "Here's a few that I can think of broken down by platform:\n", + "*Android, iOS, and other mobile platform development*\n", + "- #bernie-app - The bernie app\n", + "\n", + "*Web application development*\n", + "- #berniestrap - Bernie Sanders theme of the Twitter Bootstrap fork\n", + "- #bus-rsvp - Reserve seats in busses to travel to a Bernie event\n", + "- #carpool-app - Organize rides to travel to a Bernie event\n", + "- #elastic-searches4bs - ElasticSearch API for all our Bernie apps\n", + "- #map-berniesanders - The map on https://map.berniesanders.com\n", + "- #sites-for-bernie - The http://forberniesanders.com project\n", + "\n\n", + "Those are just a few of the many projects we have going on.\n", + "If you'd like to learn more, feel free to reach out to @jahaz, @atticusw, @schneidmaster, @rcas, @jonculver, or @validatorian!" ], type: String }, @@ -100,8 +127,7 @@ var states = [ // 5 - Management { message: [ - "Awesome, you'll want to coordinate with @jahaz. Give him a shout!\n", - "You can say 'restart' to start our conversation over again" + "Awesome, you'll want to coordinate with @jahaz. Give him a shout!\n" ], type: String }, @@ -109,8 +135,7 @@ var states = [ // 6 - Other { message: [ - "Cool, give @jahaz a shout to see where you can come in!\n", - "You can say 'restart' to start our conversation over again" + "Cool, give @jahaz a shout to see where you can come in!\n" ], type: String } From 5310c846a414850a8c537c1cd96ead4a1a06fdef Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 3 Sep 2015 21:29:35 -0400 Subject: [PATCH 30/59] more to say --- src/dialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dialog.js b/src/dialog.js index 36cff1d..0777104 100644 --- a/src/dialog.js +++ b/src/dialog.js @@ -96,7 +96,7 @@ var states = [ "- #map-berniesanders - The map on https://map.berniesanders.com\n", "- #sites-for-bernie - The http://forberniesanders.com project\n", "\n\n", - "Those are just a few of the many projects we have going on.\n", + "Those are just a few of the many projects we have going on. You will find new ideas brewing in #pitch-zone\n", "If you'd like to learn more, feel free to reach out to @jahaz, @atticusw, @schneidmaster, @rcas, @jonculver, or @validatorian!" ], type: String @@ -118,7 +118,7 @@ var states = [ "- #map-berniesanders - The map on https://map.berniesanders.com\n", "- #sites-for-bernie - The http://forberniesanders.com project\n", "\n\n", - "Those are just a few of the many projects we have going on.\n", + "Those are just a few of the many projects we have going on. You will find new ideas brewing in #pitch-zone\n", "If you'd like to learn more, feel free to reach out to @jahaz, @atticusw, @schneidmaster, @rcas, @jonculver, or @validatorian!" ], type: String From 23343cc060864a32883eb2c9c7572b5425b17a29 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 3 Sep 2015 21:31:25 -0400 Subject: [PATCH 31/59] better logging --- src/bot.js | 10 ++++++---- src/conversation.js | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/bot.js b/src/bot.js index ee089e0..a754780 100644 --- a/src/bot.js +++ b/src/bot.js @@ -18,19 +18,21 @@ module.exports = (function () { } Bot.prototype.dispatch = function (message) { - var messageObject; + var messageObject, + user; if (!message.user || !message.channel) { return; } + user = this.service.getUserByID(message.user); messageObject = this.service.getChannelGroupOrDMByID(message.channel); if (messageObject.getType() === CHANNEL_TYPE_DM) { if (!(message.user in this.conversations)) { - logger.info('new user conversation', message.user); + logger.info('new user conversation', message.user, '(', user.name, ')'); this.conversations[message.user] = new Conversation(this, message.channel); } this.conversations[message.user].push(message); } else if (_.contains(message.text, '<@' + BOT_ID + '>')) { - logger.info(message.user, 'pinged from', messageObject.getType(), 'by user', message.user, 'with message', message.text); + logger.info(message.user, '(', user.name, ') pinged from', messageObject.getType(), 'with message', message.text); messageObject.send('I only respond to DMs right now'); } }; @@ -60,7 +62,7 @@ module.exports = (function () { channel = response.channel; this.conversations[userId] = new Conversation(this, channel.id); this.conversations[userId].introduce(); - logger.info(userId, 'joined - conversation initialized', channel.id); + logger.info(userId, '(', event.name, ') joined - conversation initialized', channel.id); }.bind(this)); }; diff --git a/src/conversation.js b/src/conversation.js index 07476da..f1f9932 100644 --- a/src/conversation.js +++ b/src/conversation.js @@ -19,9 +19,10 @@ module.exports = (function () { */ Conversation.prototype.push = function (message) { var transitionNode, + user = this.delegate.service.getUserByID(message.user), response = 'Try again, I did not understand'; - logger.info(message.user, 'sent', message.text, 'in response to node', this.node ? this.node.state : 'initial node'); + logger.info(message.user, '(', user.name, ') sent', message.text, 'in response to node', this.node ? this.node.state : 'initial node'); if (this.node === null) { // the converation has not yet started, grab the root (welcome) node this.node = NodeFactory.getRootNode(); From 5a033b8baf496b45060580839f97efa9985e9c79 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 3 Sep 2015 22:49:24 -0400 Subject: [PATCH 32/59] formatting filters for channel names and user names --- src/conversation.js | 6 +++++ src/filters.js | 65 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/filters.js diff --git a/src/conversation.js b/src/conversation.js index f1f9932..f140ea2 100644 --- a/src/conversation.js +++ b/src/conversation.js @@ -1,4 +1,5 @@ var NodeFactory = require('./node-factory'), + Filters = require('./filters'), logger = require('log4js').getLogger('conversation'); module.exports = (function () { @@ -34,6 +35,11 @@ module.exports = (function () { // respond with the next node message response = this.node.getValue(); } + + // apply any escape rules to the message + // @link https://api.slack.com/docs/formatting + response = Filters.escapeMessage(response, this.delegate.service); + // send the node's message to the user this.channel.send(response); }; diff --git a/src/filters.js b/src/filters.js new file mode 100644 index 0000000..4818441 --- /dev/null +++ b/src/filters.js @@ -0,0 +1,65 @@ +var _ = require('lodash'); + +module.exports = (function () { + var CHANNEL_REGEX = /(#\w+-*\w+)/g, + USER_REGEX = /(@\w+)/g, + TOKEN = { + OPEN: '<', + CLOSE: '>', + CHANNEL: '#', + USER: '@', + SEPARATOR: '|' + }, + service = {}; + + service.escapeChannel = function (name, client) { + var channel = client.getChannelByName(name); + if (!channel || !channel.id) { + console.log('no channel', name); + return name; + } + return [ + TOKEN.OPEN, + TOKEN.CHANNEL, + channel.id, + TOKEN.SEPARATOR, + name, + TOKEN.CLOSE + ].join(''); + }; + + service.escapeUser = function (name, client) { + var user = client.getUserByName(name); + if (!user || !user.id) { + return name; + } + return [ + TOKEN.OPEN, + TOKEN.USER, + user.id, + TOKEN.SEPARATOR, + name, + TOKEN.CLOSE + ].join(''); + }; + + service.escapeMessage = function (message, client) { + var messageBits = message.split(' '); + return _.map(messageBits, function (word) { + var match = null, + filtered = null; + + if (match = CHANNEL_REGEX.exec(word)) { + filtered = service.escapeChannel(match[1].substring(1, match[1].length), client); + } else if (match = USER_REGEX.exec(word)) { + filtered = service.escapeUser(match[1].substring(1, match[1].length), client); + } + if (filtered) { + word = word.replace(match[1], filtered); + } + return word; + }).join(' '); + }; + + return service; +}).call(this); From 3b9b256826c303863b615733f1b74721805ff7d0 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 3 Sep 2015 22:55:26 -0400 Subject: [PATCH 33/59] regex --- src/filters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filters.js b/src/filters.js index 4818441..4c9315e 100644 --- a/src/filters.js +++ b/src/filters.js @@ -1,7 +1,7 @@ var _ = require('lodash'); module.exports = (function () { - var CHANNEL_REGEX = /(#\w+-*\w+)/g, + var CHANNEL_REGEX = /(#[\w\-]+)/g, USER_REGEX = /(@\w+)/g, TOKEN = { OPEN: '<', From 6bff6b7b79335c2ac8358e35c6aaf6d7e07a467a Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 3 Sep 2015 22:56:18 -0400 Subject: [PATCH 34/59] channel typo --- src/dialog.js | 8 ++++---- src/filters.js | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/dialog.js b/src/dialog.js index 0777104..8ba6f12 100644 --- a/src/dialog.js +++ b/src/dialog.js @@ -92,12 +92,12 @@ var states = [ "- #berniestrap - Bernie Sanders theme of the Twitter Bootstrap fork\n", "- #bus-rsvp - Reserve seats in busses to travel to a Bernie event\n", "- #carpool-app - Organize rides to travel to a Bernie event\n", - "- #elastic-searches4bs - ElasticSearch API for all our Bernie apps\n", + "- #elastic-search-es4bs - ElasticSearch API for all our Bernie apps\n", "- #map-berniesanders - The map on https://map.berniesanders.com\n", "- #sites-for-bernie - The http://forberniesanders.com project\n", "\n\n", "Those are just a few of the many projects we have going on. You will find new ideas brewing in #pitch-zone\n", - "If you'd like to learn more, feel free to reach out to @jahaz, @atticusw, @schneidmaster, @rcas, @jonculver, or @validatorian!" + "If you'd like to learn more, feel free to reach out to some of our mods @jahaz, @atticusw, @schneidmaster, @rcas, @jonculver, or @validatorian!" ], type: String }, @@ -114,12 +114,12 @@ var states = [ "- #berniestrap - Bernie Sanders theme of the Twitter Bootstrap fork\n", "- #bus-rsvp - Reserve seats in busses to travel to a Bernie event\n", "- #carpool-app - Organize rides to travel to a Bernie event\n", - "- #elastic-searches4bs - ElasticSearch API for all our Bernie apps\n", + "- #elastic-search-es4bs - ElasticSearch API for all our Bernie apps\n", "- #map-berniesanders - The map on https://map.berniesanders.com\n", "- #sites-for-bernie - The http://forberniesanders.com project\n", "\n\n", "Those are just a few of the many projects we have going on. You will find new ideas brewing in #pitch-zone\n", - "If you'd like to learn more, feel free to reach out to @jahaz, @atticusw, @schneidmaster, @rcas, @jonculver, or @validatorian!" + "If you'd like to learn more, feel free to reach out to some of our mods @jahaz, @atticusw, @schneidmaster, @rcas, @jonculver, or @validatorian!" ], type: String }, diff --git a/src/filters.js b/src/filters.js index 4c9315e..bbeeba6 100644 --- a/src/filters.js +++ b/src/filters.js @@ -15,7 +15,7 @@ module.exports = (function () { service.escapeChannel = function (name, client) { var channel = client.getChannelByName(name); if (!channel || !channel.id) { - console.log('no channel', name); + console.log('no channel found for filter', name); return name; } return [ @@ -31,6 +31,7 @@ module.exports = (function () { service.escapeUser = function (name, client) { var user = client.getUserByName(name); if (!user || !user.id) { + console.log('no user found for filter', name); return name; } return [ From b28dff766b49bff60a8a67acc2f434af8c57a62d Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 3 Sep 2015 23:15:48 -0400 Subject: [PATCH 35/59] logging --- src/filters.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/filters.js b/src/filters.js index bbeeba6..6859212 100644 --- a/src/filters.js +++ b/src/filters.js @@ -1,4 +1,5 @@ -var _ = require('lodash'); +var _ = require('lodash'), + logger = require('log4js').getLogger('filter'); module.exports = (function () { var CHANNEL_REGEX = /(#[\w\-]+)/g, @@ -15,7 +16,7 @@ module.exports = (function () { service.escapeChannel = function (name, client) { var channel = client.getChannelByName(name); if (!channel || !channel.id) { - console.log('no channel found for filter', name); + logger.warn('no channel found for filter', name); return name; } return [ @@ -31,7 +32,7 @@ module.exports = (function () { service.escapeUser = function (name, client) { var user = client.getUserByName(name); if (!user || !user.id) { - console.log('no user found for filter', name); + logger.warn('no user found for filter', name); return name; } return [ From 9211343b8d9b4d3069432237e96d8f19ee0ca93d Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 3 Sep 2015 23:27:32 -0400 Subject: [PATCH 36/59] dont leave channels, use dynamic ID --- src/bot.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/bot.js b/src/bot.js index a754780..fc771d7 100644 --- a/src/bot.js +++ b/src/bot.js @@ -4,8 +4,7 @@ var Slack = require('slack-client'), _ = require('lodash'); module.exports = (function () { - var CHANNEL_TYPE_DM = 'DM', - BOT_ID = 'U09J43MQ9'; // this will later be dynamic + var CHANNEL_TYPE_DM = 'DM'; function Bot (slackClient) { this.service = slackClient; @@ -31,7 +30,7 @@ module.exports = (function () { this.conversations[message.user] = new Conversation(this, message.channel); } this.conversations[message.user].push(message); - } else if (_.contains(message.text, '<@' + BOT_ID + '>')) { + } else if (_.contains(message.text, '<@' + this.service.self.id + '>')) { logger.info(message.user, '(', user.name, ') pinged from', messageObject.getType(), 'with message', message.text); messageObject.send('I only respond to DMs right now'); } @@ -40,11 +39,6 @@ module.exports = (function () { Bot.prototype.connected = function () { // leave all channels that the bot may have been added to logger.info('connection successful'); - _.filter(this.service.channels, function (channel) { - return channel.is_member; - }).forEach(function (channel) { - channel.leave(); - }); }; Bot.prototype.userJoined = function (event) { From 61e4c2ed686ca0c7b6508e11f7753908e319b300 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 3 Sep 2015 23:33:21 -0400 Subject: [PATCH 37/59] dispatch restructuring --- src/bot.js | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/bot.js b/src/bot.js index fc771d7..33b803a 100644 --- a/src/bot.js +++ b/src/bot.js @@ -4,7 +4,8 @@ var Slack = require('slack-client'), _ = require('lodash'); module.exports = (function () { - var CHANNEL_TYPE_DM = 'DM'; + var CHANNEL_TYPE_DM = 'DM', + CHANNEL_TYPE_CHANNEL = 'Channel'; function Bot (slackClient) { this.service = slackClient; @@ -22,18 +23,29 @@ module.exports = (function () { if (!message.user || !message.channel) { return; } + user = this.service.getUserByID(message.user); messageObject = this.service.getChannelGroupOrDMByID(message.channel); + if (messageObject.getType() === CHANNEL_TYPE_DM) { - if (!(message.user in this.conversations)) { - logger.info('new user conversation', message.user, '(', user.name, ')'); - this.conversations[message.user] = new Conversation(this, message.channel); - } - this.conversations[message.user].push(message); - } else if (_.contains(message.text, '<@' + this.service.self.id + '>')) { - logger.info(message.user, '(', user.name, ') pinged from', messageObject.getType(), 'with message', message.text); - messageObject.send('I only respond to DMs right now'); + this.respondToDM(user, message); + } else if (messageObject.getType() === CHANNEL_TYPE_CHANNEL && + _.contains(message.text, '<@' + this.service.self.id + '>')) { + this.respondToMention(user, message, messageObject); + } + }; + + Bot.prototype.respondToDM = function (user, message) { + if (!(message.user in this.conversations)) { + logger.info('new user conversation', message.user, '(', user.name, ')'); + this.conversations[message.user] = new Conversation(this, message.channel); } + this.conversations[message.user].push(message); + }; + + Bot.prototype.respondToMention = function (user, message, channel) { + channel.send('I only respond to DMs right now'); + logger.info(message.user, '(', user.name, ') pinged from', channel.getType(), 'with message', message.text); }; Bot.prototype.connected = function () { From f9ddbbfeb1526a6d3f4175aacc082657bb0d6d2c Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 3 Sep 2015 23:44:06 -0400 Subject: [PATCH 38/59] interpretter service --- src/bot.js | 9 ++++++++- src/interpretter.js | 13 +++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/interpretter.js diff --git a/src/bot.js b/src/bot.js index 33b803a..fab7ee3 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,5 +1,6 @@ var Slack = require('slack-client'), Conversation = require('./conversation'), + Interpretter = require('./interpretter'), logger = require('log4js').getLogger('bot'), _ = require('lodash'); @@ -44,7 +45,13 @@ module.exports = (function () { }; Bot.prototype.respondToMention = function (user, message, channel) { - channel.send('I only respond to DMs right now'); + // channel.send('I only respond to DMs right now'); + if (Interpretter.isLookingForHelp(message.text)) { + channel.send('What do you need help with?'); + } else if (Interpretter.isAskingForHelp(message.text)) { + channel.send('Let me find you someone who could use your help'); + } + logger.info(message.user, '(', user.name, ') pinged from', channel.getType(), 'with message', message.text); }; diff --git a/src/interpretter.js b/src/interpretter.js new file mode 100644 index 0000000..d1e8435 --- /dev/null +++ b/src/interpretter.js @@ -0,0 +1,13 @@ +module.exports = (function () { + var service = {}; + + service.isLookingForHelp = function (message) { + return message.indexOf('I need help') > -1; + }; + + service.isAskingForHelp = function (message) { + return message.indexOf('anyone need help') > -1; + }; + + return service; +}).call(this); From 3ae1f73adeaa55b061bed55caeb61f24e78e9cfe Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 3 Sep 2015 23:55:27 -0400 Subject: [PATCH 39/59] Bluebird and Redis --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index d10db74..f1ce5b8 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,10 @@ "author": "Atticus White (http://atticuswhite.com/)", "license": "MIT", "dependencies": { + "bluebird": "^2.9.34", "lodash": "^3.10.1", "log4js": "^0.6.26", + "redis": "^1.0.0", "slack-client": "^1.4.1" } } From fde2399844f5934eb4f8e1c7bd94792be30c945b Mon Sep 17 00:00:00 2001 From: Atticus White Date: Thu, 3 Sep 2015 23:55:31 -0400 Subject: [PATCH 40/59] task coordinator --- index.js | 7 ++++++- src/bot.js | 13 ++++++++----- src/task-coordinator.js | 15 +++++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 src/task-coordinator.js diff --git a/index.js b/index.js index f92f211..270fcd6 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,13 @@ var Slack = require('slack-client'), + Redis = require('redis'), + Promise = require('bluebird'), Bot = require('./src/bot'), http = require('http'); (function () { var token = process.env.SLACK_TOKEN, slack, + redis, bot; if (!token) { @@ -12,7 +15,9 @@ var Slack = require('slack-client'), } slack = new Slack(token, true); - bot = new Bot(slack); + redis = Redis.createClient({detect_buffers: true}); + redis = Promise.promisifyAll(redis); + bot = new Bot(slack, redis); http.createServer(function (req, res) { res.end('Talk to me through slack'); diff --git a/src/bot.js b/src/bot.js index fab7ee3..600c707 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,6 +1,9 @@ var Slack = require('slack-client'), Conversation = require('./conversation'), Interpretter = require('./interpretter'), + TaskCoordinator = require('./task-coordinator'), + Promise = require('bluebird'), + Redis = require('redis'), logger = require('log4js').getLogger('bot'), _ = require('lodash'); @@ -8,8 +11,10 @@ module.exports = (function () { var CHANNEL_TYPE_DM = 'DM', CHANNEL_TYPE_CHANNEL = 'Channel'; - function Bot (slackClient) { + function Bot (slackClient, redisClient) { this.service = slackClient; + this.redisClient = redisClient; + this.taskCoordinator = new TaskCoordinator(this) this.conversations = {}; this.service.on('message', this.dispatch.bind(this)); @@ -45,13 +50,11 @@ module.exports = (function () { }; Bot.prototype.respondToMention = function (user, message, channel) { - // channel.send('I only respond to DMs right now'); if (Interpretter.isLookingForHelp(message.text)) { - channel.send('What do you need help with?'); + this.taskCoordinator.requestHelp(user, message, channel); } else if (Interpretter.isAskingForHelp(message.text)) { - channel.send('Let me find you someone who could use your help'); + this.taskCoordinator.provideHelp(user, message, channel); } - logger.info(message.user, '(', user.name, ') pinged from', channel.getType(), 'with message', message.text); }; diff --git a/src/task-coordinator.js b/src/task-coordinator.js new file mode 100644 index 0000000..e6e41dc --- /dev/null +++ b/src/task-coordinator.js @@ -0,0 +1,15 @@ +module.exports = (function () { + function TaskCoordinator (delegate) { + this.delegate = delegate; + } + + TaskCoordinator.prototype.requestHelp = function (user, message, channel) { + channel.send('What do you need help with?'); + }; + + TaskCoordinator.prototype.provideHelp = function (user, message, channel) { + channel.send('Let me find you someone who could use your help'); + }; + + return TaskCoordinator; +}).call(this); From a78666b195ecad2e6327845d3960f4d3410a921b Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 00:40:53 -0400 Subject: [PATCH 41/59] promisify redis endpoints --- index.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 270fcd6..4a3c183 100644 --- a/index.js +++ b/index.js @@ -14,9 +14,23 @@ var Slack = require('slack-client'), throw new Error('Slack token is required. Please provide SLACK_TOKEN as an environment variable'); } - slack = new Slack(token, true); redis = Redis.createClient({detect_buffers: true}); - redis = Promise.promisifyAll(redis); + + // promisify lists + redis.lpushAsync = Promise.promisify(redis.lpush); + redis.lrangeAsync = Promise.promisify(redis.lrange); + redis.lremAsync = Promise.promisify(redis.lrem); + + // promisify hashes + redis.hmsetAsync = Promise.promisify(redis.hmset); + redis.hmgetAsync = Promise.promisify(redis.hmget); + redis.hgetallAsync = Promise.promisify(redis.hgetall); + + // promisify all + redis.getAsync = Promise.promisify(redis.get); + redis.setAsync = Promise.promisify(redis.set); + + slack = new Slack(token, true); bot = new Bot(slack, redis); http.createServer(function (req, res) { From 53d39b2c6ba2574aa4622a7fea898d1e121e4ee5 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 00:41:08 -0400 Subject: [PATCH 42/59] store help requests --- src/interpretter.js | 2 +- src/task-coordinator.js | 28 +++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/interpretter.js b/src/interpretter.js index d1e8435..e180177 100644 --- a/src/interpretter.js +++ b/src/interpretter.js @@ -2,7 +2,7 @@ module.exports = (function () { var service = {}; service.isLookingForHelp = function (message) { - return message.indexOf('I need help') > -1; + return message.indexOf('i need help') > -1; }; service.isAskingForHelp = function (message) { diff --git a/src/task-coordinator.js b/src/task-coordinator.js index e6e41dc..9d19af7 100644 --- a/src/task-coordinator.js +++ b/src/task-coordinator.js @@ -1,14 +1,40 @@ +var _ = require('lodash'); + module.exports = (function () { function TaskCoordinator (delegate) { this.delegate = delegate; } TaskCoordinator.prototype.requestHelp = function (user, message, channel) { - channel.send('What do you need help with?'); + var key = 'help:user:' + user.id; + + this.delegate.redisClient.hmsetAsync(key, { + user: user.id, + channel: channel.id, + message: message.text + }); + + this.delegate.redisClient.lremAsync('help', 0, key).finally(function () { + return this.delegate.redisClient.lpush('help', key); + }.bind(this)); + channel.send('We\'ll find you some help!'); }; TaskCoordinator.prototype.provideHelp = function (user, message, channel) { channel.send('Let me find you someone who could use your help'); + this.delegate.redisClient.lrangeAsync('help', 0, 10).then(function (keys) { + var lookups = _.map(keys, function (key) { + return this.delegate.redisClient.hgetallAsync(key); + }.bind(this)); + return Promise.all(lookups); + }.bind(this)).then(function (helpRequests) { + var message = _.map(helpRequests, function (request) { + return 'User ' + request.user + ' needed help in ' + request.channel; + }); + channel.send(message.join('\n')); + }).catch(function (error) { + console.log('error', error); + }) }; return TaskCoordinator; From e2e5195e1aff38ddd80790f81baa4609ec0c76b4 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 00:43:19 -0400 Subject: [PATCH 43/59] filter method name updates --- src/filters.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/filters.js b/src/filters.js index 6859212..81fd348 100644 --- a/src/filters.js +++ b/src/filters.js @@ -13,7 +13,7 @@ module.exports = (function () { }, service = {}; - service.escapeChannel = function (name, client) { + service.escapeChannelByName = function (name, client) { var channel = client.getChannelByName(name); if (!channel || !channel.id) { logger.warn('no channel found for filter', name); @@ -29,7 +29,7 @@ module.exports = (function () { ].join(''); }; - service.escapeUser = function (name, client) { + service.escapeUserByName = function (name, client) { var user = client.getUserByName(name); if (!user || !user.id) { logger.warn('no user found for filter', name); @@ -52,9 +52,9 @@ module.exports = (function () { filtered = null; if (match = CHANNEL_REGEX.exec(word)) { - filtered = service.escapeChannel(match[1].substring(1, match[1].length), client); + filtered = service.escapeChannelByName(match[1].substring(1, match[1].length), client); } else if (match = USER_REGEX.exec(word)) { - filtered = service.escapeUser(match[1].substring(1, match[1].length), client); + filtered = service.escapeUserByName(match[1].substring(1, match[1].length), client); } if (filtered) { word = word.replace(match[1], filtered); From 715b2f37bf612182516eceaa88b6875d284154e7 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 00:47:01 -0400 Subject: [PATCH 44/59] escape by ID --- src/filters.js | 55 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/src/filters.js b/src/filters.js index 81fd348..385f279 100644 --- a/src/filters.js +++ b/src/filters.js @@ -13,36 +13,59 @@ module.exports = (function () { }, service = {}; - service.escapeChannelByName = function (name, client) { - var channel = client.getChannelByName(name); - if (!channel || !channel.id) { - logger.warn('no channel found for filter', name); - return name; - } + service.escape = function (id, name, token) { return [ TOKEN.OPEN, - TOKEN.CHANNEL, - channel.id, + token, + id, TOKEN.SEPARATOR, name, TOKEN.CLOSE ].join(''); }; + service.escapeChannel = function (id, name) { + return service.escape(id, name, TOKEN.CHANNEL); + }; + + service.escapeUser = function (id, name) { + return service.escape(id, name, TOKEN.USER); + } + + service.escapeChannelByName = function (name, client) { + var channel = client.getChannelByName(name); + if (!channel || !channel.id) { + logger.warn('no channel found for filter', name); + return name; + } + return service.escapeChannel(channel.id, name); + }; + service.escapeUserByName = function (name, client) { var user = client.getUserByName(name); if (!user || !user.id) { logger.warn('no user found for filter', name); return name; } - return [ - TOKEN.OPEN, - TOKEN.USER, - user.id, - TOKEN.SEPARATOR, - name, - TOKEN.CLOSE - ].join(''); + return service.escapeUser(user.id, name); + }; + + service.escapeUserById = function (id, client) { + var user = client.getUserByID(id); + if (!user || !user.name) { + logger.warn('no user found for filter', id); + return id; + } + return service.escapeUser(id, user.name); + }; + + service.escapeChannelById = function (id, client) { + var channel = client.getChannelByID(id); + if (!channel || !channel.name) { + logger.warn('no channel found for filter', id); + return id; + } + return service.escapeChannel(id, channel.name); }; service.escapeMessage = function (message, client) { From 3be1cf10605f56f309bbef2ef4d046198699e32d Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 00:52:01 -0400 Subject: [PATCH 45/59] message formatting for displaying open tasks --- src/task-coordinator.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/task-coordinator.js b/src/task-coordinator.js index 9d19af7..e4949bb 100644 --- a/src/task-coordinator.js +++ b/src/task-coordinator.js @@ -1,4 +1,5 @@ -var _ = require('lodash'); +var _ = require('lodash'), + Filter = require('./filters'); module.exports = (function () { function TaskCoordinator (delegate) { @@ -11,7 +12,8 @@ module.exports = (function () { this.delegate.redisClient.hmsetAsync(key, { user: user.id, channel: channel.id, - message: message.text + message: message.text, + date: (new Date()).getTime() }); this.delegate.redisClient.lremAsync('help', 0, key).finally(function () { @@ -21,19 +23,23 @@ module.exports = (function () { }; TaskCoordinator.prototype.provideHelp = function (user, message, channel) { - channel.send('Let me find you someone who could use your help'); this.delegate.redisClient.lrangeAsync('help', 0, 10).then(function (keys) { var lookups = _.map(keys, function (key) { return this.delegate.redisClient.hgetallAsync(key); }.bind(this)); return Promise.all(lookups); }.bind(this)).then(function (helpRequests) { - var message = _.map(helpRequests, function (request) { - return 'User ' + request.user + ' needed help in ' + request.channel; - }); - channel.send(message.join('\n')); - }).catch(function (error) { + var messages = _.map(helpRequests, function (request) { + console.log('request', request); + var message = 'User ' + Filter.escapeUserById(request.user, this.delegate.service) + ' needed help in ' + Filter.escapeChannelById(request.channel, this.delegate.service); + message += '\n'; + message += '> ' + request.message.replace('<@U09KH1WV8>', ''); + return message; + }.bind(this)); + channel.send(messages.join('\n')); + }.bind(this)).catch(function (error) { console.log('error', error); + channel.send('I had some trouble looking up help requests'); }) }; From fc55d078c2f4cddf49865195090fea131d112ff4 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 00:57:24 -0400 Subject: [PATCH 46/59] dates --- package.json | 1 + src/task-coordinator.js | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index f1ce5b8..e0b3a7c 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "bluebird": "^2.9.34", "lodash": "^3.10.1", "log4js": "^0.6.26", + "moment": "^2.10.6", "redis": "^1.0.0", "slack-client": "^1.4.1" } diff --git a/src/task-coordinator.js b/src/task-coordinator.js index e4949bb..1bbb599 100644 --- a/src/task-coordinator.js +++ b/src/task-coordinator.js @@ -1,4 +1,5 @@ var _ = require('lodash'), + moment = require('moment'), Filter = require('./filters'); module.exports = (function () { @@ -13,7 +14,7 @@ module.exports = (function () { user: user.id, channel: channel.id, message: message.text, - date: (new Date()).getTime() + date: moment().unix() }); this.delegate.redisClient.lremAsync('help', 0, key).finally(function () { @@ -30,11 +31,15 @@ module.exports = (function () { return Promise.all(lookups); }.bind(this)).then(function (helpRequests) { var messages = _.map(helpRequests, function (request) { - console.log('request', request); - var message = 'User ' + Filter.escapeUserById(request.user, this.delegate.service) + ' needed help in ' + Filter.escapeChannelById(request.channel, this.delegate.service); - message += '\n'; - message += '> ' + request.message.replace('<@U09KH1WV8>', ''); - return message; + return [ + Filter.escapeUserById(request.user, this.delegate.service), + 'neeeded help in', + Filter.escapeChannelById(request.channel, this.delegate.service), + moment.unix(request.date).from(moment()) + '.', + '\n', + '>', + request.message.replace('<@' + this.delegate.service.self.id + '>', '') + ].join(' '); }.bind(this)); channel.send(messages.join('\n')); }.bind(this)).catch(function (error) { From 9c73f4e3d95316e73b57be55464bfef02fef1f2a Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 01:02:16 -0400 Subject: [PATCH 47/59] resolve promises --- src/task-coordinator.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/task-coordinator.js b/src/task-coordinator.js index 1bbb599..fec6f15 100644 --- a/src/task-coordinator.js +++ b/src/task-coordinator.js @@ -8,18 +8,26 @@ module.exports = (function () { } TaskCoordinator.prototype.requestHelp = function (user, message, channel) { - var key = 'help:user:' + user.id; + var key = 'help:user:' + user.id, + promises = []; - this.delegate.redisClient.hmsetAsync(key, { + promises.push(this.delegate.redisClient.hmsetAsync(key, { user: user.id, channel: channel.id, message: message.text, date: moment().unix() - }); + })); - this.delegate.redisClient.lremAsync('help', 0, key).finally(function () { + promises.push(this.delegate.redisClient.lremAsync('help', 0, key).finally(function () { return this.delegate.redisClient.lpush('help', key); - }.bind(this)); + }.bind(this))); + + Promise.all(promises).then(function () { + channel.send('I\'ll let someone know the next time they ask!'); + }).catch(function (error) { + channel.send('Sorry! having an issue right now processing help requests'); + console.log('error requesting help', error); + }); channel.send('We\'ll find you some help!'); }; From a274c99221ae09f1f38eae81ebddb953ed8e3e81 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 01:04:54 -0400 Subject: [PATCH 48/59] :fire: second message --- src/task-coordinator.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/task-coordinator.js b/src/task-coordinator.js index fec6f15..d641134 100644 --- a/src/task-coordinator.js +++ b/src/task-coordinator.js @@ -28,7 +28,6 @@ module.exports = (function () { channel.send('Sorry! having an issue right now processing help requests'); console.log('error requesting help', error); }); - channel.send('We\'ll find you some help!'); }; TaskCoordinator.prototype.provideHelp = function (user, message, channel) { From 124a69fd703d367ffa1fa68a89bee150dd771cfb Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 01:08:31 -0400 Subject: [PATCH 49/59] typo --- src/task-coordinator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/task-coordinator.js b/src/task-coordinator.js index d641134..78a91e3 100644 --- a/src/task-coordinator.js +++ b/src/task-coordinator.js @@ -40,7 +40,7 @@ module.exports = (function () { var messages = _.map(helpRequests, function (request) { return [ Filter.escapeUserById(request.user, this.delegate.service), - 'neeeded help in', + 'needed help in', Filter.escapeChannelById(request.channel, this.delegate.service), moment.unix(request.date).from(moment()) + '.', '\n', From 8bf740d8f181d688df723f94343a6e1385ff7151 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 02:03:51 -0400 Subject: [PATCH 50/59] comments --- src/interpretter.js | 4 ++-- src/task-coordinator.js | 43 +++++++++++++++++++++++++++++++++++------ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/interpretter.js b/src/interpretter.js index e180177..777f72b 100644 --- a/src/interpretter.js +++ b/src/interpretter.js @@ -2,11 +2,11 @@ module.exports = (function () { var service = {}; service.isLookingForHelp = function (message) { - return message.indexOf('i need help') > -1; + return message.toLowerCase().indexOf('i need help') > -1; }; service.isAskingForHelp = function (message) { - return message.indexOf('anyone need help') > -1; + return message.toLoserCase().indexOf('anyone need help') > -1; }; return service; diff --git a/src/task-coordinator.js b/src/task-coordinator.js index 78a91e3..804c5ea 100644 --- a/src/task-coordinator.js +++ b/src/task-coordinator.js @@ -1,16 +1,30 @@ var _ = require('lodash'), moment = require('moment'), + logger = require('log4js').getLogger('task-coordinator'), Filter = require('./filters'); module.exports = (function () { + var HELP_LIST_KEY = 'help', + HELP_USER_HASHMAP_KEY_PREFIX = HELP_LIST_KEY + ':' + 'user:', + HELP_LIST_LIMIT_LOWER = 0, + HELP_LIST_LIMIT_UPPER = 10; + function TaskCoordinator (delegate) { this.delegate = delegate; } + /** + * Create a help request + * @param {Object} user The user creating the help request + * @param {Object} message The message the user posted + * @param {Object} channel The channel the help request was created in + * @return {Object} Promise + */ TaskCoordinator.prototype.requestHelp = function (user, message, channel) { - var key = 'help:user:' + user.id, + var key = HELP_USER_HASHMAP_KEY_PREFIX + user.id, promises = []; + // store the help request keyed by the user promises.push(this.delegate.redisClient.hmsetAsync(key, { user: user.id, channel: channel.id, @@ -18,20 +32,35 @@ module.exports = (function () { date: moment().unix() })); - promises.push(this.delegate.redisClient.lremAsync('help', 0, key).finally(function () { - return this.delegate.redisClient.lpush('help', key); + // remove any existing help requests by the user in the help collection. + // prevents any duplicated hashmap keys in the collection + promises.push(this.delegate.redisClient.lremAsync(HELP_LIST_KEY, 0, key).finally(function () { + // add the help request hashmap key + return this.delegate.redisClient.lpush(HELP_LIST_KEY, key); }.bind(this))); - Promise.all(promises).then(function () { + // wait for all redis transactions to complete + return Promise.all(promises).then(function () { + // transactions completed successfully, acknoweldge channel.send('I\'ll let someone know the next time they ask!'); + logger.info('help request stored for user', user.id, '(' + user.name + ')') }).catch(function (error) { + // transactions failed, acknoweldge channel.send('Sorry! having an issue right now processing help requests'); - console.log('error requesting help', error); + logger.error('problem storing help request for user', user.id, '('+ user.name + ')', error); }); }; + /** + * [provideHelp description] + * @param {[type]} user [description] + * @param {[type]} message [description] + * @param {[type]} channel [description] + * @return {[type]} [description] + */ TaskCoordinator.prototype.provideHelp = function (user, message, channel) { - this.delegate.redisClient.lrangeAsync('help', 0, 10).then(function (keys) { + this.delegate.redisClient.lrangeAsync(HELP_LIST_KEY, HELP_LIST_LIMIT_LOWER, HELP_LIST_LIMIT_UPPER) + .then(function (keys) { var lookups = _.map(keys, function (key) { return this.delegate.redisClient.hgetallAsync(key); }.bind(this)); @@ -49,8 +78,10 @@ module.exports = (function () { ].join(' '); }.bind(this)); channel.send(messages.join('\n')); + logger.info('help requests reported for user', user.id, '(' + user.name + ')'); }.bind(this)).catch(function (error) { console.log('error', error); + logger.error('problem fetching help requests for user', user.id, '(' + user.name + ')', error); channel.send('I had some trouble looking up help requests'); }) }; From 6cd268634da797a1b4082d934b2ab44e80119041 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 02:16:15 -0400 Subject: [PATCH 51/59] task coordinator data service --- src/services/task-coordinator-data.js | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/services/task-coordinator-data.js diff --git a/src/services/task-coordinator-data.js b/src/services/task-coordinator-data.js new file mode 100644 index 0000000..ab59fdc --- /dev/null +++ b/src/services/task-coordinator-data.js @@ -0,0 +1,50 @@ +var Promise = require('bluebird'), + moment = require('moment'), + _ = require('lodash'); + +module.exports = (function () { + var HELP_LIST_KEY = 'help', + HELP_USER_HASHMAP_KEY_PREFIX = HELP_LIST_KEY + ':' + 'user:', + HELP_LIST_LIMIT_LOWER = 0, + HELP_LIST_LIMIT_UPPER = 10; + + function TaskCoordinatorDataService (redis) { + this.redis = redis; + } + + TaskCoordinatorDataService.prototype.createHelpRequest = function (userId, channelId, message) { + var key = HELP_USER_HASHMAP_KEY_PREFIX + userId, + promises = []; + + // store the help request keyed by the user + promises.push(this.redis.hmsetAsync(key, { + user: userId, + channel: channelId, + message: message, + date: moment().unix() + })); + + // remove any existing help requests by the user in the help collection. + // prevents any duplicated hashmap keys in the collection + promises.push(this.redis.lremAsync(HELP_LIST_KEY, 0, key).finally(function () { + // add the help request hashmap key + return this.redis.lpush(HELP_LIST_KEY, key); + }.bind(this))); + + // wait for all redis transactions to complete + return Promise.all(promises) + }; + + TaskCoordinatorDataService.prototype.getHelpRequests = function () { + return this.redis.lrangeAsync(HELP_LIST_KEY, HELP_LIST_LIMIT_LOWER, HELP_LIST_LIMIT_UPPER).then(function (keys) { + var lookupPromises = _.map(keys, this.getHelpRequest.bind(this)); + return Promise.all(lookupPromises); + }.bind(this)); + }; + + TaskCoordinatorDataService.prototype.getHelpRequest = function (key) { + return this.redis.hgetallAsync(key); + }; + + return TaskCoordinatorDataService; +}).call(this); From ecd409f7be4d6683ada87b6bee62bec14d2abb5b Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 02:16:24 -0400 Subject: [PATCH 52/59] comments and wiring the task coordinator data service --- src/interpretter.js | 2 +- src/task-coordinator.js | 58 ++++++++++++----------------------------- 2 files changed, 18 insertions(+), 42 deletions(-) diff --git a/src/interpretter.js b/src/interpretter.js index 777f72b..b61b3d3 100644 --- a/src/interpretter.js +++ b/src/interpretter.js @@ -6,7 +6,7 @@ module.exports = (function () { }; service.isAskingForHelp = function (message) { - return message.toLoserCase().indexOf('anyone need help') > -1; + return message.toLowerCase().indexOf('anyone need help') > -1; }; return service; diff --git a/src/task-coordinator.js b/src/task-coordinator.js index 804c5ea..0f9ac4e 100644 --- a/src/task-coordinator.js +++ b/src/task-coordinator.js @@ -1,7 +1,8 @@ var _ = require('lodash'), moment = require('moment'), logger = require('log4js').getLogger('task-coordinator'), - Filter = require('./filters'); + Filter = require('./filters'), + TaskCoordinatorDataService = require('./services/task-coordinator-data'); module.exports = (function () { var HELP_LIST_KEY = 'help', @@ -11,6 +12,7 @@ module.exports = (function () { function TaskCoordinator (delegate) { this.delegate = delegate; + this.service = new TaskCoordinatorDataService(delegate.redisClient); } /** @@ -21,69 +23,43 @@ module.exports = (function () { * @return {Object} Promise */ TaskCoordinator.prototype.requestHelp = function (user, message, channel) { - var key = HELP_USER_HASHMAP_KEY_PREFIX + user.id, - promises = []; - - // store the help request keyed by the user - promises.push(this.delegate.redisClient.hmsetAsync(key, { - user: user.id, - channel: channel.id, - message: message.text, - date: moment().unix() - })); - - // remove any existing help requests by the user in the help collection. - // prevents any duplicated hashmap keys in the collection - promises.push(this.delegate.redisClient.lremAsync(HELP_LIST_KEY, 0, key).finally(function () { - // add the help request hashmap key - return this.delegate.redisClient.lpush(HELP_LIST_KEY, key); - }.bind(this))); - - // wait for all redis transactions to complete - return Promise.all(promises).then(function () { - // transactions completed successfully, acknoweldge + return this.service.createHelpRequest(user.id, channel.id, message.text).then(function () { channel.send('I\'ll let someone know the next time they ask!'); logger.info('help request stored for user', user.id, '(' + user.name + ')') }).catch(function (error) { - // transactions failed, acknoweldge channel.send('Sorry! having an issue right now processing help requests'); logger.error('problem storing help request for user', user.id, '('+ user.name + ')', error); }); }; /** - * [provideHelp description] - * @param {[type]} user [description] - * @param {[type]} message [description] - * @param {[type]} channel [description] - * @return {[type]} [description] + * Get open help requests + * @param {Object} user The user requesting open help requests + * @param {Object} message The message the user posted + * @param {Object} channel The channel the help request was requested in + * @return {Object} Promise */ TaskCoordinator.prototype.provideHelp = function (user, message, channel) { - this.delegate.redisClient.lrangeAsync(HELP_LIST_KEY, HELP_LIST_LIMIT_LOWER, HELP_LIST_LIMIT_UPPER) - .then(function (keys) { - var lookups = _.map(keys, function (key) { - return this.delegate.redisClient.hgetallAsync(key); - }.bind(this)); - return Promise.all(lookups); - }.bind(this)).then(function (helpRequests) { - var messages = _.map(helpRequests, function (request) { + return this.service.getHelpRequests().then(function (helpRequests) { + // generate all the responses + return _.map(helpRequests, function (request) { + // format the response sentence return [ Filter.escapeUserById(request.user, this.delegate.service), 'needed help in', Filter.escapeChannelById(request.channel, this.delegate.service), moment.unix(request.date).from(moment()) + '.', - '\n', - '>', + '\n>', request.message.replace('<@' + this.delegate.service.self.id + '>', '') ].join(' '); }.bind(this)); + }.bind(this)).then(function (messages) { channel.send(messages.join('\n')); logger.info('help requests reported for user', user.id, '(' + user.name + ')'); - }.bind(this)).catch(function (error) { - console.log('error', error); + }).catch(function (error) { logger.error('problem fetching help requests for user', user.id, '(' + user.name + ')', error); channel.send('I had some trouble looking up help requests'); - }) + }); }; return TaskCoordinator; From 369a9d5da5c82244c4083b197f315c5cba702b35 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 02:19:14 -0400 Subject: [PATCH 53/59] comments --- src/services/task-coordinator-data.js | 18 ++++++++++++++++++ src/task-coordinator.js | 5 ----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/services/task-coordinator-data.js b/src/services/task-coordinator-data.js index ab59fdc..c2ee235 100644 --- a/src/services/task-coordinator-data.js +++ b/src/services/task-coordinator-data.js @@ -12,6 +12,13 @@ module.exports = (function () { this.redis = redis; } + /** + * Create a help request + * @param {Integer} userId ID of the user creating the request + * @param {Integer} channelId ID of the channel the request was posted in + * @param {String} message The message that was posted + * @return {Object} Promise + */ TaskCoordinatorDataService.prototype.createHelpRequest = function (userId, channelId, message) { var key = HELP_USER_HASHMAP_KEY_PREFIX + userId, promises = []; @@ -35,13 +42,24 @@ module.exports = (function () { return Promise.all(promises) }; + /** + * Get open help requests + * @return {Object} Promise + */ TaskCoordinatorDataService.prototype.getHelpRequests = function () { + // fetch the collection of help request keys return this.redis.lrangeAsync(HELP_LIST_KEY, HELP_LIST_LIMIT_LOWER, HELP_LIST_LIMIT_UPPER).then(function (keys) { + // fetch each help request by the key var lookupPromises = _.map(keys, this.getHelpRequest.bind(this)); return Promise.all(lookupPromises); }.bind(this)); }; + /** + * Get an open help request + * @param {String} key The key of the help request + * @return {Object} Promise + */ TaskCoordinatorDataService.prototype.getHelpRequest = function (key) { return this.redis.hgetallAsync(key); }; diff --git a/src/task-coordinator.js b/src/task-coordinator.js index 0f9ac4e..3e0cd90 100644 --- a/src/task-coordinator.js +++ b/src/task-coordinator.js @@ -5,11 +5,6 @@ var _ = require('lodash'), TaskCoordinatorDataService = require('./services/task-coordinator-data'); module.exports = (function () { - var HELP_LIST_KEY = 'help', - HELP_USER_HASHMAP_KEY_PREFIX = HELP_LIST_KEY + ':' + 'user:', - HELP_LIST_LIMIT_LOWER = 0, - HELP_LIST_LIMIT_UPPER = 10; - function TaskCoordinator (delegate) { this.delegate = delegate; this.service = new TaskCoordinatorDataService(delegate.redisClient); From 2ca7ab75016069434f86cfd0a59801fc76528684 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 02:47:04 -0400 Subject: [PATCH 54/59] remove help requests --- index.js | 1 + src/bot.js | 2 ++ src/interpretter.js | 4 ++++ src/services/task-coordinator-data.js | 15 +++++++++++++++ src/task-coordinator.js | 22 +++++++++++++++++++++- 5 files changed, 43 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 4a3c183..f825b80 100644 --- a/index.js +++ b/index.js @@ -29,6 +29,7 @@ var Slack = require('slack-client'), // promisify all redis.getAsync = Promise.promisify(redis.get); redis.setAsync = Promise.promisify(redis.set); + redis.delAsync = Promise.promisify(redis.del); slack = new Slack(token, true); bot = new Bot(slack, redis); diff --git a/src/bot.js b/src/bot.js index 600c707..05c9a86 100644 --- a/src/bot.js +++ b/src/bot.js @@ -54,6 +54,8 @@ module.exports = (function () { this.taskCoordinator.requestHelp(user, message, channel); } else if (Interpretter.isAskingForHelp(message.text)) { this.taskCoordinator.provideHelp(user, message, channel); + } else if (Interpretter.isNoLongerLookingForHelp(message.text)) { + this.taskCoordinator.removeHelp(user, channel); } logger.info(message.user, '(', user.name, ') pinged from', channel.getType(), 'with message', message.text); }; diff --git a/src/interpretter.js b/src/interpretter.js index b61b3d3..fabd514 100644 --- a/src/interpretter.js +++ b/src/interpretter.js @@ -9,5 +9,9 @@ module.exports = (function () { return message.toLowerCase().indexOf('anyone need help') > -1; }; + service.isNoLongerLookingForHelp = function (message) { + return message.toLowerCase().indexOf('dont need help') > -1; + }; + return service; }).call(this); diff --git a/src/services/task-coordinator-data.js b/src/services/task-coordinator-data.js index c2ee235..235e01d 100644 --- a/src/services/task-coordinator-data.js +++ b/src/services/task-coordinator-data.js @@ -64,5 +64,20 @@ module.exports = (function () { return this.redis.hgetallAsync(key); }; + /** + * Remove a help request + * @param {Integer} userId ID of the user to remove the help request of + * @return {Object} Promise + */ + TaskCoordinatorDataService.prototype.removeHelpRequest = function (userId) { + var key = HELP_USER_HASHMAP_KEY_PREFIX + userId; + + // remove the help request hashmap + return this.redis.delAsync(key).then(function () { + // remove the hash map key from the list + return this.redis.lremAsync(HELP_LIST_KEY, 0, key); + }.bind(this)); + }; + return TaskCoordinatorDataService; }).call(this); diff --git a/src/task-coordinator.js b/src/task-coordinator.js index 3e0cd90..e3dca93 100644 --- a/src/task-coordinator.js +++ b/src/task-coordinator.js @@ -49,7 +49,11 @@ module.exports = (function () { ].join(' '); }.bind(this)); }.bind(this)).then(function (messages) { - channel.send(messages.join('\n')); + if (messages.length === 0) { + channel.send('Nobody has recently asked for help. I\'m sure somone could use a hand somewhere!'); + } else { + channel.send(messages.join('\n') + 'no'); + } logger.info('help requests reported for user', user.id, '(' + user.name + ')'); }).catch(function (error) { logger.error('problem fetching help requests for user', user.id, '(' + user.name + ')', error); @@ -57,5 +61,21 @@ module.exports = (function () { }); }; + /** + * Remove help request + * @param {Object} user The user requesting to remove their help request + * @param {Object} channel The channel the user had posted in + * @return {Object} Promise + */ + TaskCoordinator.prototype.removeHelp = function (user, channel) { + return this.service.removeHelpRequest(user.id).then(function () { + channel.send('Thanks for letting me know, I have removed your request.'); + logger.info('help request removed for user', user.id, '(' + user.name + ')'); + }).catch(function (error) { + channel.send('Hrmm.. Something went wrong trying to remove your request.'); + logger.error('problem removing request for user', user.id, '(' + user.name + ')', error); + }); + }; + return TaskCoordinator; }).call(this); From dabb8a65da1ffceb64f1d9a7ccfb39ff8d465b43 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 03:10:32 -0400 Subject: [PATCH 55/59] ask for a users own help requests --- src/bot.js | 2 ++ src/interpretter.js | 4 ++++ src/services/task-coordinator-data.js | 10 ++++++++++ src/task-coordinator.js | 26 ++++++++++++++++++++++++++ 4 files changed, 42 insertions(+) diff --git a/src/bot.js b/src/bot.js index 05c9a86..9f5801b 100644 --- a/src/bot.js +++ b/src/bot.js @@ -56,6 +56,8 @@ module.exports = (function () { this.taskCoordinator.provideHelp(user, message, channel); } else if (Interpretter.isNoLongerLookingForHelp(message.text)) { this.taskCoordinator.removeHelp(user, channel); + } else if (Interpretter.isCheckingForOpenHelpRequest(message.text)) { + this.taskCoordinator.hasHelpOpen(user, channel); } logger.info(message.user, '(', user.name, ') pinged from', channel.getType(), 'with message', message.text); }; diff --git a/src/interpretter.js b/src/interpretter.js index fabd514..32a7921 100644 --- a/src/interpretter.js +++ b/src/interpretter.js @@ -13,5 +13,9 @@ module.exports = (function () { return message.toLowerCase().indexOf('dont need help') > -1; }; + service.isCheckingForOpenHelpRequest = function (message) { + return message.toLowerCase().indexOf('have help') > -1; + }; + return service; }).call(this); diff --git a/src/services/task-coordinator-data.js b/src/services/task-coordinator-data.js index 235e01d..56ef4c4 100644 --- a/src/services/task-coordinator-data.js +++ b/src/services/task-coordinator-data.js @@ -64,6 +64,16 @@ module.exports = (function () { return this.redis.hgetallAsync(key); }; + /** + * Get an open help request by a user ID + * @param {Integer} userId ID of the user to check + * @return {Object} Promise + */ + TaskCoordinatorDataService.prototype.getHelpRequestByUserId = function (userId) { + var key = HELP_USER_HASHMAP_KEY_PREFIX + userId; + return this.getHelpRequest(key); + }; + /** * Remove a help request * @param {Integer} userId ID of the user to remove the help request of diff --git a/src/task-coordinator.js b/src/task-coordinator.js index e3dca93..be07b7b 100644 --- a/src/task-coordinator.js +++ b/src/task-coordinator.js @@ -77,5 +77,31 @@ module.exports = (function () { }); }; + /** + * Check if there is an open help request by a user + * @param {Object} user The user checking if they have a request + * @param {Object} channel The channel the user posted in + * @return {Object} Promise + */ + TaskCoordinator.prototype.hasHelpOpen = function (user, channel) { + return this.service.getHelpRequestByUserId(user.id).then(function (helpRequest) { + var message; + if (helpRequest) { + message = [ + 'You asked me for help', + moment.unix(helpRequest.date).from(moment()) + ':', + '\n>' + helpRequest.message.replace('<@' + this.delegate.service.self.id + '>', '') + ]; + channel.send(message.join(' ')); + } else { + channel.send('You do not have any open help requests.'); + } + logger.info('checked for open help request by user', user.id, '(' + user.name + ')'); + }.bind(this)).catch(function (error) { + logger.error('problem checking if user has open request', user.id, '(' + user.name + ')'); + channel.send('Hrrm.. Something when wrong trying to look up your request'); + }); + }; + return TaskCoordinator; }).call(this); From 96396447e6661d17ef5bd5f3d77b30415ad9048e Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 03:27:58 -0400 Subject: [PATCH 56/59] random message service --- src/bot-responses.json | 43 ++++++++++++++++++++++++++++++++++++++++++ src/messages.js | 29 ++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/bot-responses.json create mode 100644 src/messages.js diff --git a/src/bot-responses.json b/src/bot-responses.json new file mode 100644 index 0000000..f6d1b0f --- /dev/null +++ b/src/bot-responses.json @@ -0,0 +1,43 @@ +{ + "task_coordinator": { + "request_help": { + "success": [ + "I'll let someone know the next time they ask!", + "Great, I'll find someone for you.", + "Stay tuned, I'll point someone your way." + ], + "failed": [ + "Sorry! I'm having an issue right now processing help requests" + ] + }, + "provide_help": { + "success": [ + "You're in luck:\n" + ], + "empty": [ + "Nobody has recently asked for help. I'm sure someone could use a hand somewhere!", + "I've got nothing for you right now, but someone somewhere could use a hand!" + ], + "error": [ + "Hrmm.. I'm having some trouble looking up help requests. Sorry!" + ] + }, + "remove_help": { + "success": [ + "Thanks for letting me know. I have removed your request.", + "I hope it worked out! Your request has been removed" + ], + "error": [ + "Hrmm.. I'm having some trouble removing your help request." + ] + }, + "has_help": { + "success": [ + "Yes, sir. You have recently asked me for help:\n" + ], + "error": [ + "Hrmm.. I'm having some trouble finding that right now." + ] + } + } +} diff --git a/src/messages.js b/src/messages.js new file mode 100644 index 0000000..09ec47b --- /dev/null +++ b/src/messages.js @@ -0,0 +1,29 @@ +var _ = require('lodash'), + Responses = require('./bot-responses'); + +module.exports = (function () { + var service; + + // take the messages JSON file and construct an object, modeled off the JSON structure + // and provide function endpoints for each leaf to sample the available messages + function recusivelyBuildServiceResponses (value) { + if (_.isObject(value) && !_.isArray(value)) { + return _.reduce(value, function (childObject, value, property) { + childObject[_.camelCase(property)] = recusivelyBuildServiceResponses(value); + return childObject; + }, {}); + } else { + return function () { + if (_.isArray(value)) { + return _.sample(value); + } else { + return value; + } + }; + } + } + + service = recusivelyBuildServiceResponses(Responses); + + return service; +}).call(this); From db06ae7ad40f9e79106bd56c042035f3594589aa Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 03:29:06 -0400 Subject: [PATCH 57/59] removed bad string --- src/task-coordinator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/task-coordinator.js b/src/task-coordinator.js index be07b7b..77b3d7c 100644 --- a/src/task-coordinator.js +++ b/src/task-coordinator.js @@ -52,7 +52,7 @@ module.exports = (function () { if (messages.length === 0) { channel.send('Nobody has recently asked for help. I\'m sure somone could use a hand somewhere!'); } else { - channel.send(messages.join('\n') + 'no'); + channel.send(messages.join('\n')); } logger.info('help requests reported for user', user.id, '(' + user.name + ')'); }).catch(function (error) { From 99dd4b4e75fb6d9b962a4edfefe800bbdb7f728a Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 03:32:01 -0400 Subject: [PATCH 58/59] random message integration --- src/bot-responses.json | 6 +++++- src/task-coordinator.js | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/bot-responses.json b/src/bot-responses.json index f6d1b0f..72e1b0f 100644 --- a/src/bot-responses.json +++ b/src/bot-responses.json @@ -6,7 +6,7 @@ "Great, I'll find someone for you.", "Stay tuned, I'll point someone your way." ], - "failed": [ + "error": [ "Sorry! I'm having an issue right now processing help requests" ] }, @@ -37,6 +37,10 @@ ], "error": [ "Hrmm.. I'm having some trouble finding that right now." + ], + "empty": [ + "You do not have any open help requests.", + "You don't have any open help requests, do you need help?" ] } } diff --git a/src/task-coordinator.js b/src/task-coordinator.js index 77b3d7c..a63f694 100644 --- a/src/task-coordinator.js +++ b/src/task-coordinator.js @@ -2,6 +2,7 @@ var _ = require('lodash'), moment = require('moment'), logger = require('log4js').getLogger('task-coordinator'), Filter = require('./filters'), + Messages = require('./messages'), TaskCoordinatorDataService = require('./services/task-coordinator-data'); module.exports = (function () { @@ -19,10 +20,10 @@ module.exports = (function () { */ TaskCoordinator.prototype.requestHelp = function (user, message, channel) { return this.service.createHelpRequest(user.id, channel.id, message.text).then(function () { - channel.send('I\'ll let someone know the next time they ask!'); + channel.send(Messages.taskCoordinator.requestHelp.success()); logger.info('help request stored for user', user.id, '(' + user.name + ')') }).catch(function (error) { - channel.send('Sorry! having an issue right now processing help requests'); + channel.send(Messages.taskCoordinator.requestHelp.error()); logger.error('problem storing help request for user', user.id, '('+ user.name + ')', error); }); }; @@ -50,12 +51,13 @@ module.exports = (function () { }.bind(this)); }.bind(this)).then(function (messages) { if (messages.length === 0) { - channel.send('Nobody has recently asked for help. I\'m sure somone could use a hand somewhere!'); + channel.send(Messages.taskCoordinator.provideHelp.empty()); } else { - channel.send(messages.join('\n')); + channel.send(Messages.taskCoordinator.provideHelp.success() + messages.join('\n')); } logger.info('help requests reported for user', user.id, '(' + user.name + ')'); }).catch(function (error) { + channel.send(Messages.taskCoordinator.provideHelp.error()); logger.error('problem fetching help requests for user', user.id, '(' + user.name + ')', error); channel.send('I had some trouble looking up help requests'); }); @@ -69,10 +71,10 @@ module.exports = (function () { */ TaskCoordinator.prototype.removeHelp = function (user, channel) { return this.service.removeHelpRequest(user.id).then(function () { - channel.send('Thanks for letting me know, I have removed your request.'); + channel.send(Messages.taskCoordinator.removeHelp.success()); logger.info('help request removed for user', user.id, '(' + user.name + ')'); }).catch(function (error) { - channel.send('Hrmm.. Something went wrong trying to remove your request.'); + channel.send(Messages.taskCoordinator.removeHelp.error()); logger.error('problem removing request for user', user.id, '(' + user.name + ')', error); }); }; @@ -94,12 +96,12 @@ module.exports = (function () { ]; channel.send(message.join(' ')); } else { - channel.send('You do not have any open help requests.'); + channel.send(Messages.taskCoordinator.hasHelp.empty()); } logger.info('checked for open help request by user', user.id, '(' + user.name + ')'); }.bind(this)).catch(function (error) { + channel.send(Messages.taskCoordinator.hasHelp.error()); logger.error('problem checking if user has open request', user.id, '(' + user.name + ')'); - channel.send('Hrrm.. Something when wrong trying to look up your request'); }); }; From e51d020d0b1baa49be7144c3c92f19d643935774 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Fri, 4 Sep 2015 03:34:10 -0400 Subject: [PATCH 59/59] generic messages --- src/bot-responses.json | 7 ++++++- src/bot.js | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/bot-responses.json b/src/bot-responses.json index 72e1b0f..6526d56 100644 --- a/src/bot-responses.json +++ b/src/bot-responses.json @@ -43,5 +43,10 @@ "You don't have any open help requests, do you need help?" ] } - } + }, + "generic": [ + "Hey, I don't have much to say right now..", + "If only I could understand more things..", + "I can't do much right now, but I can coordinate people that need help! Ask me if anyone needs help, or if you need help!" + ] } diff --git a/src/bot.js b/src/bot.js index 9f5801b..e4c15bd 100644 --- a/src/bot.js +++ b/src/bot.js @@ -2,6 +2,7 @@ var Slack = require('slack-client'), Conversation = require('./conversation'), Interpretter = require('./interpretter'), TaskCoordinator = require('./task-coordinator'), + Messages = require('./messages'), Promise = require('bluebird'), Redis = require('redis'), logger = require('log4js').getLogger('bot'), @@ -58,6 +59,8 @@ module.exports = (function () { this.taskCoordinator.removeHelp(user, channel); } else if (Interpretter.isCheckingForOpenHelpRequest(message.text)) { this.taskCoordinator.hasHelpOpen(user, channel); + } else { + channel.send(Messages.generic()); } logger.info(message.user, '(', user.name, ') pinged from', channel.getType(), 'with message', message.text); };