diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aeeee5c42..32466c728 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,7 @@ possible with your report. If you can, please include: * Include screenshots and animated GIFs in your pull request whenever possible. * Follow the JavaScript coding style with details from `.jscsrc` and `.editorconfig` files and use necessary plugins for your text editor. * Write documentation in [Markdown](https://daringfireball.net/projects/markdown). +* Please follow, [JSDoc](http://usejsdoc.org/) for proper documentation. * Use short, present tense commit messages. See [Commit Message Styleguide](#git-commit-messages). ## Styleguides diff --git a/changelog.md b/changelog.md new file mode 100644 index 000000000..06d00ff67 --- /dev/null +++ b/changelog.md @@ -0,0 +1,62 @@ +# Change Log + +## 0.2 + +Adds support for Twilio IP Messenging bots + +Add example bot: twilio_ipm_bot.js + +## 0.1.2 + +*Slack changes:* + +Adds authentication of incoming Slack webhooks if token specified. [More info](readme_slack.md#securing-outgoing-webhooks-and-slash-commands) [Thanks to [@sgud](https://github.com/howdyai/botkit/pull/167)] + +Improves support for direct_mentions of bots in Slack (Merged [PR #189](https://github.com/howdyai/botkit/pull/189)) + +Make the oauth identity available to the user of the OAuth endpoint via `req.identity` (Merged [PR #174](https://github.com/howdyai/botkit/pull/174)) + +Fix issue where single team apps had a hard time receiving slash command events without funky workaround. (closes [Issue #108](https://github.com/howdyai/botkit/issues/108)) + +Add [team_slashcommand.js](/examples/team_slashcommand.js) and [team_outgoingwebhook.js](/examples/team_outgoingwebhook.js) to the examples folder. + + + +*Facebook changes:* + +The `attachment` field may now be used by Facebook bots within a conversation for both convo.say and convo.ask. In addition, postback messages can now be received as the answer to a convo.ask in addition to triggering their own facebook_postback event. [Thanks to [@crummy](https://github.com/howdyai/botkit/pull/220) and [@tkornblit](https://github.com/howdyai/botkit/pull/208)] + +Include attachments field in incoming Facebook messages (Merged [PR #231](https://github.com/howdyai/botkit/pull/231)) + +Adds built-in support for opening a localtunnel.me tunnel to expose Facebook webhook endpoint while developing locally. (Merged [PR #234](https://github.com/howdyai/botkit/pull/234)) + +## 0.1.1 + +Fix issue with over-zealous try/catch in Slack_web_api.js + +## 0.1.0 + +Adds support for Facebook Messenger bots. + +Rename example bot: bot.js became slack_bot.js + +Add example bot: facebook_bot.js + +## 0.0.15 + +Changes conversation.ask to use the same pattern matching function as +is used in `hears()` + +Adds `controller.changeEars()` Developers can now globally change the +way Botkit matches patterns. + + +## 0.0.14 + +Add new middleware hooks. Developers can now change affect a message +as it is received or sent, and can also change the way Botkit matches +patterns in the `hears()` handler. + +## 0.0.~ + +Next time I promise to start a change log at v0.0.0 diff --git a/examples/convo_bot.js b/examples/convo_bot.js index 7692c014f..973757ddf 100644 --- a/examples/convo_bot.js +++ b/examples/convo_bot.js @@ -1,10 +1,10 @@ /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ______ ______ ______ __ __ __ ______ + ______ ______ ______ __ __ __ ______ /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ - \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ - \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ - + \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ + \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ + This is a sample Slack bot built with Botkit. @@ -38,13 +38,13 @@ This bot demonstrates a multi-stage conversation Say where you want it delivered. - The bot will reply "Ok! Good by." + The bot will reply "Ok! Goodbye." ...and will refrain from billing your card because this is just a demo :P # EXTEND THE BOT: - Botkit is has many features for building cool and useful bots! + Botkit has many features for building cool and useful bots! Read all about it here: @@ -91,7 +91,7 @@ askSize = function(response, convo) { } askWhereDeliver = function(response, convo) { convo.ask("So where do you want it delivered?", function(response, convo) { - convo.say("Ok! Good by."); + convo.say("Ok! Goodbye."); convo.next(); }); -} \ No newline at end of file +} diff --git a/examples/demo_bot.js b/examples/demo_bot.js index 1b698c350..d5d18ba0a 100755 --- a/examples/demo_bot.js +++ b/examples/demo_bot.js @@ -1,9 +1,9 @@ /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ______ ______ ______ __ __ __ ______ + ______ ______ ______ __ __ __ ______ /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ - \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ - \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ + \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ + \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ This is a sample Slack bot built with Botkit. @@ -37,7 +37,7 @@ This bot demonstrates many of the core features of Botkit: The bot will send a message with a multi-field attachment. - Send: "dm" + Send: "dm me" The bot will reply with a direct message. @@ -45,7 +45,7 @@ This bot demonstrates many of the core features of Botkit: # EXTEND THE BOT: - Botkit is has many features for building cool and useful bots! + Botkit has many features for building cool and useful bots! Read all about it here: diff --git a/bot.js b/examples/middleware_example.js old mode 100755 new mode 100644 similarity index 69% rename from bot.js rename to examples/middleware_example.js index 448e9d28c..de4f8416e --- a/bot.js +++ b/examples/middleware_example.js @@ -1,9 +1,9 @@ /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ______ ______ ______ __ __ __ ______ + ______ ______ ______ __ __ __ ______ /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ - \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ - \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ + \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ + \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ This is a sample Slack bot built with Botkit. @@ -55,7 +55,7 @@ This bot demonstrates many of the core features of Botkit: # EXTEND THE BOT: - Botkit is has many features for building cool and useful bots! + Botkit has many features for building cool and useful bots! Read all about it here: @@ -69,7 +69,7 @@ if (!process.env.token) { process.exit(1); } -var Botkit = require('./lib/Botkit.js'); +var Botkit = require('../lib/Botkit.js'); var os = require('os'); var controller = Botkit.slackbot({ @@ -81,7 +81,49 @@ var bot = controller.spawn({ }).startRTM(); -controller.hears(['hello','hi'],'direct_message,direct_mention,mention',function(bot, message) { +// Example receive middleware. +// for example, recognize several common variations on "hello" and add an intent field to the message +// see below for example hear_intent function +controller.middleware.receive.use(function(bot, message, next) { + + console.log('Receive middleware!'); + // make changes to bot or message here before calling next + if (message.text == 'hello' || message.text == 'hi' || message.text == 'howdy' || message.text == 'hey') { + message.intent = 'hello'; + } + + next(); + +}); + +// Example send middleware +// make changes to bot or message here before calling next +// for example, do formatting or add additional information to the message +controller.middleware.send.use(function(bot, message, next) { + + console.log('Send middleware!'); + next(); + +}); + + +// Example hear middleware +// Return true if one of [patterns] matches message +// In this example, listen for an intent field, and match using that instead of the text field +function hear_intent(patterns, message) { + + for (var p = 0; p < patterns.length; p++) { + if (message.intent == patterns[p]) { + return true; + } + } + + return false; +} + + +/* note this uses example middlewares defined above */ +controller.hears(['hello'],'direct_message,direct_mention,mention',hear_intent, function(bot, message) { bot.api.reactions.add({ timestamp: message.ts, @@ -96,16 +138,17 @@ controller.hears(['hello','hi'],'direct_message,direct_mention,mention',function controller.storage.users.get(message.user,function(err, user) { if (user && user.name) { - bot.reply(message,'Hello ' + user.name + '!!'); + bot.reply(message, 'Hello ' + user.name + '!!'); } else { - bot.reply(message,'Hello.'); + bot.reply(message, 'Hello.'); } }); }); -controller.hears(['call me (.*)'],'direct_message,direct_mention,mention',function(bot, message) { - var matches = message.text.match(/call me (.*)/i); - var name = matches[1]; +controller.hears(['call me (.*)','my name is (.*)'],'direct_message,direct_mention,mention',function(bot, message) { + + // the name will be stored in the message.match field + var name = message.match[1]; controller.storage.users.get(message.user,function(err, user) { if (!user) { user = { diff --git a/examples/sentiment_analysis.js b/examples/sentiment_analysis.js new file mode 100644 index 000000000..07fdf84bc --- /dev/null +++ b/examples/sentiment_analysis.js @@ -0,0 +1,153 @@ +'use strict'; +/* "dependencies": { + "botkit": "0.0.7", + "escape-string-regexp": "^1.0.5", + "lodash": "^4.5.1", + "mongodb": "^2.1.7", + "sentiment": "^1.0.6", + } + */ +var _ = require('lodash'); +var escapeStringRegexp = require('escape-string-regexp'); +var botkit = require('botkit'); +var mongodb = require('mongodb'); +var sentiment = require('sentiment'); + +function connectToDb() { + mongodb.MongoClient.connect('mongodb://localhost:27017/sentiment', function (err, db) { + if (err) { + throw err; + } + console.log('Connection established to mongodb'); + startBot(db); + }); +} + +function startBot(db) { + var collection = db.collection('sentiment'); + + var INSTANCE_PUBLIC_SHAMING = true; + var INSTANCE_PUBLIC_SHAMING_THRESHOLD = -4; + + var INSTANCE_PRIVATE_SHAMING = true; + var INSTANCE_PRIVATE_SHAMING_THRESHOLD = -4; + + var COUNT_POSITIVE_SCORES = true; + var COUNT_NEGATIVE_SCORES = true; + + var INSTANCE_PUBLIC_SHAMING_MESSAGES = [ + 'Remember to keep up the DoublePlusGood GoodThink vibes for our SafeSpace.', + 'Remember, we\'re all in this together for the benefit of our Company.', + 'Let\'s stay positive! Remember: There\'s no I in team but there\'s an "eye" in ' + + 'this team. ;)', + 'We wouldn\'t want this to stay on our permanent record. Let\'s speak more positively' ]; + var INSTANCE_PRIVATE_SHAMING_MESSAGES = [ + 'Please remember to be civil. This will be on your HR file.', + 'Only Happy fun times are allowed here. Remember GoodThink and PositiveVibes.', + 'Let\'s stay positive! Remember: There\'s no I in team but there\'s an "eye" in this ' + + 'team. ;). Watching you.', + 'Upbeat messages only. This has been logged to keep everyone safe.' ]; + + var afinn = require('sentiment/build/AFINN.json'); + + var botkitController = botkit.slackbot({ + debug: false + }); + + botkitController.spawn({ + token: process.env.token + }).startRTM(function (err) { + if (err) { + throw err; + } + }); + + botkitController.hears([ 'hello', 'hi' ], [ 'direct_mention' ], function (bot, message) { + bot.reply(message, 'Hello. I\'m watching you.'); + }); + + var formatReportList = function formatReportList(result) { + return result.map(function (i) { + return '<@' + i._id + '>: ' + i.score; + }); + }; + + botkitController.hears([ 'report' ], [ 'direct_message', 'direct_mention' ], function (bot, message) { + collection.aggregate([ { $sort: { score: 1 } }, { $limit: 10 } ]).toArray( + function (err, result) { + if (err) { + throw err; + } + + var topList = formatReportList(result); + bot.reply(message, 'Top 10 Scores:\n' + topList.join('"\n"')); + }); + collection.aggregate([ { $sort: { score: -1 } }, { $limit: 10 } ]).toArray( + function (err, result) { + if (err) { + throw err; + } + + var bottomList = formatReportList(result); + bot.reply(message, 'Bottom 10 Scores:\n' + bottomList.join('\n')); + }); + }); + + var listeningFor = '^' + Object.keys(afinn).map(escapeStringRegexp).join('|') + '$'; + botkitController.hears([ listeningFor ], [ 'ambient' ], function (bot, message) { + var sentimentAnalysis = sentiment(message.text); + console.log({ sentimentAnalysis: sentimentAnalysis }); + if (COUNT_POSITIVE_SCORES == false && sentimentAnalysis.score > 0) { + return; + } + + if (COUNT_NEGATIVE_SCORES == false && sentimentAnalysis.score < 0) { + return; + } + + collection.findAndModify({ _id: message.user }, [ [ '_id', 1 ] ], { + $inc: { score: sentimentAnalysis.score } + }, { 'new': true, upsert: true }, function (err, result) { + if (err) { + throw err; + } + + // full doc is available in result object: + // console.log(result) + var shamed = false; + if (INSTANCE_PUBLIC_SHAMING && + sentimentAnalysis.score <= INSTANCE_PUBLIC_SHAMING_THRESHOLD) { + shamed = true; + bot.startConversation(message, function (err, convo) { + if (err) { + throw err; + } + + var publicShamingMessage = _.sample(INSTANCE_PUBLIC_SHAMING_MESSAGES); + console.log({ publicShamingMessage: publicShamingMessage }); + convo.say(publicShamingMessage); + }); + } + + if (!shamed && INSTANCE_PRIVATE_SHAMING && + sentimentAnalysis.score <= INSTANCE_PRIVATE_SHAMING_THRESHOLD) { + bot.startPrivateConversation(message, function (err, dm) { + if (err) { + throw err; + } + + var privateShamingMessage = _.sample(INSTANCE_PRIVATE_SHAMING_MESSAGES); + console.log({ privateShamingMessage: privateShamingMessage }); + dm.say(privateShamingMessage); + }); + } + }); + }); +} + +if (!process.env.token) { + console.log('Error: Specify token in environment'); + process.exit(1); +} + +connectToDb(); diff --git a/examples/slackbutton_bot.js b/examples/slackbutton_bot.js index d1dfe0471..cb4b9f956 100755 --- a/examples/slackbutton_bot.js +++ b/examples/slackbutton_bot.js @@ -4,11 +4,14 @@ \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ + + This is a sample Slack Button application that adds a bot to one or many slack teams. # RUN THE APP: Create a Slack app. Make sure to configure the bot user! -> https://api.slack.com/applications/new + -> Add the Redirect URI: http://localhost:3000/oauth Run your bot from the command line: clientId= clientSecret= port=3000 node slackbutton_bot.js # USE THE APP @@ -16,7 +19,7 @@ This is a sample Slack Button application that adds a bot to one or many slack t -> http://localhost:3000/login After you've added the app, try talking to your bot! # EXTEND THE APP: - Botkit is has many features for building cool and useful bots! + Botkit has many features for building cool and useful bots! Read all about it here: -> http://howdy.ai/botkit ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ @@ -125,7 +128,7 @@ controller.storage.teams.all(function(err,teams) { // connect all teams with bots up to slack! for (var t in teams) { if (teams[t].bot) { - var bot = controller.spawn(teams[t]).startRTM(function(err) { + controller.spawn(teams[t]).startRTM(function(err, bot) { if (err) { console.log('Error connecting bot to Slack:',err); } else { diff --git a/examples/slackbutton_incomingwebhooks.js b/examples/slackbutton_incomingwebhooks.js index 7b74480f5..76441caf6 100755 --- a/examples/slackbutton_incomingwebhooks.js +++ b/examples/slackbutton_incomingwebhooks.js @@ -1,10 +1,10 @@ /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ______ ______ ______ __ __ __ ______ + ______ ______ ______ __ __ __ ______ /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ - \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ - \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ - + \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ + \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ + This is a sample Slack Button application that allows the application to post messages into Slack. @@ -42,7 +42,7 @@ This bot demonstrates many of the core features of Botkit: # EXTEND THE APP: - Botkit is has many features for building cool and useful bots! + Botkit has many features for building cool and useful bots! Read all about it here: diff --git a/examples/slackbutton_slashcommand.js b/examples/slackbutton_slashcommand.js index ea23ab02b..546f94868 100755 --- a/examples/slackbutton_slashcommand.js +++ b/examples/slackbutton_slashcommand.js @@ -1,10 +1,10 @@ /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ______ ______ ______ __ __ __ ______ + ______ ______ ______ __ __ __ ______ /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ - \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ - \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ - + \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ + \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ + This is a sample Slack Button application that provides a custom Slash command. @@ -32,7 +32,7 @@ This bot demonstrates many of the core features of Botkit: # EXTEND THE BOT: - Botkit is has many features for building cool and useful bots! + Botkit has many features for building cool and useful bots! Read all about it here: diff --git a/examples/team_outgoingwebhook.js b/examples/team_outgoingwebhook.js index ee920d5a8..8d8490daa 100755 --- a/examples/team_outgoingwebhook.js +++ b/examples/team_outgoingwebhook.js @@ -1 +1,16 @@ -/* TODO a bot that responds to outgoing webhooks for a team */ +var Botkit = require('../lib/Botkit.js'); + +var controller = Botkit.slackbot({ + debug: true +}); + + +controller.setupWebserver(3000, function(err, webserver) { + controller.createWebhookEndpoints(webserver); +}); + +controller.on('outgoing_webhook', function(bot, message) { + + bot.replyPublic(message, 'This is a public reply to the outgoing webhook!'); + +}); diff --git a/examples/team_slashcommand.js b/examples/team_slashcommand.js index 4222eee5e..f4ae87e2f 100755 --- a/examples/team_slashcommand.js +++ b/examples/team_slashcommand.js @@ -1 +1,23 @@ -/* TODO a bot that responds to slash commands for a team */ +var Botkit = require('../lib/Botkit.js'); + +var controller = Botkit.slackbot({ + debug: true +}); + + +controller.setupWebserver(3000, function(err, webserver) { + controller.createWebhookEndpoints(webserver); +}); + +controller.on('slash_command', function(bot, message) { + // check message.command + // and maybe message.text... + // use EITHER replyPrivate or replyPublic... + bot.replyPrivate(message, 'This is a private reply to the ' + message.command + ' slash command!'); + + // and then continue to use replyPublicDelayed or replyPrivateDelayed + bot.replyPublicDelayed(message, 'This is a public reply to the ' + message.command + ' slash command!'); + + bot.replyPrivateDelayed(message, ':dash:'); + +}); diff --git a/facebook_bot.js b/facebook_bot.js new file mode 100755 index 000000000..1336d1db8 --- /dev/null +++ b/facebook_bot.js @@ -0,0 +1,353 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ______ ______ ______ __ __ __ ______ + /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ + \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ + \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ + \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ + + +This is a sample Facebook bot built with Botkit. + +This bot demonstrates many of the core features of Botkit: + +* Connect to Facebook's Messenger APIs +* Receive messages based on "spoken" patterns +* Reply to messages +* Use the conversation system to ask questions +* Use the built in storage system to store and retrieve information + for a user. + +# RUN THE BOT: + + Follow the instructions here to set up your Facebook app and page: + + -> https://developers.facebook.com/docs/messenger-platform/implementation + + Run your bot from the command line: + + page_token= verify_token= node facebook_bot.js [--lt [--ltsubdomain LOCALTUNNEL_SUBDOMAIN]] + + Use the --lt option to make your bot available on the web through localtunnel.me. + +# USE THE BOT: + + Find your bot inside Facebook to send it a direct message. + + Say: "Hello" + + The bot will reply "Hello!" + + Say: "who are you?" + + The bot will tell you its name, where it running, and for how long. + + Say: "Call me " + + Tell the bot your nickname. Now you are friends. + + Say: "who am I?" + + The bot will tell you your nickname, if it knows one for you. + + Say: "shutdown" + + The bot will ask if you are sure, and then shut itself down. + + Make sure to invite your bot into other channels using /invite @! + +# EXTEND THE BOT: + + Botkit has many features for building cool and useful bots! + + Read all about it here: + + -> http://howdy.ai/botkit + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ + + +if (!process.env.page_token) { + console.log('Error: Specify page_token in environment'); + process.exit(1); +} + +if (!process.env.verify_token) { + console.log('Error: Specify verify_token in environment'); + process.exit(1); +} + +var Botkit = require('./lib/Botkit.js'); +var os = require('os'); +var commandLineArgs = require('command-line-args'); +var localtunnel = require('localtunnel'); + +const cli = commandLineArgs([ + {name: 'lt', alias: 'l', args: 1, description: 'Use localtunnel.me to make your bot available on the web.', + type: Boolean, defaultValue: false}, + {name: 'ltsubdomain', alias: 's', args: 1, + description: 'Custom subdomain for the localtunnel.me URL. This option can only be used together with --lt.', + type: String, defaultValue: null}, + ]); + +const ops = cli.parse(); +if(ops.lt === false && ops.ltsubdomain !== null) { + console.log("error: --ltsubdomain can only be used together with --lt."); + process.exit(); +} + +var controller = Botkit.facebookbot({ + debug: true, + access_token: process.env.page_token, + verify_token: process.env.verify_token, +}); + +var bot = controller.spawn({ +}); + +controller.setupWebserver(process.env.port || 3000, function(err, webserver) { + controller.createWebhookEndpoints(webserver, bot, function() { + console.log('ONLINE!'); + if(ops.lt) { + var tunnel = localtunnel(process.env.port || 3000, {subdomain: ops.ltsubdomain}, function(err, tunnel) { + if (err) { + console.log(err); + process.exit(); + } + console.log("Your bot is available on the web at the following URL: " + tunnel.url + '/facebook/receive'); + }); + + tunnel.on('close', function() { + console.log("Your bot is no longer available on the web at the localtunnnel.me URL."); + process.exit(); + }); + } + }); +}); + + +controller.hears(['hello', 'hi'], 'message_received', function(bot, message) { + controller.storage.users.get(message.user, function(err, user) { + if (user && user.name) { + bot.reply(message, 'Hello ' + user.name + '!!'); + } else { + bot.reply(message, 'Hello.'); + } + }); +}); + + +controller.hears(['structured'], 'message_received', function(bot, message) { + + bot.startConversation(message, function(err, convo) { + convo.ask({ + attachment: { + 'type': 'template', + 'payload': { + 'template_type': 'generic', + 'elements': [ + { + 'title': 'Classic White T-Shirt', + 'image_url': 'http://petersapparel.parseapp.com/img/item100-thumb.png', + 'subtitle': 'Soft white cotton t-shirt is back in style', + 'buttons': [ + { + 'type': 'web_url', + 'url': 'https://petersapparel.parseapp.com/view_item?item_id=100', + 'title': 'View Item' + }, + { + 'type': 'web_url', + 'url': 'https://petersapparel.parseapp.com/buy_item?item_id=100', + 'title': 'Buy Item' + }, + { + 'type': 'postback', + 'title': 'Bookmark Item', + 'payload': 'White T-Shirt' + } + ] + }, + { + 'title': 'Classic Grey T-Shirt', + 'image_url': 'http://petersapparel.parseapp.com/img/item101-thumb.png', + 'subtitle': 'Soft gray cotton t-shirt is back in style', + 'buttons': [ + { + 'type': 'web_url', + 'url': 'https://petersapparel.parseapp.com/view_item?item_id=101', + 'title': 'View Item' + }, + { + 'type': 'web_url', + 'url': 'https://petersapparel.parseapp.com/buy_item?item_id=101', + 'title': 'Buy Item' + }, + { + 'type': 'postback', + 'title': 'Bookmark Item', + 'payload': 'Grey T-Shirt' + } + ] + } + ] + } + } + }, function(response, convo) { + // whoa, I got the postback payload as a response to my convo.ask! + convo.next(); + }); + }); +}); + +controller.on('facebook_postback', function(bot, message) { + + bot.reply(message, 'Great Choice!!!! (' + message.payload + ')'); + +}); + + +controller.hears(['call me (.*)', 'my name is (.*)'], 'message_received', function(bot, message) { + var name = message.match[1]; + controller.storage.users.get(message.user, function(err, user) { + if (!user) { + user = { + id: message.user, + }; + } + user.name = name; + controller.storage.users.save(user, function(err, id) { + bot.reply(message, 'Got it. I will call you ' + user.name + ' from now on.'); + }); + }); +}); + +controller.hears(['what is my name', 'who am i'], 'message_received', function(bot, message) { + controller.storage.users.get(message.user, function(err, user) { + if (user && user.name) { + bot.reply(message, 'Your name is ' + user.name); + } else { + bot.startConversation(message, function(err, convo) { + if (!err) { + convo.say('I do not know your name yet!'); + convo.ask('What should I call you?', function(response, convo) { + convo.ask('You want me to call you `' + response.text + '`?', [ + { + pattern: 'yes', + callback: function(response, convo) { + // since no further messages are queued after this, + // the conversation will end naturally with status == 'completed' + convo.next(); + } + }, + { + pattern: 'no', + callback: function(response, convo) { + // stop the conversation. this will cause it to end with status == 'stopped' + convo.stop(); + } + }, + { + default: true, + callback: function(response, convo) { + convo.repeat(); + convo.next(); + } + } + ]); + + convo.next(); + + }, {'key': 'nickname'}); // store the results in a field called nickname + + convo.on('end', function(convo) { + if (convo.status == 'completed') { + bot.reply(message, 'OK! I will update my dossier...'); + + controller.storage.users.get(message.user, function(err, user) { + if (!user) { + user = { + id: message.user, + }; + } + user.name = convo.extractResponse('nickname'); + controller.storage.users.save(user, function(err, id) { + bot.reply(message, 'Got it. I will call you ' + user.name + ' from now on.'); + }); + }); + + + + } else { + // this happens if the conversation ended prematurely for some reason + bot.reply(message, 'OK, nevermind!'); + } + }); + } + }); + } + }); +}); + +controller.hears(['shutdown'], 'message_received', function(bot, message) { + + bot.startConversation(message, function(err, convo) { + + convo.ask('Are you sure you want me to shutdown?', [ + { + pattern: bot.utterances.yes, + callback: function(response, convo) { + convo.say('Bye!'); + convo.next(); + setTimeout(function() { + process.exit(); + }, 3000); + } + }, + { + pattern: bot.utterances.no, + default: true, + callback: function(response, convo) { + convo.say('*Phew!*'); + convo.next(); + } + } + ]); + }); +}); + + +controller.hears(['uptime', 'identify yourself', 'who are you', 'what is your name'], 'message_received', + function(bot, message) { + + var hostname = os.hostname(); + var uptime = formatUptime(process.uptime()); + + bot.reply(message, + ':|] I am a bot. I have been running for ' + uptime + ' on ' + hostname + '.'); + }); + + + +controller.on('message_received', function(bot, message) { + bot.reply(message, 'Try: `what is my name` or `structured` or `call me captain`'); + return false; +}); + + +function formatUptime(uptime) { + var unit = 'second'; + if (uptime > 60) { + uptime = uptime / 60; + unit = 'minute'; + } + if (uptime > 60) { + uptime = uptime / 60; + unit = 'hour'; + } + if (uptime != 1) { + unit = unit + 's'; + } + + uptime = uptime + ' ' + unit; + return uptime; +} diff --git a/lib/Botkit.js b/lib/Botkit.js index ae1e1d19d..86e44d8fe 100755 --- a/lib/Botkit.js +++ b/lib/Botkit.js @@ -1,7 +1,11 @@ var CoreBot = require(__dirname + '/CoreBot.js'); var Slackbot = require(__dirname + '/SlackBot.js'); +var Facebookbot = require(__dirname + '/Facebook.js'); +var TwilioIPMbot = require(__dirname + '/TwilioIPMBot.js'); module.exports = { core: CoreBot, slackbot: Slackbot, + facebookbot: Facebookbot, + twilioipmbot: TwilioIPMbot, }; diff --git a/lib/CoreBot.js b/lib/CoreBot.js index c266b6cb8..ee0d61065 100755 --- a/lib/CoreBot.js +++ b/lib/CoreBot.js @@ -7,6 +7,7 @@ var mustache = require('mustache'); var simple_storage = require(__dirname + '/storage/simple_storage.js'); var ConsoleLogger = require(__dirname + '/console_logger.js'); var LogLevels = ConsoleLogger.LogLevels; +var ware = require('ware'); function Botkit(configuration) { var botkit = { @@ -27,6 +28,14 @@ function Botkit(configuration) { no: new RegExp(/^(no|nah|nope|n)/i), }; + // define some middleware points where custom functions + // can plug into key points of botkits process + botkit.middleware = { + send: ware(), + receive: ware(), + }; + + function Conversation(task, message) { this.messages = []; @@ -52,10 +61,26 @@ function Botkit(configuration) { this.capture = function(response) { var capture_key = this.sent[this.sent.length - 1].text; + if (response.text) { + response.text = response.text.trim(); + } else { + response.text = ''; + } + if (this.capture_options.key) { capture_key = this.capture_options.key; } + // capture the question that was asked + // if text is an array, get 1st + if (typeof(this.sent[this.sent.length - 1].text) == 'string') { + response.question = this.sent[this.sent.length - 1].text; + } else if (Array.isArray(this.sent[this.sent.length - 1])) { + response.question = this.sent[this.sent.length - 1].text[0]; + } else { + response.question = ''; + } + if (this.capture_options.multiple) { if (!this.responses[capture_key]) { this.responses[capture_key] = []; @@ -84,10 +109,9 @@ function Botkit(configuration) { } else { // handle might be a mapping of keyword to callback. // lets see if the message matches any of the keywords - var match, patterns = this.handler; + var patterns = this.handler; for (var p = 0; p < patterns.length; p++) { - if (patterns[p].pattern && (match = message.text.match(patterns[p].pattern))) { - message.match = match; + if (patterns[p].pattern && botkit.hears_test([patterns[p].pattern], message)) { patterns[p].callback(message, this); return; } @@ -162,7 +186,7 @@ function Botkit(configuration) { } } } else { - botkit.debug('No handler for ', event); + botkit.debug('No handler for', event); } }; @@ -242,6 +266,9 @@ function Botkit(configuration) { }; this.combineMessages = function(messages) { + if (!messages) { + return ''; + }; if (messages.length > 1) { var txt = []; var last_user = null; @@ -273,6 +300,37 @@ function Botkit(configuration) { } }; + this.getResponses = function() { + + var res = {}; + for (var key in this.responses) { + + res[key] = { + question: this.responses[key].length ? + this.responses[key][0].question : this.responses[key].question, + key: key, + answer: this.extractResponse(key), + }; + } + return res; + }; + + this.getResponsesAsArray = function() { + + var res = []; + for (var key in this.responses) { + + res.push({ + question: this.responses[key].length ? + this.responses[key][0].question : this.responses[key].question, + key: key, + answer: this.extractResponse(key), + }); + } + return res; + }; + + this.extractResponses = function() { var res = {}; @@ -294,7 +352,17 @@ function Botkit(configuration) { origin: this.task.source_message, vars: this.vars, }; - return mustache.render(text, vars); + + var rendered = ''; + + try { + rendered = mustache.render(text, vars); + } catch (err) { + botkit.log('Error in message template. Mustache failed with error: ', err); + rendered = text; + }; + + return rendered; }; this.stop = function(status) { @@ -318,7 +386,7 @@ function Botkit(configuration) { if (this.task.timeLimit && // has a timelimit (duration > this.task.timeLimit) && // timelimit is up - (lastActive > (60 * 1000)) // nobody has typed for 60 seconds at least + (lastActive > this.task.timeLimit) // nobody has typed for 60 seconds at least ) { if (this.topics.timeout) { @@ -356,24 +424,40 @@ function Botkit(configuration) { this.transcript.push(message); this.lastActive = new Date(); - if (message.text || message.attachments) { - message.text = message.text && this.replaceTokens(message.text) || ""; + // is there any text? + // or an attachment? (facebook) + // or multiple attachments (slack) + if (message.text || message.attachments || message.attachment) { + + // clone this object so as not to modify source + var outbound = JSON.parse(JSON.stringify(message)); + + if (typeof(message.text) == 'string') { + outbound.text = this.replaceTokens(message.text); + } else if (message.text) { + outbound.text = this.replaceTokens( + message.text[Math.floor(Math.random() * message.text.length)] + ); + } + if (this.messages.length && !message.handler) { - message.continue_typing = true; + outbound.continue_typing = true; } if (typeof(message.attachments) == 'function') { - message.attachments = message.attachments(this); + outbound.attachments = message.attachments(this); } - this.task.bot.say(message, function(err) { + this.task.bot.reply(this.source_message, outbound, function(err) { if (err) { botkit.log('An error occurred while sending a message: ', err); } }); } if (message.action) { - if (message.action == 'repeat') { + if (typeof(message.action) == 'function') { + message.action(this); + } else if (message.action == 'repeat') { this.repeat(); } else if (message.action == 'wait') { this.silentRepeat(); @@ -454,6 +538,16 @@ function Botkit(configuration) { }; + this.endImmediately = function(reason) { + + for (var c = 0; c < this.convos.length; c++) { + if (this.convos[c].isActive()) { + this.convos[c].stop(reason || 'stopped'); + } + } + + }; + this.taskEnded = function() { botkit.log('[End] ', this.id, ' Task for ', this.source_message.user, 'in', this.source_message.channel); @@ -484,7 +578,7 @@ function Botkit(configuration) { } } } else { - botkit.debug('No handler for ', event); + botkit.debug('No handler for', event); } }; @@ -590,6 +684,9 @@ function Botkit(configuration) { } }; + + + botkit.debug = function() { if (configuration.debug) { var args = []; @@ -610,7 +707,59 @@ function Botkit(configuration) { } }; - botkit.hears = function(keywords, events, cb) { + + /** + * hears_regexp - default string matcher uses regular expressions + * + * @param {array} tests patterns to match + * @param {object} message message object with various fields + * @return {boolean} whether or not a pattern was matched + */ + botkit.hears_regexp = function(tests, message) { + for (var t = 0; t < tests.length; t++) { + if (message.text) { + + // the pattern might be a string to match (including regular expression syntax) + // or it might be a prebuilt regular expression + var test; + if (typeof(tests[t]) == 'string') { + test = new RegExp(tests[t], 'i'); + } else { + test = tests[t]; + } + + var match = message.text.match(test); + if (message.text.match(test)) { + message.match = match; + return true; + } + } + } + return false; + }; + + + /** + * changeEars - change the default matching function + * + * @param {function} new_test a function that accepts (tests, message) and returns a boolean + */ + botkit.changeEars = function(new_test) { + botkit.hears_test = new_test; + }; + + + botkit.hears = function(keywords, events, middleware_or_cb, cb) { + + // the third parameter is EITHER a callback handler + // or a middleware function that redefines how the hear works + var test_function = botkit.hears_test; + if (cb) { + test_function = middleware_or_cb; + } else { + cb = middleware_or_cb; + } + if (typeof(keywords) == 'string') { keywords = [keywords]; } @@ -618,24 +767,18 @@ function Botkit(configuration) { events = events.split(/\,/g); } - var match; - for (var k = 0; k < keywords.length; k++) { - var keyword = keywords[k]; - for (var e = 0; e < events.length; e++) { - (function(keyword) { - botkit.on(events[e], function(bot, message) { - if (message.text) { - if (match = message.text.match(new RegExp(keyword, 'i'))) { - botkit.debug('I HEARD ', keyword); - message.match = match; - cb.apply(this, [bot, message]); - return false; - } - } - }); - })(keyword); - } + for (var e = 0; e < events.length; e++) { + (function(keywords, test_function) { + botkit.on(events[e], function(bot, message) { + if (test_function(keywords, message)) { + botkit.debug('I HEARD', keywords); + cb.apply(this, [bot, message]); + return false; + } + }); + })(keywords, test_function); } + return this; }; @@ -661,7 +804,7 @@ function Botkit(configuration) { } } } else { - botkit.debug('No handler for ', event); + botkit.debug('No handler for', event); } }; @@ -680,6 +823,14 @@ function Botkit(configuration) { botkit.spawn = function(config, cb) { var worker = new this.worker(this, config); + + // mutate the worker so that we can call middleware + worker.say = function(message, cb) { + botkit.middleware.send.run(worker, message, function(err, worker, message) { + worker.send(message, cb); + }); + }; + if (cb) { cb(worker); } return worker; }; @@ -720,12 +871,18 @@ function Botkit(configuration) { }; botkit.receiveMessage = function(bot, message) { - botkit.debug('RECEIVED MESSAGE'); - bot.findConversation(message, function(convo) { - if (convo) { - convo.handle(message); + botkit.middleware.receive.run(bot, message, function(err, bot, message) { + if (err) { + botkit.log('ERROR IN RECEIVE MIDDLEWARE: ', err); } else { - botkit.trigger('message_received', [bot, message]); + botkit.debug('RECEIVED MESSAGE'); + bot.findConversation(message, function(convo) { + if (convo) { + convo.handle(message); + } else { + botkit.trigger('message_received', [bot, message]); + } + }); } }); }; @@ -755,7 +912,7 @@ function Botkit(configuration) { this.config = config; this.say = function(message, cb) { - botkit.debug('SAY: ', message); + botkit.debug('SAY:', message); }; this.replyWithQuestion = function(message, question, cb) { @@ -767,7 +924,7 @@ function Botkit(configuration) { }; this.reply = function(src, resp) { - botkit.debug('REPLY: ', resp); + botkit.debug('REPLY:', resp); }; @@ -833,6 +990,9 @@ function Botkit(configuration) { botkit.log('** No persistent storage method specified! Data may be lost when process shuts down.'); } + // set the default set of ears to use the regular expression matching + botkit.changeEars(botkit.hears_regexp); + return botkit; } diff --git a/lib/Facebook.js b/lib/Facebook.js new file mode 100644 index 000000000..edf510e86 --- /dev/null +++ b/lib/Facebook.js @@ -0,0 +1,257 @@ +var Botkit = require(__dirname + '/CoreBot.js'); +var request = require('request'); +var express = require('express'); +var bodyParser = require('body-parser'); + +function Facebookbot(configuration) { + + // Create a core botkit bot + var facebook_botkit = Botkit(configuration || {}); + + // customize the bot definition, which will be used when new connections + // spawn! + facebook_botkit.defineBot(function(botkit, config) { + + var bot = { + botkit: botkit, + config: config || {}, + utterances: botkit.utterances, + }; + + bot.startConversation = function(message, cb) { + botkit.startConversation(this, message, cb); + }; + + bot.send = function(message, cb) { + + var facebook_message = { + recipient: {}, + message: {} + }; + + if (typeof(message.channel) == 'string' && message.channel.match(/\+\d+\(\d\d\d\)\d\d\d\-\d\d\d\d/)) { + facebook_message.recipient.phone_number = message.channel; + } else { + facebook_message.recipient.id = message.channel; + } + + if (message.text) { + facebook_message.message.text = message.text; + } + + if (message.attachment) { + facebook_message.message.attachment = message.attachment; + } + + request.post('https://graph.facebook.com/me/messages?access_token=' + configuration.access_token, + function(err, res, body) { + if (err) { + botkit.debug('WEBHOOK ERROR', err); + return cb && cb(err); + } + + try { + + var json = JSON.parse(body); + + } catch (err) { + + botkit.debug('JSON Parse error: ', err); + return cb && cb(err); + + } + + if (json.error) { + botkit.debug('API ERROR', json.error); + return cb && cb(json.error.message); + } + + botkit.debug('WEBHOOK SUCCESS', body); + cb && cb(null, body); + }).form(facebook_message); + }; + + bot.reply = function(src, resp, cb) { + var msg = {}; + + if (typeof(resp) == 'string') { + msg.text = resp; + } else { + msg = resp; + } + + msg.channel = src.channel; + + bot.say(msg, cb); + }; + + bot.findConversation = function(message, cb) { + botkit.debug('CUSTOM FIND CONVO', message.user, message.channel); + for (var t = 0; t < botkit.tasks.length; t++) { + for (var c = 0; c < botkit.tasks[t].convos.length; c++) { + if ( + botkit.tasks[t].convos[c].isActive() && + botkit.tasks[t].convos[c].source_message.user == message.user + ) { + botkit.debug('FOUND EXISTING CONVO!'); + cb(botkit.tasks[t].convos[c]); + return; + } + } + } + + cb(); + }; + + return bot; + + }); + + + // set up a web route for receiving outgoing webhooks and/or slash commands + + facebook_botkit.createWebhookEndpoints = function(webserver, bot, cb) { + + facebook_botkit.log( + '** Serving webhook endpoints for Slash commands and outgoing ' + + 'webhooks at: http://MY_HOST:' + facebook_botkit.config.port + '/facebook/receive'); + webserver.post('/facebook/receive', function(req, res) { + + facebook_botkit.debug('GOT A MESSAGE HOOK'); + var obj = req.body; + if (obj.entry) { + for (var e = 0; e < obj.entry.length; e++) { + for (var m = 0; m < obj.entry[e].messaging.length; m++) { + var facebook_message = obj.entry[e].messaging[m]; + if (facebook_message.message) { + + var message = { + text: facebook_message.message.text, + user: facebook_message.sender.id, + channel: facebook_message.sender.id, + timestamp: facebook_message.timestamp, + seq: facebook_message.message.seq, + mid: facebook_message.message.mid, + attachments: facebook_message.message.attachments, + }; + + facebook_botkit.receiveMessage(bot, message); + } else if (facebook_message.postback) { + + // trigger BOTH a facebook_postback event + // and a normal message received event. + // this allows developers to receive postbacks as part of a conversation. + var message = { + payload: facebook_message.postback.payload, + user: facebook_message.sender.id, + channel: facebook_message.sender.id, + timestamp: facebook_message.timestamp, + }; + + facebook_botkit.trigger('facebook_postback', [bot, message]); + + var message = { + text: facebook_message.postback.payload, + user: facebook_message.sender.id, + channel: facebook_message.sender.id, + timestamp: facebook_message.timestamp, + }; + + facebook_botkit.receiveMessage(bot, message); + + } else if (facebook_message.optin) { + + var message = { + optin: facebook_message.optin, + user: facebook_message.sender.id, + channel: facebook_message.sender.id, + timestamp: facebook_message.timestamp, + }; + + facebook_botkit.trigger('facebook_optin', [bot, message]); + } else if (facebook_message.delivery) { + + var message = { + optin: facebook_message.delivery, + user: facebook_message.sender.id, + channel: facebook_message.sender.id, + timestamp: facebook_message.timestamp, + }; + + facebook_botkit.trigger('message_delivered', [bot, message]); + + } else { + botkit.log('Got an unexpected message from Facebook: ', facebook_message); + } + } + } + } + res.send('ok'); + }); + + webserver.get('/facebook/receive', function(req, res) { + console.log(req.query); + if (req.query['hub.mode'] == 'subscribe') { + if (req.query['hub.verify_token'] == configuration.verify_token) { + res.send(req.query['hub.challenge']); + } else { + res.send('OK'); + } + } + }); + + if (cb) { + cb(); + } + + return facebook_botkit; + }; + + facebook_botkit.setupWebserver = function(port, cb) { + + if (!port) { + throw new Error('Cannot start webserver without a port'); + } + if (isNaN(port)) { + throw new Error('Specified port is not a valid number'); + } + + var static_dir = __dirname + '/public'; + + if (facebook_botkit.config && facebook_botkit.config.webserver && facebook_botkit.config.webserver.static_dir) + static_dir = facebook_botkit.config.webserver.static_dir; + + facebook_botkit.config.port = port; + + facebook_botkit.webserver = express(); + facebook_botkit.webserver.use(bodyParser.json()); + facebook_botkit.webserver.use(bodyParser.urlencoded({ extended: true })); + facebook_botkit.webserver.use(express.static(static_dir)); + + var server = facebook_botkit.webserver.listen( + facebook_botkit.config.port, + function() { + facebook_botkit.log('** Starting webserver on port ' + + facebook_botkit.config.port); + if (cb) { cb(null, facebook_botkit.webserver); } + }); + + + request.post('https://graph.facebook.com/me/subscribed_apps?access_token=' + configuration.access_token, + function(err, res, body) { + if (err) { + facebook_botkit.log('Could not subscribe to page messages'); + } else { + facebook_botkit.debug('Successfully subscribed to Facebook events:', body); + facebook_botkit.startTicking(); + } + }); + + return facebook_botkit; + + }; + + return facebook_botkit; +}; + +module.exports = Facebookbot; diff --git a/lib/SlackBot.js b/lib/SlackBot.js index 33e34044c..b9ff31e40 100755 --- a/lib/SlackBot.js +++ b/lib/SlackBot.js @@ -55,14 +55,33 @@ function Slackbot(configuration) { }; - // set up a web route for receiving outgoing webhooks and/or slash commands - slack_botkit.createWebhookEndpoints = function(webserver) { + + // adds the webhook authentication middleware module to the webserver + function secureWebhookEndpoints() { + var authenticationMiddleware = require(__dirname + '/middleware/slack_authentication.js'); + // convert a variable argument list to an array, drop the webserver argument + var tokens = Array.prototype.slice.call(arguments); + var webserver = tokens.shift(); + slack_botkit.log( + '** Requiring token authentication for webhook endpoints for Slash commands ' + + 'and outgoing webhooks; configured ' + tokens.length + ' token(s)' + ); + + webserver.use(authenticationMiddleware(tokens)); + } + + // set up a web route for receiving outgoing webhooks and/or slash commands + slack_botkit.createWebhookEndpoints = function(webserver, authenticationTokens) { slack_botkit.log( '** Serving webhook endpoints for Slash commands and outgoing ' + 'webhooks at: http://MY_HOST:' + slack_botkit.config.port + '/slack/receive'); webserver.post('/slack/receive', function(req, res) { + if (authenticationTokens !== undefined && arguments.length > 1 && arguments[1].length) { + secureWebhookEndpoints.apply(null, arguments); + } + // this is a slash command if (req.body.command) { var message = {}; @@ -75,31 +94,56 @@ function Slackbot(configuration) { message.user = message.user_id; message.channel = message.channel_id; - slack_botkit.findTeamById(message.team_id, function(err, team) { - // FIX THIS - // this won't work for single team bots because the team info - // might not be in a db - if (err || !team) { - slack_botkit.log.error('Received slash command, but could not load team'); - } else { - message.type = 'slash_command'; - // HEY THERE - // Slash commands can actually just send back a response - // and have it displayed privately. That means - // the callback needs access to the res object - // to send an optional response. + // Is this configured to use Slackbutton? + // If so, validate this team before triggering the event! + // Otherwise, it's ok to just pass a generic bot in + if (slack_botkit.config.clientId && slack_botkit.config.clientSecret) { - res.status(200); + slack_botkit.findTeamById(message.team_id, function(err, team) { + if (err || !team) { + slack_botkit.log.error('Received slash command, but could not load team'); + } else { + message.type = 'slash_command'; + // HEY THERE + // Slash commands can actually just send back a response + // and have it displayed privately. That means + // the callback needs access to the res object + // to send an optional response. - var bot = slack_botkit.spawn(team); + res.status(200); - bot.team_info = team; - bot.res = res; + var bot = slack_botkit.spawn(team); - slack_botkit.receiveMessage(bot, message); + bot.team_info = team; + bot.res = res; - } - }); + slack_botkit.receiveMessage(bot, message); + + } + }); + } else { + + message.type = 'slash_command'; + // HEY THERE + // Slash commands can actually just send back a response + // and have it displayed privately. That means + // the callback needs access to the res object + // to send an optional response. + + var team = { + id: message.team_id, + }; + + res.status(200); + + var bot = slack_botkit.spawn({}); + + bot.team_info = team; + bot.res = res; + + slack_botkit.receiveMessage(bot, message); + + } } else if (req.body.trigger_word) { @@ -109,36 +153,29 @@ function Slackbot(configuration) { message[key] = req.body[key]; } + + var team = { + id: message.team_id, + }; + // let's normalize some of these fields to match the rtm message format message.user = message.user_id; message.channel = message.channel_id; - slack_botkit.findTeamById(message.team_id, function(err, team) { - - // FIX THIS - // this won't work for single team bots because the team info - // might not be in a db - if (err || !team) { - slack_botkit.log.error('Received outgoing webhook but could not load team'); - } else { - message.type = 'outgoing_webhook'; - - - res.status(200); + message.type = 'outgoing_webhook'; - var bot = slack_botkit.spawn(team); - bot.res = res; - bot.team_info = team; + res.status(200); + var bot = slack_botkit.spawn(team); + bot.res = res; + bot.team_info = team; - slack_botkit.receiveMessage(bot, message); - // outgoing webhooks are also different. They can simply return - // a response instead of using the API to reply. Maybe this is - // a different type of event!! + slack_botkit.receiveMessage(bot, message); - } - }); + // outgoing webhooks are also different. They can simply return + // a response instead of using the API to reply. Maybe this is + // a different type of event!! } @@ -166,12 +203,17 @@ function Slackbot(configuration) { throw new Error('Specified port is not a valid number'); } + var static_dir = __dirname + '/public'; + + if (slack_botkit.config && slack_botkit.config.webserver && slack_botkit.config.webserver.static_dir) + static_dir = slack_botkit.config.webserver.static_dir; + slack_botkit.config.port = port; slack_botkit.webserver = express(); slack_botkit.webserver.use(bodyParser.json()); slack_botkit.webserver.use(bodyParser.urlencoded({ extended: true })); - slack_botkit.webserver.use(express.static(__dirname + '/public')); + slack_botkit.webserver.use(express.static(static_dir)); var server = slack_botkit.webserver.listen( slack_botkit.config.port, @@ -188,17 +230,15 @@ function Slackbot(configuration) { // get a team url to redirect the user through oauth process slack_botkit.getAuthorizeURL = function(team_id) { - var url = 'https://slack.com/oauth/authorize'; var scopes = slack_botkit.config.scopes; - url = url + '?client_id=' + slack_botkit.config.clientId + '&scope=' + - scopes.join(',') + '&state=botkit'; + var url = 'https://slack.com/oauth/authorize' + '?client_id=' + + slack_botkit.config.clientId + '&scope=' + scopes.join(',') + '&state=botkit'; - if (team_id) { - url = url + '&team=' + team_id; - } - if (slack_botkit.config.redirectUri) { - url = url + '&redirect_uri=' + slack_botkit.config.redirectUri; - } + if (team_id) + url += '&team=' + team_id; + + if (slack_botkit.config.redirectUri) + url += '&redirect_uri=' + slack_botkit.config.redirectUri; return url; @@ -308,6 +348,7 @@ function Slackbot(configuration) { slack_botkit.trigger('oauth_error', [err]); } else { + req.identity = identity; // we need to deal with any team-level provisioning info // like incoming webhooks and bot users @@ -416,7 +457,9 @@ function Slackbot(configuration) { slack_botkit.log('** Setting up custom handlers for processing Slack messages'); slack_botkit.on('message_received', function(bot, message) { - + var mentionSyntax = '<@' + bot.identity.id + '(\\|' + bot.identity.name.replace('.', '\\.') + ')?>'; + var mention = new RegExp(mentionSyntax, 'i'); + var direct_mention = new RegExp('^' + mentionSyntax, 'i'); if (message.ok != undefined) { // this is a confirmation of something we sent. @@ -463,7 +506,6 @@ function Slackbot(configuration) { } // remove direct mention so the handler doesn't have to deal with it - var direct_mention = new RegExp('^\<\@' + bot.identity.id + '\>', 'i'); message.text = message.text.replace(direct_mention, '') .replace(/^\s+/, '').replace(/^\:\s+/, '').replace(/^\s+/, ''); @@ -481,8 +523,6 @@ function Slackbot(configuration) { return false; } - var direct_mention = new RegExp('^\<\@' + bot.identity.id + '\>', 'i'); - var mention = new RegExp('\<\@' + bot.identity.id + '\>', 'i'); if (message.text.match(direct_mention)) { // this is a direct mention diff --git a/lib/Slack_web_api.js b/lib/Slack_web_api.js index ee350ea6c..a74bda592 100755 --- a/lib/Slack_web_api.js +++ b/lib/Slack_web_api.js @@ -15,14 +15,53 @@ module.exports = function(bot, config) { request.post(this.api_url + command, function(error, response, body) { bot.debug('Got response', error, body); if (!error && response.statusCode == 200) { - var json = JSON.parse(body); + var json; + try { + json = JSON.parse(body); + } catch (err) { + if (cb) return cb(err || 'Invalid JSON'); + return; + } + if (json.ok) { if (cb) cb(null, json); } else { if (cb) cb(json.error, json); } } else { - if (cb) cb(error); + if (cb) cb(error || 'Invalid response'); + } + }).form(options); + }, + callAPIWithoutToken: function(command, options, cb) { + bot.log('** API CALL: ' + slack_api.api_url + command); + if (!options.client_id) { + options.client_id = bot.config.clientId; + } + if (!options.client_secret) { + options.client_secret = bot.config.clientSecret; + } + if (!options.redirect_uri) { + options.redirect_uri = bot.config.redirectUri; + } + request.post(this.api_url + command, function(error, response, body) { + bot.debug('Got response', error, body); + if (!error && response.statusCode == 200) { + var json; + try { + json = JSON.parse(body); + } catch (err) { + if (cb) return cb(err || 'Invalid JSON'); + return; + } + + if (json.ok) { + if (cb) cb(null, json); + } else { + if (cb) cb(json.error, json); + } + } else { + if (cb) cb(error || 'Invalid response'); } }).form(options); }, @@ -85,7 +124,7 @@ module.exports = function(bot, config) { slack_api.callAPI('chat.delete', options, cb); }, postMessage: function(options, cb) { - if (options.attachments && typeof(options.attachments)!='string') { + if (options.attachments && typeof(options.attachments) != 'string') { options.attachments = JSON.stringify(options.attachments); } slack_api.callAPI('chat.postMessage', options, cb); @@ -197,6 +236,17 @@ module.exports = function(bot, config) { slack_api.callAPI('mpim.open', options, cb); } }, + pins: { + add: function(options, cb) { + slack_api.callAPI('pins.add', options, cb); + }, + list: function(options, cb) { + slack_api.callAPI('pins.list', options, cb); + }, + remove: function(options, cb) { + slack_api.callAPI('pins.remove', options, cb); + } + }, reactions: { add: function(options, cb) { slack_api.callAPI('reactions.add', options, cb); diff --git a/lib/Slackbot_worker.js b/lib/Slackbot_worker.js index c068e6afb..28cee1b89 100755 --- a/lib/Slackbot_worker.js +++ b/lib/Slackbot_worker.js @@ -1,15 +1,30 @@ var Ws = require('ws'); var request = require('request'); var slackWebApi = require(__dirname + '/Slack_web_api.js'); +var HttpsProxyAgent = require('https-proxy-agent'); +var Back = require('back'); + module.exports = function(botkit, config) { var bot = { botkit: botkit, config: config || {}, utterances: botkit.utterances, - api: slackWebApi(botkit, config || {}) + api: slackWebApi(botkit, config || {}), + identity: { // default identity values + id: null, + name: '', + } }; + var pingIntervalId = null; + var lastPong = 0; + var retryBackoff = null; + + // config.retry, can be Infinity too + var retryEnabled = bot.config.retry ? true : false; + var maxRetry = isNaN(bot.config.retry) || bot.config.retry <= 0 ? 3 : bot.config.retry; + /** * Set up API to send incoming webhook */ @@ -44,9 +59,62 @@ module.exports = function(botkit, config) { return bot; }; - bot.closeRTM = function() { - if (bot.rtm) + bot.closeRTM = function(err) { + if (bot.rtm) { + bot.rtm.removeAllListeners(); bot.rtm.close(); + } + + if (pingIntervalId) { + clearInterval(pingIntervalId); + } + + lastPong = 0; + botkit.trigger('rtm_close', [bot, err]); + + // only retry, if enabled, when there was an error + if (err && retryEnabled) { + reconnect(); + } + }; + + + function reconnect(err) { + var options = { + minDelay: 1000, + maxDelay: 30000, + retries: maxRetry + }; + var back = retryBackoff || (retryBackoff = new Back(options)); + return back.backoff(function(fail) { + if (fail) { + botkit.log.error('** BOT ID:', bot.identity.name, '...reconnect failed after #' + + back.settings.attempt + ' attempts and ' + back.settings.timeout + 'ms'); + botkit.trigger('rtm_reconnect_failed', [bot, err]); + return; + } + + botkit.log.notice('** BOT ID:', bot.identity.name, '...reconnect attempt #' + + back.settings.attempt + ' of ' + options.retries + ' being made after ' + back.settings.timeout + 'ms'); + bot.startRTM(function(err) { + if (err) { + return reconnect(err); + } + retryBackoff = null; + }); + }); + } + + /** + * Shutdown and cleanup the spawned worker + */ + bot.destroy = function() { + if (retryBackoff) { + retryBackoff.close(); + retryBackoff = null; + } + bot.closeRTM(); + botkit.shutdown(); }; bot.startRTM = function(cb) { @@ -73,25 +141,54 @@ module.exports = function(botkit, config) { * Could be stored & cached for later use. */ - botkit.log.notice('** BOT ID: ', bot.identity.name, ' ...attempting to connect to RTM!'); + botkit.log.notice('** BOT ID:', bot.identity.name, '...attempting to connect to RTM!'); - bot.rtm = new Ws(res.url); + var agent = null; + var proxyUrl = process.env.https_proxy || process.env.http_proxy; + if (proxyUrl) { + agent = new HttpsProxyAgent(proxyUrl); + } + + bot.rtm = new Ws(res.url, null, {agent: agent}); bot.msgcount = 1; + bot.rtm.on('pong', function(obj) { + lastPong = Date.now(); + botkit.debug('PONG received'); + }); + bot.rtm.on('open', function() { - botkit.trigger('rtm_open', [this]); + + pingIntervalId = setInterval(function() { + if (lastPong && lastPong + 12000 < Date.now()) { + var err = new Error('Stale RTM connection, closing RTM'); + bot.closeRTM(err); + return; + } + + botkit.debug('PING sent'); + bot.rtm.ping(null, null, true); + }, 5000); + + botkit.trigger('rtm_open', [bot]); bot.rtm.on('message', function(data, flags) { - var message = JSON.parse(data); + var message = null; + try { + message = JSON.parse(data); + } catch (err) { + console.log('** RECEIVED BAD JSON FROM SLACK'); + } /** * Lets construct a nice quasi-standard botkit message * it leaves the main slack message at the root * but adds in additional fields for internal use! * (including the teams api details) */ - botkit.receiveMessage(bot, message); - + if (message != null) { + botkit.receiveMessage(bot, message); + } }); botkit.startTicking(); @@ -101,10 +198,16 @@ module.exports = function(botkit, config) { bot.rtm.on('error', function(err) { botkit.log.error('RTM websocket error!', err); + if (pingIntervalId) { + clearInterval(pingIntervalId); + } botkit.trigger('rtm_close', [bot, err]); }); bot.rtm.on('close', function() { + if (pingIntervalId) { + clearInterval(pingIntervalId); + } botkit.trigger('rtm_close', [bot]); }); }); @@ -173,14 +276,13 @@ module.exports = function(botkit, config) { }); }; - bot.say = function(message, cb) { - botkit.debug('SAY ', message); + bot.send = function(message, cb) { + botkit.debug('SAY', message); /** * Construct a valid slack message. */ var slack_message = { - id: message.id || bot.msgcount, type: message.type || 'message', channel: message.channel, text: message.text || null, @@ -189,8 +291,8 @@ module.exports = function(botkit, config) { link_names: message.link_names || null, attachments: message.attachments ? JSON.stringify(message.attachments) : null, - unfurl_links: message.unfurl_links || null, - unfurl_media: message.unfurl_media || null, + unfurl_links: typeof message.unfurl_links !== 'undefined' ? message.unfurl_links : null, + unfurl_media: typeof message.unfurl_media !== 'undefined' ? message.unfurl_media : null, icon_url: message.icon_url || null, icon_emoji: message.icon_emoji || null, }; @@ -225,6 +327,9 @@ module.exports = function(botkit, config) { if (!bot.rtm) throw new Error('Cannot use the RTM API to send messages.'); + slack_message.id = message.id || bot.msgcount; + + try { bot.rtm.send(JSON.stringify(slack_message), function(err) { if (err) { @@ -280,7 +385,12 @@ module.exports = function(botkit, config) { msg.channel = src.channel; msg.response_type = 'in_channel'; - request.post(src.response_url, function(err, resp, body) { + var requestOptions = { + uri: src.response_url, + method: 'POST', + json: msg + }; + request(requestOptions, function(err, resp, body) { /** * Do something? */ @@ -290,7 +400,7 @@ module.exports = function(botkit, config) { } else { cb && cb(); } - }).form(JSON.stringify(msg)); + }); } }; @@ -330,7 +440,13 @@ module.exports = function(botkit, config) { msg.channel = src.channel; msg.response_type = 'ephemeral'; - request.post(src.response_url, function(err, resp, body) { + + var requestOptions = { + uri: src.response_url, + method: 'POST', + json: msg + }; + request(requestOptions, function(err, resp, body) { /** * Do something? */ @@ -340,7 +456,7 @@ module.exports = function(botkit, config) { } else { cb && cb(); } - }).form(JSON.stringify(msg)); + }); } }; @@ -409,6 +525,21 @@ module.exports = function(botkit, config) { botkit.tasks[t].convos[c].source_message.channel == message.channel ) { botkit.debug('FOUND EXISTING CONVO!'); + + // modify message text to prune off the bot's name (@bot hey -> hey) + // and trim whitespace that is sometimes added + // this would otherwise happen in the handleSlackEvents function + // which does not get called for messages attached to conversations. + + if (message.text) { + message.text = message.text.trim(); + } + + var direct_mention = new RegExp('^\<\@' + bot.identity.id + '\>', 'i'); + + message.text = message.text.replace(direct_mention, '') + .replace(/^\s+/, '').replace(/^\:\s+/, '').replace(/^\s+/, ''); + cb(botkit.tasks[t].convos[c]); return; } diff --git a/lib/TwilioIPMBot.js b/lib/TwilioIPMBot.js new file mode 100644 index 000000000..52f2ca4c0 --- /dev/null +++ b/lib/TwilioIPMBot.js @@ -0,0 +1,402 @@ +var Botkit = require(__dirname + '/CoreBot.js'); +var request = require('request'); +var express = require('express'); +var bodyParser = require('body-parser'); +var twilio = require('twilio'); +var async = require('async'); + +var AccessToken = twilio.AccessToken; +var IpMessagingGrant = AccessToken.IpMessagingGrant; + +function Twiliobot(configuration) { + + // Create a core botkit bot + var twilio_botkit = Botkit(configuration || {}); + + // customize the bot definition, which will be used when new connections + // spawn! + twilio_botkit.defineBot(function(botkit, config) { + var bot = { + botkit: botkit, + config: config || {}, + utterances: botkit.utterances, + }; + + bot.startConversation = function(message, cb) { + botkit.startConversation(this, message, cb); + }; + + bot.send = function(message, cb) { + botkit.debug('SEND ', message); + + if (bot.identity === null || bot.identity === '') { + bot.api.channels(message.channel).messages.create({ + body: message.text, + }).then(function(response) { + cb(null, response); + }).catch(function(err) { + cb(err); + }); + } else { + bot.api.channels(message.channel).messages.create({ + body: message.text, + from: bot.identity + }).then(function(response) { + cb(null, response); + }).catch(function(err) { + cb(err); + }); + } + }; + + bot.reply = function(src, resp, cb) { + var msg = {}; + + if (typeof(resp) == 'string') { + msg.text = resp; + } else { + msg = resp; + } + + msg.user = src.user; + msg.channel = src.channel; + + bot.say(msg, cb); + }; + + bot.autoJoinChannels = function() { + bot.api.channels.list().then(function(full_channel_list) { + if (bot.config.autojoin === true) { + bot.channels = full_channel_list; + bot.channels.channels.forEach(function(chan) { + bot.api.channels(chan.sid).members.create({ + identity: bot.identity + }).then(function(response) { + botkit.debug('added ' + + bot.identity + ' as a member of the ' + chan.friendly_name); + }).fail(function(error) { + botkit.debug('Couldn\'t join the channel: ' + + chan.friendly_name + ': ' + error); + }); + }); + } else if (bot.identity) { + + // load up a list of all the channels that the bot is currently + + bot.channels = { + channels: [] + }; + + async.each(full_channel_list.channels, function(chan, next) { + bot.api.channels(chan.sid).members.list().then(function(members) { + for (var x = 0; x < members.members.length; x++) { + if (members.members[x].identity == bot.identity) { + bot.channels.channels.push(chan); + } + } + next(); + }).fail(function(error) { + botkit.log('Error loading channel member list: ', error); + next(); + }); + }); + } + }).fail(function(error) { + botkit.log('Error loading channel list: ' + error); + // fails if no channels exist + // set the channels to empty + bot.channels = { channels: [] }; + }); + + }; + + bot.configureBotIdentity = function() { + if (bot.identity !== null || bot.identity !== '') { + var userRespIter = 0; + var existingIdentity = null; + + // try the get by identity thing + bot.api.users(bot.identity).get().then(function(response) { + bot.autoJoinChannels(); + }).fail(function(error) { + // if not make the new user and see if they need to be added to all the channels + bot.api.users.create({ + identity: bot.identity + }).then(function(response) { + bot.autoJoinChannels(); + }).fail(function(error) { + botkit.log('Could not get Bot Identity:'); + botkit.log(error); + process.exit(1); + }); + }); + } + }; + + /** + * This handles the particulars of finding an existing conversation or + * topic to fit the message into... + */ + bot.findConversation = function(message, cb) { + botkit.debug('CUSTOM FIND CONVO', message.user, message.channel); + for (var t = 0; t < botkit.tasks.length; t++) { + for (var c = 0; c < botkit.tasks[t].convos.length; c++) { + if ( + botkit.tasks[t].convos[c].isActive() && + botkit.tasks[t].convos[c].source_message.user == message.user && + botkit.tasks[t].convos[c].source_message.channel == message.channel + ) { + botkit.debug('FOUND EXISTING CONVO!'); + cb(botkit.tasks[t].convos[c]); + return; + } + } + } + + cb(); + }; + + + bot.client = new twilio.IpMessagingClient(config.TWILIO_ACCOUNT_SID, config.TWILIO_AUTH_TOKEN); + bot.api = bot.client.services(config.TWILIO_IPM_SERVICE_SID); + + if (config.identity) { + bot.identity = config.identity; + bot.configureBotIdentity(); + } + + return bot; + + }); + + + twilio_botkit.setupWebserver = function(port, cb) { + + if (!port) { + throw new Error('Cannot start webserver without a port'); + } + if (isNaN(port)) { + throw new Error('Specified port is not a valid number'); + } + + twilio_botkit.config.port = port; + + twilio_botkit.webserver = express(); + twilio_botkit.webserver.use(bodyParser.json()); + twilio_botkit.webserver.use(bodyParser.urlencoded({ extended: true })); + twilio_botkit.webserver.use(express.static(__dirname + '/public')); + + var server = twilio_botkit.webserver.listen( + twilio_botkit.config.port, + function() { + twilio_botkit.log('** Starting webserver on port ' + + twilio_botkit.config.port); + if (cb) { cb(null, twilio_botkit.webserver); } + }); + + return twilio_botkit; + + }; + + + + + // set up a web route for receiving outgoing webhooks and/or slash commands + twilio_botkit.createWebhookEndpoints = function(webserver, bot) { + + twilio_botkit.log( + '** Serving webhook endpoints for receiving messages ' + + 'webhooks at: http://MY_HOST:' + twilio_botkit.config.port + '/twilio/receive'); + webserver.post('/twilio/receive', function(req, res) { + // ensure all messages + // have a user & channel + var message = req.body; + if (req.body.EventType == 'onMessageSent') { + + // customize fields to be compatible with Botkit + message.text = req.body.Body; + message.from = req.body.From; + message.to = req.body.To; + message.user = req.body.From; + message.channel = req.body.ChannelSid; + + twilio_botkit.receiveMessage(bot, message); + + }else if (req.body.EventType == 'onChannelAdded' || req.body.EventType == 'onChannelAdd') { + // this event has a channel sid but not a user + message.channel = req.body.ChannelSid; + twilio_botkit.trigger(req.body.EventType, [bot, message]); + + }else if (req.body.EventType == 'onChannelDestroyed' || req.body.EventType == 'onChannelDestroy') { + // this event has a channel sid but not a user + message.channel = req.body.ChannelSid; + twilio_botkit.trigger(req.body.EventType, [bot, message]); + + }else if (req.body.EventType == 'onMemberAdded' || req.body.EventType == 'onMemberAdd') { + // should user be MemberSid the The Member Sid of the newly added Member + message.user = req.body.Identity; + message.channel = req.body.ChannelSid; + twilio_botkit.trigger(req.body.EventType, [bot, message]); + } else if (req.body.EventType == 'onMemberRemoved' || req.body.EventType == 'onMemberRemove') { + message.user = req.body.Identity; + message.channel = req.body.ChannelSid; + twilio_botkit.trigger(req.body.EventType, [bot, message]); + + if (req.body.EventType == 'onMemberRemoved') { + + } + } else { + twilio_botkit.trigger(req.body.EventType, [bot, message]); + } + + res.status(200); + res.send('ok'); + + + }); + + twilio_botkit.startTicking(); + + return twilio_botkit; + }; + + + + // handle events here + twilio_botkit.handleTwilioEvents = function() { + twilio_botkit.log('** Setting up custom handlers for processing Twilio messages'); + twilio_botkit.on('message_received', function(bot, message) { + + + + if (bot.identity && message.from == bot.identity) { + return false; + } + + if (!message.text) { + // message without text is probably an edit + return false; + } + + if (bot.identity) { + var channels = bot.channels.channels; + + // if its not in a channel with the bot + var apprChan = channels.filter(function(ch) { + return ch.sid == message.channel; + }); + + if (apprChan.length === 0) { + return false; + } + } + }); + + + // if a member is removed from a channel, check to see if it matches the bot's identity + // and if so remove it from the list of channels the bot listens to + twilio_botkit.on('onMemberRemoved', function(bot, message) { + if (bot.identity && message.user == bot.identity) { + // remove that channel from bot.channels.channels + var chan_to_rem = bot.channels.channels.map(function(ch) { + return ch.sid; + }).indexOf(message.channel); + + if (chan_to_rem != -1) { + bot.channels.channels.splice(chan_to_rem, 1); + twilio_botkit.debug('Unsubscribing from channel because of memberremove.'); + + } + } else if (bot.identity) { + var channels = bot.channels.channels; + + // if its not in a channel with the bot + var apprChan = channels.filter(function(ch) { + return ch.sid == message.channel; + }); + + if (apprChan.length === 0) { + return false; + } + } + + if (bot.identity && bot.identity == message.user) { + twilio_botkit.trigger('bot_channel_leave', [bot, message]); + } else { + twilio_botkit.trigger('user_channel_leave', [bot, message]); + } + }); + + twilio_botkit.on('onMemberAdded', function(bot, message) { + if (bot.identity && message.user == bot.identity) { + bot.api.channels(message.channel).get().then(function(response) { + bot.channels.channels.push(response); + twilio_botkit.debug('Subscribing to channel because of memberadd.'); + + }).fail(function(error) { + botkit.log(error); + }); + } else if (bot.identity) { + var channels = bot.channels.channels; + + // if its not in a channel with the bot + var apprChan = channels.filter(function(ch) { + return ch.sid == message.channel; + }); + + if (apprChan.length === 0) { + return false; + } + } + + if (bot.identity && bot.identity == message.user) { + twilio_botkit.trigger('bot_channel_join', [bot, message]); + } else { + twilio_botkit.trigger('user_channel_join', [bot, message]); + } + + }); + + + // if a channel is destroyed, remove it from the list of channels this bot listens to + twilio_botkit.on('onChannelDestroyed', function(bot, message) { + if (bot.identity) { + var chan_to_rem = bot.channels.channels.map(function(ch) { + return ch.sid; + }).indexOf(message.channel); + if (chan_to_rem != -1) { + bot.channels.channels.splice(chan_to_rem, 1); + twilio_botkit.debug('Unsubscribing from destroyed channel.'); + } + } + }); + + // if a channel is created, and the bot is set in autojoin mode, join the channel + twilio_botkit.on('onChannelAdded', function(bot, message) { + if (bot.identity && bot.config.autojoin === true) { + // join the channel + bot.api.channels(message.channel).members.create({ + identity: bot.identity + }).then(function(response) { + bot.api.channels(message.channel).get().then(function(response) { + bot.channels.channels.push(response); + twilio_botkit.debug('Subscribing to new channel.'); + + }).fail(function(error) { + botkit.log(error); + }); + }).fail(function(error) { + botkit.log(error); + }); + } + }); + + }; + + twilio_botkit.handleTwilioEvents(); + + return twilio_botkit; + +} + +module.exports = Twiliobot; diff --git a/lib/middleware/slack_authentication.js b/lib/middleware/slack_authentication.js new file mode 100644 index 000000000..1aea5518d --- /dev/null +++ b/lib/middleware/slack_authentication.js @@ -0,0 +1,63 @@ +/** + * Authentication module composed of an Express middleware used to validate + * incoming requests from the Slack API for Slash commands and outgoing + * webhooks. + */ + +// Comparison constant +var TOKEN_NOT_FOUND = -1; + +function init(tokens) { + var authenticationTokens = flatten(tokens); + + if (authenticationTokens.length === 0) { + console.warn('No auth tokens provided, webhook endpoints will always reply HTTP 401.'); + } + + /** + * Express middleware that verifies a Slack token is passed; + * if the expected token value is not passed, end with request test + * with a 401 HTTP status code. + * + * Note: Slack is totally wacky in that the auth token is sent in the body + * of the request instead of a header value. + * + * @param {object} req - Express request object + * @param {object} res - Express response object + * @param {function} next - Express callback + */ + function authenticate(req, res, next) { + if (!req.body || !req.body.token || authenticationTokens.indexOf(req.body.token) === TOKEN_NOT_FOUND) { + res.status(401).send({ + 'code': 401, + 'message': 'Unauthorized' + }); + + return; + } + + slack_botkit.log( + '** Requiring token authentication for webhook endpoints for Slash commands ' + + 'and outgoing webhooks; configured ' + tokens.length + ' tokens' + ); + next(); + } + + return authenticate; +} +/** + * Function that flattens a series of arguments into an array. + * + * @param {Array} args - No token (null), single token (string), or token array (array) + * @returns {Array} - Every element of the array is an authentication token + */ +function flatten(args) { + var result = []; + + // convert a variable argument list to an array + args.forEach(function(arg) { + result = result.concat(arg); + }); + return result; + } +module.exports = init; diff --git a/lib/storage/firebase_storage.js b/lib/storage/firebase_storage.js deleted file mode 100644 index 7bf320c98..000000000 --- a/lib/storage/firebase_storage.js +++ /dev/null @@ -1,100 +0,0 @@ -/* -Firebase storage module for bots. - -Note that this storage module does not specify how to authenticate to Firebase. -There are many methods of user authentication for Firebase. -Please read: https://www.firebase.com/docs/web/guide/user-auth.html - -Supports storage of data on a team-by-team, user-by-user, and chnnel-by-channel basis. - -save can be used to store arbitrary object. -These objects must include an id by which they can be looked up. -It is recommended to use the team/user/channel id for this purpose. -Example usage of save: -controller.storage.teams.save({id: message.team, foo:"bar"}, function(err){ - if (err) - console.log(err)` -}); - -get looks up an object by id. -Example usage of get: -controller.storage.teams.get(message.team, function(err, team_data){ - if (err) - console.log(err) - else - console.log(team_data) -}); -*/ - -var Firebase = require('firebase'); - -module.exports = function(config) { - - if (!config && !config.firebase_uri) - throw new Error('Need to provide firebase address. This should look something like ' + - '"https://botkit-example.firebaseio.com/"'); - - var rootRef = new Firebase(config.firebase_uri); - var teamsRef = rootRef.child('teams'); - var usersRef = rootRef.child('users'); - var channelsRef = rootRef.child('channels'); - - var get = function(firebaseRef) { - return function(id, cb) { - firebaseRef.child(id).once('value', - function(records) { - cb(undefined, records.val()); - }, - function(err) { - cb(err, undefined); - } - ); - }; - }; - - var save = function(firebaseRef) { - return function(data, cb) { - var firebase_update = {}; - firebase_update[data.id] = data; - firebaseRef.update(firebase_update, cb); - }; - }; - - var all = function(firebaseRef) { - return function(cb) { - firebaseRef.once('value', - function(records) { - var list = []; - for (key of Object.keys(records.val())) { - list.push(records.val()[key]); - } - cb(undefined, list); - }, - function(err) { - cb(err, undefined); - } - ); - }; - }; - - var storage = { - teams: { - get: get(teamsRef), - save: save(teamsRef), - all: all(teamsRef) - }, - channels: { - get: get(channelsRef), - save: save(channelsRef), - all: all(channelsRef) - }, - users: { - get: get(usersRef), - save: save(usersRef), - all: all(usersRef) - } - }; - - return storage; - -}; diff --git a/lib/storage/redis_storage.js b/lib/storage/redis_storage.js deleted file mode 100644 index 74fb07132..000000000 --- a/lib/storage/redis_storage.js +++ /dev/null @@ -1,64 +0,0 @@ -var redis = require('redis'); //https://github.com/NodeRedis/node_redis - -/* - * All optional - * - * config = { - * namespace: namespace, - * host: host, - * port: port - * } - * // see - * https://github.com/NodeRedis/node_redis - * #options-is-an-object-with-the-following-possible-properties for a full list of the valid options - */ -module.exports = function(config) { - config = config || {}; - config.namespace = config.namespace || 'botkit:store'; - - var storage = {}, - client = redis.createClient(config), // could pass specific redis config here - methods = config.methods || ['teams', 'users', 'channels']; - - // Implements required API methods - for (var i = 0; i < methods.length; i++) { - storage[methods[i]] = function(hash) { - return { - get: function(id, cb) { - client.hget(config.namespace + ':' + hash, id, function(err, res) { - cb(err, JSON.parse(res)); - }); - }, - save: function(object, cb) { - if (!object.id) // Silently catch this error? - return cb(new Error('The given object must have an id property'), {}); - client.hset(config.namespace + ':' + hash, object.id, JSON.stringify(object), cb); - }, - all: function(cb, options) { - client.hgetall(config.namespace + ':' + hash, function(err, res) { - if (err) - return cb(err, {}); - - if (null === res) - return cb(err, res); - - var parsed; - var array = []; - - for (var i in res) { - parsed = JSON.parse(res[i]); - res[i] = parsed; - array.push(parsed); - } - - cb(err, options && options.type === 'object' ? res : array); - }); - }, - allById: function(cb) { - this.all(cb, {type: 'object'}); - } - }; - }(methods[i]); - } - return storage; -}; diff --git a/lib/storage/storage_test.js b/lib/storage/storage_test.js index 45a7c202d..5a868b2ec 100644 --- a/lib/storage/storage_test.js +++ b/lib/storage/storage_test.js @@ -1,6 +1,6 @@ /* Tests for storage modules. -This file currently test simple_storage.js and mongo_storage.js. +This file currently test simple_storage.js, redis_storage, and firebase_storage. If you build a new storage module, you must add it to this test file before your PR will be considered. @@ -30,6 +30,10 @@ var testStorageMethod = function(storageMethod) { console.log(data); test.assert(data.foo === testObj0.foo); }); + storageMethod.get('shouldnt-be-here', function(err, data) { + test.assert(err.displayName === 'NotFound'); + test.assert(!data); + }); storageMethod.all(function(err, data) { test.assert(!err); console.log(data); diff --git a/package.json b/package.json index deab50733..ca5c11bf3 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,27 @@ { "name": "botkit", - "version": "0.0.7", + "version": "0.2.0", "description": "Building blocks for Building Bots", "main": "lib/Botkit.js", "dependencies": { + "async": "^2.0.0-rc.5", + "back": "^1.0.1", "body-parser": "^1.14.2", + "command-line-args": "^2.1.6", "express": "^4.13.3", + "https-proxy-agent": "^1.0.0", "jfs": "^0.2.6", + "localtunnel": "^1.8.1", "mustache": "^2.2.1", "request": "^2.67.0", - "ws": "^1.0.0" + "twilio": "^2.9.1", + "ware": "^1.3.0", + "ws": "^1.0.1" }, "devDependencies": { "jscs": "^2.7.0", - "node-env-file": "^0.1.8", + "mocha": "^2.4.5", "should": "^8.0.2", - "tap-spec": "^4.1.1", - "tape": "^4.4.0", "winston": "^2.1.1" }, "scripts": { diff --git a/readme-facebook.md b/readme-facebook.md new file mode 100644 index 000000000..c2d94f34e --- /dev/null +++ b/readme-facebook.md @@ -0,0 +1,194 @@ +# Botkit and Facebook + +Botkit is designed to ease the process of designing and running useful, creative bots that live inside [Slack](http://slack.com), [Facebook Messenger](http://facebook.com), [Twilio IP Messaging](https://www.twilio.com/docs/api/ip-messaging), and other messaging platforms. + + +Botkit features a comprehensive set of tools +to deal with [Facebooks's Messenger platform](https://developers.facebook.com/docs/messenger-platform/implementation), and allows +developers to build interactive bots and applications that send and receive messages just like real humans. Facebook bots can be connected to Facebook Pages, and can be triggered using a variety of [useful web plugins](https://developers.facebook.com/docs/messenger-platform/plugin-reference). + +This document covers the Facebook-specific implementation details only. [Start here](readme.md) if you want to learn about to develop with Botkit. + +Table of Contents + +* [Getting Started](#getting-started) +* [Facebook-specific Events](#facebook-specific-events) +* [Working with Facebook Webhooks](#working-with-facebook-messenger) +* [Using Structured Messages and Postbacks](#using-structured-messages-and-postbacks) + +## Getting Started + +1) Install Botkit [more info here](readme.md#installation) + +2) Create a [Facebook App for Web](https://developers.facebook.com/quickstarts/?platform=web) and note down or [create a new Facebook Page](https://www.facebook.com/pages/create/). Your Facebook page will be used for the app's identity. + +3) [Get a page access token for your app](https://developers.facebook.com/docs/messenger-platform/implementation#page_access_token) + +Copy this token, you'll need it! + +4) Define your own "verify token" - this a string that you control that Facebook will use to verify your web hook endpoint. + +5) Run the example bot app, using the two tokens you just created. If you are _not_ running your bot at a public, SSL-enabled internet address, use the --lt option and note the URL it gives you. + +``` +page_token= verify_token= node facebook_bot.js [--lt [--ltsubdomain CUSTOM_SUBDOMAIN]] +``` + +6) [Set up a webhook endpoint for your app](https://developers.facebook.com/docs/messenger-platform/implementation#setting_webhooks) that uses your public URL. Use the verify token you defined in step 4! + +7) Your bot should be online! Within Facebook, find your page, and click the "Message" button in the header. + +Try: + * who are you? + * call me Bob + * shutdown + + +### Things to note + +Since Facebook delivers messages via web hook, your application must be available at a public internet address. Additionally, Facebook requires this address to use SSL. Luckily, you can use [LocalTunnel](https://localtunnel.me/) to make a process running locally or in your dev environment available in a Facebook-friendly way. + +When you are ready to go live, consider [LetsEncrypt.org](http://letsencrypt.org), a _free_ SSL Certificate Signing Authority which can be used to secure your website very quickly. It is fabulous and we love it. + +## Facebook-specific Events + +Once connected to Facebook, bots receive a constant stream of events. + +Normal messages will be sent to your bot using the `message_received` event. In addition, several other events may fire, depending on your implementation and the webhooks you subscribed to within your app's Facebook configuration. + +| Event | Description +|--- |--- +| message_received | a message was received by the bot +| facebook_postback | user clicked a button in an attachment and triggered a webhook postback +| message_delivered | a confirmation from Facebook that a message has been received +| facebook_optin | a user has clicked the [Send-to-Messenger plugin](https://developers.facebook.com/docs/messenger-platform/implementation#send_to_messenger_plugin) + +All incoming events will contain the fields `user` and `channel`, both of which represent the Facebook user's ID, and a `timestamp` field. + +`message_received` events will also contain either a `text` field or an `attachment` field. + +`facebook_postback` events will contain a `payload` field. + +More information about the data found in these fields can be found [here](https://developers.facebook.com/docs/messenger-platform/webhook-reference). + +## Working with Facebook Messenger + +Botkit receives messages from Facebook using webhooks, and sends messages using Facebook's APIs. This means that your bot application must present a web server that is publicly addressable. Everything you need to get started is already included in Botkit. + +To connect your bot to Facebook, [follow the instructions here](https://developers.facebook.com/docs/messenger-platform/implementation). You will need to collect your `page token` as well as a `verify token` that you define yourself and configure inside Facebook's app settings. A step by step guide [can be found here](#getting-started). Since you must *already be running* your Botkit app to configure your Facebook app, there is a bit of back-and-forth. It's ok! You can do it. + +Here is the complete code for a basic Facebook bot: + +```javascript +var Botkit = require('botkit'); +var controller = Botkit.facebookbot({ + access_token: process.env.access_token, + verify_token: process.env.verify_token, +}) + +var bot = controller.spawn({ +}); + +// if you are already using Express, you can use your own server instance... +// see "Use BotKit with an Express web server" +controller.setupWebserver(process.env.port,function(err,webserver) { + controller.createWebhookEndpoints(controller.webserver, bot, function() { + console.log('This bot is online!!!'); + }); +}); + +// this is triggered when a user clicks the send-to-messenger plugin +controller.on('facebook_optin', function(bot, message) { + + bot.reply(message, 'Welcome to my app!'); + +}); + +// user said hello +controller.hears(['hello'], 'message_received', function(bot, message) { + + bot.reply(message, 'Hey there.'); + +}); + +controller.hears(['cookies'], 'message_received', function(bot, message) { + + bot.startConversation(message, function(err, convo) { + + convo.say('Did someone say cookies!?!!'); + convo.ask('What is your favorite type of cookie?', function(response, convo) { + convo.say('Golly, I love ' + response.text + ' too!!!'); + convo.next(); + }); + }); +}); +``` + + +#### controller.setupWebserver() +| Argument | Description +|--- |--- +| port | port for webserver +| callback | callback function + +Setup an [Express webserver](http://expressjs.com/en/index.html) for +use with `createWebhookEndpoints()` + +If you need more than a simple webserver to receive webhooks, +you should by all means create your own Express webserver! + +The callback function receives the Express object as a parameter, +which may be used to add further web server routes. + +#### controller.createWebhookEndpoints() + +This function configures the route `https://_your_server_/facebook/receive` +to receive webhooks from Facebook. + +This url should be used when configuring Facebook. + +## Using Structured Messages and Postbacks + +You can attach little bubbles + +And in those bubbles can be buttons +and when a user clicks the button, it sends a postback with the value. + +```javascript +controller.hears('test', 'message_received', function(bot, message) { + + var attachment = { + 'type':'template', + 'payload':{ + 'template_type':'generic', + 'elements':[ + { + 'title':'Chocolate Cookie', + 'image_url':'http://cookies.com/cookie.png', + 'subtitle':'A delicious chocolate cookie', + 'buttons':[ + { + 'type':'postback', + 'title':'Eat Cookie', + 'payload':'chocolate' + } + ] + }, + ] + } + }; + + bot.reply(message, { + attachment: attachment, + }); + +}); + +controller.on('facebook_postback', function(bot, message) { + + if (message.payload == 'chocolate') { + bot.reply(message, 'You ate the chocolate cookie!') + } + +}); +``` diff --git a/readme-slack.md b/readme-slack.md new file mode 100644 index 000000000..9a6125f7f --- /dev/null +++ b/readme-slack.md @@ -0,0 +1,597 @@ +# Botkit and Slack + +Botkit is designed to ease the process of designing and running useful, creative bots that live inside [Slack](http://slack.com), [Facebook Messenger](http://facebook.com), [Twilio IP Messaging](https://www.twilio.com/docs/api/ip-messaging), and other messaging platforms. + +Botkit features a comprehensive set of tools +to deal with [Slack's integration platform](http://api.slack.com), and allows +developers to build both custom integrations for their +team, as well as public "Slack Button" applications that can be +run from a central location, and be used by many teams at the same time. + +This document covers the Slack-specific implementation details only. [Start here](readme.md) if you want to learn about to develop with Botkit. + +Table of Contents + +* [Getting Started](#getting-started) +* [Connecting Your Bot To Slack](#connecting-your-bot-to-slack) +* [Slack-specific Events](#slack-specific-events) +* [Working with Slack Custom Integrations](#working-with-slack-integrations) +* [Using the Slack Button](#use-the-slack-button) + +--- +## Getting Started + +1) Install Botkit [more info here](readme.md#installation) + +2) First make a bot integration inside of your Slack channel. Go here: + +https://my.slack.com/services/new/bot + +Enter a name for your bot. +Make it something fun and friendly, but avoid a single task specific name. +Bots can do lots! Let's not pigeonhole them. + +3) When you click "Add Bot Integration", you are taken to a page where you can add additional details about your bot, like an avatar, as well as customize its name & description. + +Copy the API token that Slack gives you. You'll need it. + +4) Run the example bot app, using the token you just copied: +​ +``` +token=REPLACE_THIS_WITH_YOUR_TOKEN node slack_bot.js +``` +​ +5) Your bot should be online! Within Slack, send it a quick direct message to say hello. It should say hello back! + +Try: + * who are you? + * call me Bob + * shutdown +​ + +### Things to note +​ +Much like a vampire, a bot has to be invited into a channel. DO NOT WORRY bots are not vampires. + +Type: `/invite @` to invite your bot into another channel. + + +## Connecting Your Bot to Slack + +Bot users connect to Slack using a real time API based on web sockets. +The bot connects to Slack using the same protocol that the native Slack clients use! + +To connect a bot to Slack, [get a Bot API token from the Slack integrations page](https://my.slack.com/services/new/bot). + +Note: Since API tokens can be used to connect to your team's Slack, it is best practices to handle API tokens with caution. For example, pass tokens in to your application via evironment variable or command line parameter rather than include it in the code itself. +This is particularly true if you store and use API tokens on behalf of users other than yourself! + +[Read Slack's Bot User documentation](https://api.slack.com/bot-users) + + +#### controller.spawn() +| Argument | Description +|--- |--- +| config | Incoming message object + +Spawn an instance of your bot and connect it to Slack. +This function takes a configuration object which should contain +at least one method of talking to the Slack API. + +To use the real time / bot user API, pass in a token. + +Controllers can also spawn bots that use [incoming webhooks](#incoming-webhooks). + +Spawn `config` object accepts these properties: + +| Name | Value | Description +|--- |--- |--- +| token | String | Slack bot token +| retry | Positive integer or `Infinity` | Maximum number of reconnect attempts after failed connection to Slack's real time messaging API. Retry is disabled by default + + + +#### bot.startRTM() +| Argument | Description +|--- |--- +| callback | _Optional_ Callback in the form function(err,bot,payload) { ... } + +Opens a connection to Slack's real time API. This connection will remain +open until it fails or is closed using `closeRTM()`. + +The optional callback function receives: + +* Any error that occurred while connecting to Slack +* An updated bot object +* The resulting JSON payload of the Slack API command [rtm.start](https://api.slack.com/methods/rtm.start) + +The payload that this callback function receives contains a wealth of information +about the bot and its environment, including a complete list of the users +and channels visible to the bot. This information should be cached and used +when possible instead of calling Slack's API. + +A successful connection the API will also cause a `rtm_open` event to be +fired on the `controller` object. + + +#### bot.closeRTM() + +Close the connection to the RTM. Once closed, an `rtm_close` event is fired +on the `controller` object. + + +```javascript +var Botkit = require('Botkit'); + +var controller = Botkit.slackbot(); + +var bot = controller.spawn({ + token: my_slack_bot_token +}) + +bot.startRTM(function(err,bot,payload) { + if (err) { + throw new Error('Could not connect to Slack'); + } + + // close the RTM for the sake of it in 5 seconds + setTimeout(function() { + bot.closeRTM(); + }, 5000); +}); +``` + +#### bot.destroy() + +Completely shutdown and cleanup the spawned worker. Use `bot.closeRTM()` only to disconnect +but not completely tear down the worker. + + +```javascript +var Botkit = require('Botkit'); +var controller = Botkit.slackbot(); +var bot = controller.spawn({ + token: my_slack_bot_token +}) + +bot.startRTM(function(err, bot, payload) { + if (err) { + throw new Error('Could not connect to Slack'); + } +}); + +// some time later (e.g. 10s) when finished with the RTM connection and worker +setTimeout(bot.destroy.bind(bot), 10000) +``` + +### Slack-Specific Events + +Once connected to Slack, bots receive a constant stream of events - everything from the normal messages you would expect to typing notifications and presence change events. + +Botkit's message parsing and event system does a great deal of filtering on this +real time stream so developers do not need to parse every message. See [Receiving Messages](readme.md#receiving-messages) +for more information about listening for and responding to messages. + +It is also possible to bind event handlers directly to any of the enormous number of native Slack events, as well as a handful of custom events emitted by Botkit. + +You can receive and handle any of the [native events thrown by slack](https://api.slack.com/events). + +```javascript +controller.on('channel_joined',function(bot,message) { + + // message contains data sent by slack + // in this case: + // https://api.slack.com/events/channel_joined + +}); +``` + +You can also receive and handle a long list of additional events caused +by messages that contain a subtype field, [as listed here](https://api.slack.com/events/message) + +```javascript +controller.on('channel_leave',function(bot,message) { + + // message format matches this: + // https://api.slack.com/events/message/channel_leave + +}) +``` + +Finally, Botkit throws a handful of its own events! +Events related to the general operation of bots are below. +When used in conjunction with the Slack Button, Botkit also fires +a [few additional events](#use-the-slack-button). + + +#### User Activity Events: + +| Event | Description +|--- |--- +| message_received | a message was received by the bot +| bot_channel_join | the bot has joined a channel +| user_channel_join | a user has joined a channel +| bot_group_join | the bot has joined a group +| user_group_join | a user has joined a group + +#### Message Received Events +| Event | Description +|--- |--- +| direct_message | the bot received a direct message from a user +| direct_mention | the bot was addressed directly in a channel +| mention | the bot was mentioned by someone in a message +| ambient | the message received had no mention of the bot + +#### Websocket Events: + +| Event | Description +|--- |--- +| rtm_open | a connection has been made to the RTM api +| rtm_close | a connection to the RTM api has closed +| rtm_reconnect_failed | if retry enabled, retry attempts have been exhausted + + +## Working with Slack Integrations + +There are a dizzying number of ways to integrate your application into Slack. +Up to this point, this document has mainly dealt with the real time / bot user +integration. In addition to this type of integration, Botkit also supports: + +* Incoming Webhooks - a way to send (but not receive) messages to Slack +* Outgoing Webhooks - a way to receive messages from Slack based on a keyword or phrase +* Slash Command - a way to add /slash commands to Slack +* Slack Web API - a full set of RESTful API tools to deal with Slack +* The Slack Button - a way to build Slack applications that can be used by multiple teams + + +```javascript +var Botkit = require('botkit'); +var controller = Botkit.slackbot({}) + +var bot = controller.spawn({ + token: my_slack_bot_token +}); + +// use RTM +bot.startRTM(function(err,bot,payload) { + // handle errors... +}); + +// send webhooks +bot.configureIncomingWebhook({url: webhook_url}); +bot.sendWebhook({ + text: 'Hey!', + channel: '#testing', +},function(err,res) { + // handle error +}); + +// receive outgoing or slash commands +// if you are already using Express, you can use your own server instance... +// see "Use BotKit with an Express web server" +controller.setupWebserver(process.env.port,function(err,webserver) { + + controller.createWebhookEndpoints(controller.webserver); + +}); + +controller.on('slash_command',function(bot,message) { + + // reply to slash command + bot.replyPublic(message,'Everyone can see the results of this slash command'); + +}); +``` + +### Incoming webhooks + +Incoming webhooks allow you to send data from your application into Slack. +To configure Botkit to send an incoming webhook, first set one up +via [Slack's integration page](https://my.slack.com/services/new/incoming-webhook/). + +Once configured, use the `sendWebhook` function to send messages to Slack. + +[Read official docs](https://api.slack.com/incoming-webhooks) + +#### bot.configureIncomingWebhook() + +| Argument | Description +|--- |--- +| config | Configure a bot to send webhooks + +Add a webhook configuration to an already spawned bot. +It is preferable to spawn the bot pre-configured, but hey, sometimes +you need to do it later. + +#### bot.sendWebhook() + +| Argument | Description +|--- |--- +| message | A message object +| callback | _Optional_ Callback in the form function(err,response) { ... } + +Pass `sendWebhook` an object that contains at least a `text` field. + This object may also contain other fields defined [by Slack](https://api.slack.com/incoming-webhooks) which can alter the + appearance of your message. + +```javascript +var bot = controller.spawn({ + incoming_webhook: { + url: + } +}) + +bot.sendWebhook({ + text: 'This is an incoming webhook', + channel: '#general', +},function(err,res) { + if (err) { + // ... + } +}); +``` + +### Outgoing Webhooks and Slash commands + +Outgoing webhooks and Slash commands allow you to send data out of Slack. + +Outgoing webhooks are used to match keywords or phrases in Slack. [Read Slack's official documentation here.](https://api.slack.com/outgoing-webhooks) + +Slash commands are special commands triggered by typing a "/" then a command. +[Read Slack's official documentation here.](https://api.slack.com/slash-commands) + +Though these integrations are subtly different, Botkit normalizes the details +so developers may focus on providing useful functionality rather than peculiarities +of the Slack API parameter names. + +Note that since these integrations use send webhooks from Slack to your application, +your application will have to be hosted at a public IP address or domain name, +and properly configured within Slack. + +[Set up an outgoing webhook](https://my.slack.com/services/new/outgoing-webhook) + +[Set up a Slash command](https://my.slack.com/services/new/slash-commands) + +```javascript +controller.setupWebserver(port,function(err,express_webserver) { + controller.createWebhookEndpoints(express_webserver) +}); +``` + +#### Securing Outgoing Webhooks and Slash commands + +You can optionally protect your application with authentication of the requests +from Slack. Slack will generate a unique request token for each Slash command and +outgoing webhook (see [Slack documentation](https://api.slack.com/slash-commands#validating_the_command)). +You can configure the web server to validate that incoming requests contain a valid api token +by adding an express middleware authentication module. + +```javascript +controller.setupWebserver(port,function(err,express_webserver) { + controller.createWebhookEndpoints(express_webserver, ['AUTH_TOKEN', 'ANOTHER_AUTH_TOKEN']); + // you can pass the tokens as an array, or variable argument list + //controller.createWebhookEndpoints(express_webserver, 'AUTH_TOKEN_1', 'AUTH_TOKEN_2'); + // or + //controller.createWebhookEndpoints(express_webserver, 'AUTH_TOKEN'); +}); +``` + +#### Handling `slash_command` and `outgoing_webhook` events + +``` +controller.on('slash_command',function(bot,message) { + + // reply to slash command + bot.replyPublic(message,'Everyone can see this part of the slash command'); + bot.replyPrivate(message,'Only the person who used the slash command can see this.'); + +}) + +controller.on('outgoing_webhook',function(bot,message) { + + // reply to outgoing webhook command + bot.replyPublic(message,'Everyone can see the results of this webhook command'); + +}) +``` + +#### controller.setupWebserver() +| Argument | Description +|--- |--- +| port | port for webserver +| callback | callback function + +Setup an [Express webserver](http://expressjs.com/en/index.html) for +use with `createWebhookEndpoints()` + +If you need more than a simple webserver to receive webhooks, +you should by all means create your own Express webserver! + +The callback function receives the Express object as a parameter, +which may be used to add further web server routes. + +#### controller.createWebhookEndpoints() + +This function configures the route `http://_your_server_/slack/receive` +to receive webhooks from Slack. + +This url should be used when configuring Slack. + +When a slash command is received from Slack, Botkit fires the `slash_command` event. + +When an outgoing webhook is recieved from Slack, Botkit fires the `outgoing_webhook` event. + + +#### bot.replyPublic() + +| Argument | Description +|--- |--- +| src | source message as received from slash or webhook +| reply | reply message (string or object) +| callback | optional callback + +When used with outgoing webhooks, this function sends an immediate response that is visible to everyone in the channel. + +When used with slash commands, this function has the same functionality. However, +slash commands also support private, and delayed messages. See below. +[View Slack's docs here](https://api.slack.com/slash-commands) + +#### bot.replyPrivate() + +| Argument | Description +|--- |--- +| src | source message as received from slash +| reply | reply message (string or object) +| callback | optional callback + + +#### bot.replyPublicDelayed() + +| Argument | Description +|--- |--- +| src | source message as received from slash +| reply | reply message (string or object) +| callback | optional callback + +#### bot.replyPrivateDelayed() + +| Argument | Description +|--- |--- +| src | source message as received from slash +| reply | reply message (string or object) +| callback | optional callback + + + +### Using the Slack Web API + +All (or nearly all - they change constantly!) of Slack's current web api methods are supported +using a syntax designed to match the endpoints themselves. + +If your bot has the appropriate scope, it may call [any of these method](https://api.slack.com/methods) using this syntax: + +```javascript +bot.api.channels.list({},function(err,response) { + //Do something... +}) +``` + + + +## Use the Slack Button + +The [Slack Button](https://api.slack.com/docs/slack-button) is a way to offer a Slack +integration as a service available to multiple teams. Botkit includes a framework +on top of which Slack Button applications can be built. + +Slack button applications can use one or more of the [real time API](http://api.slack.com/rtm), +[incoming webhook](http://api.slack.com/incoming-webhooks) and [slash command](http://api.slack.com/slash-commands) integrations, which can be +added *automatically* to a team using a special oauth scope. + +If special oauth scopes sounds scary, this is probably not for you! +The Slack Button is useful for developers who want to offer a service +to multiple teams. + +How many teams can a Slack button app built using Botkit handle? +This will largely be dependent on the environment it is hosted in and the +type of integrations used. A reasonably well equipped host server should +be able to easily handle _at least one hundred_ real time connections at once. + +To handle more than one hundred bots at once, [consider speaking to the +creators of Botkit at Howdy.ai](http://howdy.ai) + +For Slack button applications, Botkit provides: + +* A simple webserver +* OAuth Endpoints for login via Slack +* Storage of API tokens and team data via built-in Storage +* Events for when a team joins, a new integration is added, and others... + +See the [included examples](readme.md#included-examples) for several ready to use example apps. + +#### controller.configureSlackApp() + +| Argument | Description +|--- |--- +| config | configuration object containing clientId, clientSecret, redirectUri and scopes + +Configure Botkit to work with a Slack application. + +Get a clientId and clientSecret from [Slack's API site](https://api.slack.com/applications). +Configure Slash command, incoming webhook, or bot user integrations on this site as well. + +Configuration must include: + +* clientId - Application clientId from Slack +* clientSecret - Application clientSecret from Slack +* redirectUri - the base url of your application +* scopes - an array of oauth permission scopes + +Slack has [_many, many_ oauth scopes](https://api.slack.com/docs/oauth-scopes) +that can be combined in different ways. There are also [_special oauth scopes_ +used when requesting Slack Button integrations](https://api.slack.com/docs/slack-button). +It is important to understand which scopes your application will need to function, +as without the proper permission, your API calls will fail. + +#### controller.createOauthEndpoints() +| Argument | Description +|--- |--- +| webserver | an Express webserver Object +| error_callback | function to handle errors that may occur during oauth + +Call this function to create two web urls that handle login via Slack. +Once called, the resulting webserver will have two new routes: `http://_your_server_/login` and `http://_your_server_/oauth`. The second url will be used when configuring +the "Redirect URI" field of your application on Slack's API site. + + +```javascript +var Botkit = require('botkit'); +var controller = Botkit.slackbot(); + +controller.configureSlackApp({ + clientId: process.env.clientId, + clientSecret: process.env.clientSecret, + redirectUri: 'http://localhost:3002', + scopes: ['incoming-webhook','team:read','users:read','channels:read','im:read','im:write','groups:read','emoji:read','chat:write:bot'] +}); + +controller.setupWebserver(process.env.port,function(err,webserver) { + + // set up web endpoints for oauth, receiving webhooks, etc. + controller + .createHomepageEndpoint(controller.webserver) + .createOauthEndpoints(controller.webserver,function(err,req,res) { ... }) + .createWebhookEndpoints(controller.webserver); + +}); + +``` + +### How to identify what team your message came from +```javascript +bot.identifyTeam(function(err,team_id) { + +}) +``` + + +### How to identify the bot itself (for RTM only) +```javascript +bot.identifyBot(function(err,identity) { + // identity contains... + // {name, id, team_id} +}) +``` + + +### Slack Button specific events: + +| Event | Description +|--- |--- +| create_incoming_webhook | +| create_bot | +| update_team | +| create_team | +| create_user | +| update_user | +| oauth_error | diff --git a/readme-twilioipm.md b/readme-twilioipm.md new file mode 100644 index 000000000..e6a127738 --- /dev/null +++ b/readme-twilioipm.md @@ -0,0 +1,289 @@ +# Botkit and Twilio IP Messaging + +Botkit is designed to ease the process of designing and running useful, creative bots that live inside [Slack](http://slack.com), [Facebook Messenger](http://facebook.com), [Twilio IP Messaging](https://www.twilio.com/docs/api/ip-messaging), and other messaging platforms. + +Built in to [Botkit](https://howdy.ai/botkit/) are a comprehensive set of features and tools to deal with [Twilio IP Messaging platform](https://www.twilio.com/docs/api/ip-messaging), allowing +developers to build interactive bots and applications that send and receive messages just like real humans. + +This document covers the Twilio-IPM implementation details only. [Start here](readme.md) if you want to learn about to develop with Botkit. + +Table of Contents + +* [Getting Started](#getting-started) +* [Twilio IPM Events](#twilio-ipm-specific-events) +* [Working with Twilio IPM](#working-with-twilio-ip-messaging) +* [System Bots vs User Bots](#system-bots-vs-user-bots) +* [Using Twilio's API](#using-the-twilio-api) + +## Getting Started + +1) Install Botkit [more info here](readme.md#installation) + +2) Register a developer account with Twilio. Once you've got it, navigate your way to the [Get Started with IP Messaging](https://www.twilio.com/user/account/ip-messaging/getting-started) documentation on Twilio's site. Read up!! + +3) To get your bot running, you need to collect *5 different API credentials*. You will need to acquire your Twilio Account SID, Auth Token, Service SID, API Key, and your API Secret to integrate with your Botkit. This is a multi-step process! + +##### Twilio Account SID and Auth Token + +These values are available on your [Twilio Account page](https://www.twilio.com/user/account/settings). Copy both the SID and token values. + +##### API Key and API Secret + +To get an API key and secret [go here](https://www.twilio.com/user/account/ip-messaging/dev-tools/api-keys) and click 'Create an API Key'. Provide a friendly name for the API service and click 'Create API Key'. Be sure to copy your Twilio API key and API Secret keys to a safe location - this is the last time Twilio will show you your secret! Click the checkbox for 'Got it! I have saved my API Key Sid and Secret in a safe place to use in my application.' + +##### Service SID + +To generate a Twilio service SID, [go here](https://www.twilio.com/user/account/ip-messaging/services) and click 'Create an IP Messaging Service'. + +Provide a friendly name and click 'create'. At the top under 'Properties' you should see Service SID. Copy this to a safe place. You now have all 5 values! + +*Keep this tab open!* You'll come back here in step 7 to specify your bot's webhook endpoint URL. + +4) Now that you've got all the credentials, you need to set up an actual IP Messaging client. If you don't already have a native app built, the quickest way to get started is to clone the Twilio IPM client demo, which is available at [https://github.com/twilio/ip-messaging-demo-js](https://github.com/twilio/ip-messaging-demo-js) + +Follow the instructions to get your IP Messaging Demo client up and running using the credentials you collected above. + +5) Start up the sample Twilio IPM Bot. From inside your cloned Botkit repo, run: +``` +TWILIO_ACCOUNT_SID= TWILIO_AUTH_TOKEN= TWILIO_IPM_SERVICE_SID= TWILIO_API_KEY= TWILIO_API_SECRET= node twilio_ipm_bot.js +``` + +6) If you are _not_ running your bot at a public, SSL-enabled internet address, use [localtunnel.me](http://localtunnel.me) to make it available to Twilio. Note the URL it gives you. For example, it may say your url is `https://xyx.localtunnel.me/` In this case, the webhook URL for use in step 7 would be `https://xyx.localtunnel.me/twilio/receive` + +7) Set up a webhook endpoint for your app that uses your public URL, or the URL that localtunnel gave you. This is done on [settings page for your IP Messaging service](https://www.twilio.com/user/account/ip-messaging/services). Enable *all of the POST-event* webhooks events! + +6) Load your IP Messaging client, and talk to your bot! + +Try: + +* hello +* who am i? +* call me Bob +* shutdown + +### Things to note + +Since Twilio delivers messages via web hook, your application must be available at a public internet address. Additionally, Twilio requires this address to use SSL. Luckily, you can use [LocalTunnel](https://localtunnel.me/) to make a process running locally or in your dev environment available in a Twilio-friendly way. + +Additionally, you need to enable your Twilio IPM instance's webhook callback events. This can be done via the Twilio dashboard, but can also be done automatically using a Bash script. You can use the sample script below to enable all of the post-event webhook callbacks: + +``` +#!/bin/bash +echo 'please enter the service uri' +read servuri + +echo 'please enter the service sid' +read servsid + +echo 'please enter the account sid' +read accsid + +echo 'please enter the auth token' +read authtok + +onChannelDestroyedCurl="curl -X POST https://ip-messaging.twilio.com/v1/Services/$servsid -d 'Webhooks.OnChannelDestroyed.Url=$servuri/twilio/receive' -d 'Webhooks.OnChannelDestroyed.Method=POST' -d 'Webhooks.OnChannelDestroyed.Format=XML' -u '$accsid:$authtok'" +eval $onChannelDestroyedCurl + +onChannelAddedCurl="curl -X POST https://ip-messaging.twilio.com/v1/Services/$servsid -d 'Webhooks.OnChannelAdded.Url=$servuri/twilio/receive' -d 'Webhooks.OnChannelAdded.Method=POST' -d 'Webhooks.OnChannelAdded.Format=XML' -u '$accsid:$authtok'" +eval $onChannelAddedCurl + +onMemberRemovedCurl="curl -X POST https://ip-messaging.twilio.com/v1/Services/$servsid -d 'Webhooks.OnMemberRemoved.Url=$servuri/twilio/receive' -d 'Webhooks.OnMemberRemoved.Method=POST' -d 'Webhooks.OnMemberRemoved.Format=XML' -u '$accsid:$authtok'" +eval $onMemberRemovedCurl +onMessageRemovedCurl="curl -X POST https://ip-messaging.twilio.com/v1/Services/$servsid -d 'Webhooks.OnMessageRemoved.Url=$servuri/twilio/receive' -d 'Webhooks.OnMessageRemoved.Method=POST' -d 'Webhooks.OnMessageRemoved.Format=XML' -u '$accsid:$authtok'" +eval $onMessageRemovedCurl + +onMessageUpdatedCurl="curl -X POST https://ip-messaging.twilio.com/v1/Services/$servsid -d 'Webhooks.OnMessageUpdated.Url=$servuri/twilio/receive' -d 'Webhooks.OnMessageUpdated.Method=POST' -d 'Webhooks.OnMessageUpdated.Format=XML' -u '$accsid:$authtok'" +eval $onMessageUpdatedCurl + +onChannelUpdatedCurl="curl -X POST https://ip-messaging.twilio.com/v1/Services/$servsid -d 'Webhooks.OnChannelUpdated.Url=$servuri/twilio/receive' -d 'Webhooks.OnChannelUpdated.Method=POST' -d 'Webhooks.OnChannelUpdated.Format=XML' -u '$accsid:$authtok'" +eval $onChannelUpdatedCurl + +onMemberAddedCurl="curl -X POST https://ip-messaging.twilio.com/v1/Services/$servsid -d 'Webhooks.OnMemberAdded.Url=$servuri/twilio/receive' -d 'Webhooks.OnMemberAdded.Method=POST' -d 'Webhooks.OnMemberAdded.Format=XML' -u '$accsid:$authtok'" +eval $onMemberAddedCurl +``` + +When you are ready to go live, consider [LetsEncrypt.org](http://letsencrypt.org), a _free_ SSL Certificate Signing Authority which can be used to secure your website very quickly. It is fabulous and we love it. + +## Twilio IPM Specific Events + +Once connected to your Twilio IPM service, bots receive a constant stream of events. + +Normal messages will be sent to your bot using the `message_received` event. In addition, Botkit will trigger these Botkit-specific events: + +| Event | Description +|--- |--- +| bot_channel_join| The bot has joined a channel +| bot_channel_leave | The bot has left a channel +| user_channel_join | A user (not the bot) has joined a channel +| user_channel_leave | A user (not the bot) has left a channel + +Botkit will handle and distribute [all of the Twilio IPM API webhooks events](https://www.twilio.com/docs/api/ip-messaging/webhooks). Your Bot can act on any of these events, and will receive the complete payload from Twilio. Below, is a list of the IPM API callback events that can be subscribed to in your Bot: + +| Event | Description +|--- |--- +| onMessageSent | Message sent +| onMessageRemoved | Message removed/deleted +| onMessageUpdated | Message edited +| onChannelAdded | Channel created +| onChannelUpdated | Channel FriendlyName or Attributes updated +| onChannelDestroyed | Channel Deleted/Destroyed +| onMemberAdded | Channel Member Joined or Added +| onMemberRemoved | Channel Member Removed or Left + + +## Working with Twilio IP Messaging + +Botkit receives messages from Twilio IPM using Webhooks, and sends messages using Twilio's REST APIs. This means that your Bot application must present a web server that is publicly addressable. Everything you need to get started is already included in Botkit. + +To connect your bot to Twilio, [follow the instructions here](https://www.twilio.com/user/account/ip-messaging/getting-started). You will need to collect 5 separate pieces of your API credentials. A step by step guide [can be found here](#getting-started). Since you must *already be running* your Botkit app to fully configure your Twilio app, there is a bit of back-and-forth. It's ok! You can do it. + +Here is the complete code for a basic Twilio bot: + +```javascript +var Botkit = require('botkit'); +var controller = Botkit.twilioipmbot({ + debug: false +}) + +var bot = controller.spawn({ + TWILIO_IPM_SERVICE_SID: process.env.TWILIO_IPM_SERVICE_SID, + TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID, + TWILIO_API_KEY: process.env.TWILIO_API_KEY, + TWILIO_API_SECRET: process.env.TWILIO_API_SECRET, + identity: 'Botkit', + autojoin: true +}); + +// if you are already using Express, you can use your own server instance... +// see "Use BotKit with an Express web server" +controller.setupWebserver(process.env.port,function(err,webserver) { + controller.createWebhookEndpoints(controller.webserver, bot, function() { + console.log('This bot is online!!!'); + }); +}); + +// user said hello +controller.hears(['hello'], 'message_received', function(bot, message) { + + bot.reply(message, 'Hey there.'); + +}); + +controller.hears(['cookies'], 'message_received', function(bot, message) { + + bot.startConversation(message, function(err, convo) { + + convo.say('Did someone say cookies!?!!'); + convo.ask('What is your favorite type of cookie?', function(response, convo) { + convo.say('Golly, I love ' + response.text + ' too!!!'); + convo.next(); + }); + }); +}); +``` + + +#### controller.setupWebserver() +| Argument | Description +|--- |--- +| port | port for webserver +| callback | callback function + +Setup an [Express webserver](http://expressjs.com/en/index.html) for +use with `createWebhookEndpoints()` + +If you need more than a simple webserver to receive webhooks, +you should by all means create your own Express webserver! + +The callback function receives the Express object as a parameter, +which may be used to add further web server routes. + +#### controller.createWebhookEndpoints() + +This function configures the route `https://_your_server_/twilio/receive` +to receive webhooks from twilio. + +This url should be used when configuring Twilio. + +## System Bots vs User Bots + +Bots inside a Twilio IPM environment can run in one of two ways: as the "system" user, +ever present and automatically available in all channels, OR, as a specific "bot" user +who must be added to channels in order to interact. + +By default, bots are "system" users, and can be configured as below: + +``` +var bot = controller.spawn({ + TWILIO_IPM_SERVICE_SID: process.env.TWILIO_IPM_SERVICE_SID, + TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID, + TWILIO_API_KEY: process.env.TWILIO_API_KEY, + TWILIO_API_SECRET: process.env.TWILIO_API_SECRET, +}); +``` + +To connect as a "bot" user, pass in an `identity` field: + +``` +var bot = controller.spawn({ + TWILIO_IPM_SERVICE_SID: process.env.TWILIO_IPM_SERVICE_SID, + TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID, + TWILIO_API_KEY: process.env.TWILIO_API_KEY, + TWILIO_API_SECRET: process.env.TWILIO_API_SECRET, + identity: 'My Bot Name', +}); +``` + +To have your bot automatically join every channel as they are created and removed, +pass in `autojoin`: + +``` +var bot = controller.spawn({ + TWILIO_IPM_SERVICE_SID: process.env.TWILIO_IPM_SERVICE_SID, + TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID, + TWILIO_API_KEY: process.env.TWILIO_API_KEY, + TWILIO_API_SECRET: process.env.TWILIO_API_SECRET, + identity: 'Botkit', + autojoin: true +}); +``` + +## Using the Twilio API + +You can use the Twilio API directly in your Bot via Botkit's bot.api object. Botkit's bot.api provides a thin wrapper on the [Twilio official module](http://twilio.github.io/twilio-node/). + +For example, to [retrieve a member from a channel](https://www.twilio.com/docs/api/ip-messaging/rest/members#action-get) using the un-wrapped Twilio API client, you would use the following code: + +```javascript +service.channels('CHANNEL_SID').members('MEMBER_SID').get().then(function(response) { + console.log(response); +}).fail(function(error) { + console.log(error); +}); +``` + +In Botkit, this can be accomplished by simply replacing the reference to a `service` object, with the `bot.api` object, as shown here: + +```javascript +bot.api.channels('CHANNEL_SID').members('MEMBER_SID').get().then(function(response) { + console.log(response); +}).fail(function(error) { + console.log(error); +}); +``` +This gives you full access to all of the Twilio API methods so that you can use them in your Bot. + +Here is an example showing how to join a channel using Botkit's bot.api object, which creates a member to the channel, by wrapping the IPM API. + +```javascript +controller.on('onChannelAdded', function(bot, message){ + // whenever a channel gets added, join it! + bot.api.channels(message.channel).members.create({ + identity: bot.identity + }).then(function(response) { + + }).fail(function(error) { + console.log(error); + }); +}); +``` diff --git a/readme.md b/readme.md index 5e6aae2bf..a8bd72d42 100755 --- a/readme.md +++ b/readme.md @@ -1,22 +1,26 @@ # [Botkit](http://howdy.ai/botkit) - Building Blocks for Building Bots -Botkit designed to ease the process of designing and running useful, creative or just plain weird bots (and other types of applications) that live inside [Slack](http://slack.com)! +[![npm](https://img.shields.io/npm/v/botkit.svg)](https://www.npmjs.com/package/botkit) +[![David](https://img.shields.io/david/howdyai/botkit.svg)](https://david-dm.org/howdyai/botkit) +[![npm](https://img.shields.io/npm/l/botkit.svg)](https://spdx.org/licenses/MIT) -It provides a semantic interface to sending and receiving messages -so that developers can focus on creating novel applications and experiences -instead of dealing with API endpoints. +Botkit is designed to ease the process of designing and running useful, creative bots that live inside [Slack](http://slack.com), [Facebook Messenger](http://facebook.com), [Twilio IP Messaging](https://www.twilio.com/docs/api/ip-messaging), and other messaging platforms. -Botkit features a comprehensive set of tools -to deal with [Slack's integration platform](http://api.slack.com), and allows -developers to build both custom integrations for their -team, as well as public "Slack Button" applications that can be -run from a central location, and be used by many teams at the same time. +It provides a semantic interface to sending and receiving messages so that developers can focus on creating novel applications and experiences instead of dealing with API endpoints. + +Botkit features a comprehensive set of tools to deal with popular messaging platforms, including: + +* [Slack](readme-slack.md) +* [Facebook Messenger](readme-facebook.md) +* [Twilio IP Messaging](readme-twilioipm.md) +* Yours? [info@howdy.ai](mailto:info@howdy.ai) ## Installation Botkit is available via NPM. -```bash +``` +bash npm install --save botkit ``` @@ -40,55 +44,59 @@ npm install --production ## Getting Started -1) Install Botkit. See [Installation](#installation) instructions. +After you've installed Botkit, the first thing you'll need to do is register your bot with a messaging platform, and get a few configuration options set. This will allow your bot to connect, send and receive messages. -2) First make a bot integration inside of your Slack channel. Go here: +The fastest way to get a bot online and get to work is to start from one of the [examples included in the repo](#included-examples). -https://my.slack.com/services/new/bot +If you intend to create a bot that +lives in Slack, [follow these instructions for attaining a Bot Token](readme-slack.md#getting-started). -Enter a name for your bot. -Make it something fun and friendly, but avoid a single task specific name. -Bots can do lots! Let's not pigeonhole them. +If you intend to create a bot that lives in Facebook Messenger, [follow these instructions for configuring your Facebook page](readme-facebook.md#getting-started). -3) When you click "Add Bot Integration", you are taken to a page where you can add additional details about your bot, like an avatar, as well as customize its name & description. +If you intent to create a bot that lives inside a Twilio IP Messaging client, [follow these instructions for configuring your app](readme-twilioipm.md#getting-started). -Copy the API token that Slack gives you. You'll need it. +## Core Concepts -4) Run the example bot app, using the token you just copied: -​ -``` -token=REPLACE_THIS_WITH_YOUR_TOKEN node bot.js -``` -​ -5) Your bot should be online! Within Slack, send it a quick direct message to say hello. It should say hello back! +Bots built with Botkit have a few key capabilities, which can be used to create clever, conversational applications. These capabilities map to the way real human people talk to each other. -Try: - * who are you? - * call me Bob - * shutdown -​ +Bots can [hear things](#receiving-messages). Bots can [say things and reply](#sending-messages) to what they hear. -### Things to note -​ -Much like a vampire, a bot has to be invited into a channel. DO NOT WORRY bots are not vampires. +With these two building blocks, almost any type of conversation can be created. -Type: `/invite @` to invite your bot into another channel. +To organize the things a bot says and does into useful units, Botkit bots have a subsystem available for managing [multi-message conversations](#multi-message-replies-to-incoming-messages). Conversations add features like the ability to ask a question, queue several messages at once, and track when an interaction has ended. Handy! +After a bot has been told what to listen for and how to respond, +it is ready to be connected to a stream of incoming messages. Currently, Botkit supports receiving messages from a variety of sources: -## Core Concepts +* [Slack Real Time Messaging (RTM)](http://api.slack.com/rtm) +* [Slack Incoming Webhooks](http://api.slack.com/incoming-webhooks) +* [Slack Slash Commands](http://api.slack.com/slash-commands) +* [Facebook Messenger Webhooks](https://developers.facebook.com/docs/messenger-platform/implementation) +* [Twilio IP Messaging](https://www.twilio.com/user/account/ip-messaging/getting-started) -Bots built with Botkit have a few key capabilities, which can be used -to create clever, conversational applications. These capabilities -map to the way real human people talk to each other. +Read more about [connecting your bot to Slack](readme-slack.md#connecting-your-bot-to-slack), [connecting your bot to Facebook](readme-facebook.md#getting-started), or [connecting your bot to Twilio](readme-twilioipm.md#getting-started). -Bots can [hear things](#receiving-messages). Bots can [say things and reply](#sending-messages) to what they hear. +## Included Examples -With these two building blocks, almost any type of conversation can be created. +These examples are included in the Botkit [Github repo](https://github.com/howdyai/botkit). -To organize the things a bot says and does into useful units, Botkit bots have a subsystem available for managing [multi-message conversations](#multi-message-replies-to-incoming-messages). Conversations add features like the ability to ask a question, queue several messages at once, and track when an interaction has ended. Handy! +[slack_bot.js](https://github.com/howdyai/botkit/blob/master/slack_bot.js) An example bot that can be connected to your team. Useful as a basis for creating your first bot! -After a bot has been told what to listen for and how to respond, -it is ready to be connected to a stream of incoming messages. Currently, Botkit can handle [3 different types of incoming messages from Slack](#connecting-your-bot-to-slack). +[facebook_bot.js](https://github.com/howdyai/botkit/blob/master/facebook_bot.js) An example bot that can be connected to your Facebook page. Useful as a basis for creating your first bot! + +[twilio_ipm_bot.js](https://github.com/howdyai/botkit/blob/master/twilio_ipm_bot.js) An example bot that can be connected to your Twilio IP Messaging client. Useful as a basis for creating your first bot! + +[examples/demo_bot.js](https://github.com/howdyai/botkit/blob/master/examples/demo_bot.js) another example bot that uses different ways to send and receive messages. + +[examples/team_outgoingwebhook.js](https://github.com/howdyai/botkit/blob/master/examples/team_outgoingwebhook.js) an example of a Botkit app that receives and responds to outgoing webhooks from a single team. + +[examples/team_slashcommand.js](https://github.com/howdyai/botkit/blob/master/examples/team_slashcommand.js) an example of a Botkit app that receives slash commands from a single team. + +[examples/slackbutton_bot.js](https://github.com/howdyai/botkit/blob/master/examples/slackbutton_bot.js) an example of using the Slack Button to offer a bot integration. + +[examples/slackbutton_incomingwebhooks.js](https://github.com/howdyai/botkit/blob/master/examples/slackbutton_incomingwebhooks.js) an example of using the Slack Button to offer an incoming webhook integration. This example also includes a simple form which allows you to broadcast a message to any team who adds the integration. + +[example/sentiment_analysis.js](https://github.com/howdyai/botkit/blob/master/examples/sentiment_analysis.js) a simple example of a chatbot using sentiment analysis. Keeps a running score of each user based on positive and negative keywords. Messages and thresholds can be configured. ## Basic Usage @@ -107,7 +115,6 @@ a specific bot identity and connection to Slack. Once spawned and connected to the API, the bot user will appear online in Slack, and can then be used to send messages and conduct conversations with users. They are called into action by the `controller` when firing event handlers. - ```javascript var Botkit = require('botkit'); @@ -131,197 +138,65 @@ controller.hears('hello',['direct_message','direct_mention','mention'],function( ``` -## Included Examples - -These examples are included in the Botkit [Github repo](https://github.com/howdyai/botkit). - -[bot.js](https://github.com/howdyai/botkit/blob/master/bot.js) An example bot that can be connected to your team. Useful as a basis for creating your first bot! - -[examples/demo_bot.js](https://github.com/howdyai/botkit/blob/master/examples/demo_bot.js) another example bot that uses different ways to send and receive messages. - -[examples/slackbutton_bot.js](https://github.com/howdyai/botkit/blob/master/examples/slackbutton_bot.js) an example of using the Slack Button to offer a bot integration. - -[examples/slackbutton_incomingwebhooks.js](https://github.com/howdyai/botkit/blob/master/examples/slackbutton_incomingwebhooks.js) an example of using the Slack Button to offer an incoming webhook integration. This example also includes a simple form which allows you to broadcast a message to any team who adds the integration. - # Developing with Botkit Table of Contents -* [Connecting Your Bot To Slack](#connecting-your-bot-to-slack) * [Receiving Messages](#receiving-messages) * [Sending Messages](#sending-messages) -* [Working with Slack Integrations](#working-with-slack-integrations) +* [Middleware](#middleware) * [Advanced Topics](#advanced-topics) -## Connecting Your Bot to Slack - -Bot users connect to Slack using a real time API based on web sockets. -The bot connects to Slack using the same protocol that the native Slack clients use! - -To connect a bot to Slack, [get a Bot API token from the Slack integrations page](https://my.slack.com/services/new/bot). - -Note: Since API tokens can be used to connect to your team's Slack, it is best practices to handle API tokens with caution. For example, pass tokens in to your application via evironment variable or command line parameter rather than include it in the code itself. -This is particularly true if you store and use API tokens on behalf of users other than yourself! - -[Read Slack's Bot User documentation](https://api.slack.com/bot-users) - -#### controller.spawn() -| Argument | Description -|--- |--- -| config | Incoming message object - -Spawn an instance of your bot and connect it to Slack. -This function takes a configuration object which should contain -at least one method of talking to the Slack API. - -To use the real time / bot user API, pass in a token, preferably via -an environment variable. - -Controllers can also spawn bots that use [incoming webhooks](#incoming-webhooks). - -#### bot.startRTM() -| Argument | Description -|--- |--- -| callback | _Optional_ Callback in the form function(err,bot,payload) { ... } - -Opens a connection to Slack's real time API. This connection will remain -open until it fails or is closed using `closeRTM()`. - -The optional callback function receives: - -* Any error that occurred while connecting to Slack -* An updated bot object -* The resulting JSON payload of the Slack API command [rtm.start](https://api.slack.com/methods/rtm.start) - -The payload that this callback function receives contains a wealth of information -about the bot and its environment, including a complete list of the users -and channels visible to the bot. This information should be cached and used -when possible instead of calling Slack's API. - -A successful connection the API will also cause a `rtm_open` event to be -fired on the `controller` object. - - -#### bot.closeRTM() - -Close the connection to the RTM. Once closed, an `rtm_close` event is fired -on the `controller` object. - - -```javascript -var Botkit = require('Botkit'); - -var controller = Botkit.slackbot(); - -var bot = controller.spawn({ - token: my_slack_bot_token -}) - -bot.startRTM(function(err,bot,payload) { - if (err) { - throw new Error('Could not connect to Slack'); - } -}); -``` - ### Responding to events -Once connected to Slack, bots receive a constant stream of events - everything from the normal messages you would expect to typing notifications and presence change events. - -Botkit's message parsing and event system does a great deal of filtering on this -real time stream so developers do not need to parse every message. See [Receiving Messages](#receiving-messages) -for more information about listening for and responding to messages. +Once connected to a messaging platform, bots receive a constant stream of events - everything from the normal messages you would expect to typing notifications and presence change events. The set of events your bot will receive will depend on what messaging platform it is connected to. -It is also possible to bind event handlers directly to any of the enormous number of native Slack events, as well as a handful of custom events emitted by Botkit. - -You can receive and handle any of the [native events thrown by slack](https://api.slack.com/events). +All platforms will receive the `message_received` event. This event is the first event fired for every message of any type received - before any platform specific events are fired. ```javascript -controller.on('channel_joined',function(bot,message) { - - // message contains data sent by slack - // in this case: - // https://api.slack.com/events/channel_joined +controller.on('message_received', function(bot, message) { + // carefully examine and + // handle the message here! + // Note: Platforms such as Slack send many kinds of messages, not all of which contain a text field! }); ``` -You can also receive and handle a long list of additional events caused -by messages that contain a subtype field, [as listed here](https://api.slack.com/events/message) +Due to the multi-channel, multi-user nature of Slack, Botkit does additional filtering on the messages (after firing message_recieved), and will fire more specific events based on the type of message - for example, `direct_message` events indicate a message has been sent directly to the bot, while `direct_mention` indicates that the bot has been mentioned in a multi-user channel. +[List of Slack-specific Events](readme-slack.md#slack-specific-events) -```javascript -controller.on('channel_leave',function(bot,message) { - - // message format matches this: - // https://api.slack.com/events/message/channel_leave - -}) -``` - -Finally, Botkit throws a handful of its own events! -Events related to the general operation of bots are below. -When used in conjunction with the Slack Button, Botkit also fires -a [few additional events](#using-the-slack-button). +Twilio IPM bots can also exist in a multi-channel, multi-user environmnet. As a result, there are many additional events that will fire. In addition, Botkit will filter some messages, so that the bot will not receive it's own messages or messages outside of the channels in which it is present. +[List of Twilio IPM-specific Events](readme-twilioipm.md#twilio-ipm-specific-events) -#### Message/User Activity Events: - -| Event | Description -|--- |--- -| message_received | a message was received by the bot -| bot_channel_join | the bot has joined a channel -| user_channel_join | a user has joined a channel -| bot_group_join | the bot has joined a group -| user_group_join | a user has joined a group -| direct_message | the bot received a direct message from a user -| direct_mention | the bot was addressed directly in a channel -| mention | the bot was mentioned by someone in a message -| ambient | the message received had no mention of the bot - - -#### Websocket Events: - -| Event | Description -|--- |--- -| rtm_open | a connection has been made to the RTM api -| rtm_close | a connection to the RTM api has closed +Facebook messages are fairly straightforward. However, because Facebook supports inline buttons, there is an additional event fired when a user clicks a button. +[List of Facebook-specific Events](readme-facebook.md#facebook-specific-events) ## Receiving Messages -Botkit bots receive messages through a system of event handlers. Handlers can be set up to respond to specific types of messages, -or to messages that match a given keyword or pattern. +Botkit bots receive messages through a system of specialized event handlers. Handlers can be set up to respond to specific types of messages, or to messages that match a given keyword or pattern. -For Slack, Botkit supports five type of message event: - -| Event | Description -|--- |--- -| message_received | This event is fired for any message of any kind that is received and can be used as a catch all -| ambient | Ambient messages are messages that the bot can hear in a channel, but that do not mention the bot in any way -| direct_mention| Direct mentions are messages that begin with the bot's name, as in "@bot hello" -| mention | Mentions are messages that contain the bot's name, but not at the beginning, as in "hello @bot" -| direct_message | Direct messages are sent via private 1:1 direct message channels - -These message events can be handled using by attaching an event handler to the main controller object. +These message events can be handled by attaching an event handler to the main controller object. These event handlers take two parameters: the name of the event, and a callback function which is invoked whenever the event occurs. The callback function receives a bot object, which can be used to respond to the message, and a message object. ```javascript -// reply to @bot hello -controller.on('direct_mention',function(bot,message) { +// reply to any incoming message +controller.on('message_received', function(bot, message) { + bot.reply(message, 'I heard... something!'); +}); +// reply to a direct mention - @bot hello +controller.on('direct_mention',function(bot,message) { // reply to _message_ by using the _bot_ object bot.reply(message,'I heard you mention me!'); - }); // reply to a direct message controller.on('direct_message',function(bot,message) { - // reply to _message_ by using the _bot_ object bot.reply(message,'You are talking directly to me'); - }); - ``` ### Matching Patterns and Keywords with `hears()` @@ -335,22 +210,22 @@ specifies the keywords to match. |--- |--- | patterns | An _array_ or a _comma separated string_ containing a list of regular expressions to match | types | An _array_ or a _comma separated string_ of the message events in which to look for the patterns +| middleware function | _optional_ function to redefine how patterns are matched. see [Botkit Middleware](#middleware) | callback | callback function that receives a message object ```javascript -controller.hears(['keyword','^pattern$'],['direct_message','direct_mention','mention','ambient'],function(bot,message) { +controller.hears(['keyword','^pattern$'],['message_received'],function(bot,message) { // do something to respond to message - // all of the fields available in a normal Slack message object are available - // https://api.slack.com/events/message bot.reply(message,'You used a keyword!'); }); ``` -For example, + +When using the built in regular expression matching, the results of the expression will be stored in the `message.match` field and will match the expected output of normal Javascript `string.match(/pattern/i)`. For example: ```javascript -controller.hears('open the (.*) doors',['direct_message','mention'],function(bot,message) { +controller.hears('open the (.*) doors',['message_received'],function(bot,message) { var doorType = message.match[1]; //match[1] is the (.*) group. match[0] is the entire group (open the (.*) doors). if (doorType === 'pod bay') { return bot.reply(message, 'I\'m sorry, Dave. I\'m afraid I can\'t do that.'); @@ -367,12 +242,13 @@ on the type and number of messages that will be sent. Single message replies to incoming commands can be sent using the `bot.reply()` function. -Multi-message replies, particulary those that present questions for the end user to respond to, +Multi-message replies, particularly those that present questions for the end user to respond to, can be sent using the `bot.startConversation()` function and the related conversation sub-functions. Bots can originate messages - that is, send a message based on some internal logic or external stimulus - -using `bot.say()` method. Note that bots that do not need to respond to messages or hold conversations -may be better served by using Slack's [Incoming Webhooks](#incoming-webhooks) feature. +using `bot.say()` method. + +All `message` objects must contain a `text` property, even if it's only an empty string. ### Single Message Replies to Incoming Messages @@ -380,12 +256,17 @@ Once a bot has received a message using a `on()` or `hears()` event handler, a r can be sent using `bot.reply()`. Messages sent using `bot.reply()` are sent immediately. If multiple messages are sent via -`bot.reply()` in a single event handler, they will arrive in the Slack client very quickly +`bot.reply()` in a single event handler, they will arrive in the client very quickly and may be difficult for the user to process. We recommend using `bot.startConversation()` if more than one message needs to be sent. -You may pass either a string, or a message object to the function. Message objects may contain -any of the fields supported by [Slack's chat.postMessage](https://api.slack.com/methods/chat.postMessage) API. +You may pass either a string, or a message object to the function. + +Message objects may also contain any additional fields supported by the messaging platform in use: + +[Slack's chat.postMessage](https://api.slack.com/methods/chat.postMessage) API accepts several additional fields. These fields can be used to adjust the message appearance, add attachments, or even change the displayed user name. + +This is also true of Facebook. Calls to [Facebook's Send API](https://developers.facebook.com/docs/messenger-platform/send-api-reference) can include attachments which result in interactive "structured messages" which can include images, links and action buttons. #### bot.reply() @@ -395,8 +276,9 @@ any of the fields supported by [Slack's chat.postMessage](https://api.slack.com/ | reply | _String_ or _Object_ Outgoing response | callback | _Optional_ Callback in the form function(err,response) { ... } +Simple reply example: ```javascript -controller.hears(['keyword','^pattern$'],['direct_message','direct_mention','mention'],function(bot,message) { +controller.hears(['keyword','^pattern$'],['message_received'],function(bot,message) { // do something to respond to message // ... @@ -404,12 +286,16 @@ controller.hears(['keyword','^pattern$'],['direct_message','direct_mention','men bot.reply(message,"Tell me more!"); }); +``` +Slack-specific fields and attachments: +```javascript controller.on('ambient',function(bot,message) { // do something... // then respond with a message object + // bot.reply(message,{ text: "A more complex response", username: "ReplyBot", @@ -425,9 +311,9 @@ controller.hears('another_keyword','direct_message,direct_mention',function(bot, 'text': 'This is a pre-text', 'attachments': [ { - 'fallback': 'To be useful, I need your to invite me in a channel.', + 'fallback': 'To be useful, I need you to invite me in a channel.', 'title': 'How can I help you?', - 'text': 'To be useful, I need your to invite me in a channel ', + 'text': 'To be useful, I need you to invite me in a channel ', 'color': '#7CD197' } ], @@ -439,6 +325,69 @@ controller.hears('another_keyword','direct_message,direct_mention',function(bot, ``` + +Facebook-specific fields and attachments: +``` +// listen for the phrase `shirt` and reply back with structured messages +// containing images, links and action buttons +controller.hears(['shirt'],'message_received',function(bot, message) { + bot.reply(message, { + attachment: { + 'type':'template', + 'payload':{ + 'template_type':'generic', + 'elements':[ + { + 'title':'Classic White T-Shirt', + 'image_url':'http://petersapparel.parseapp.com/img/item100-thumb.png', + 'subtitle':'Soft white cotton t-shirt is back in style', + 'buttons':[ + { + 'type':'web_url', + 'url':'https://petersapparel.parseapp.com/view_item?item_id=100', + 'title':'View Item' + }, + { + 'type':'web_url', + 'url':'https://petersapparel.parseapp.com/buy_item?item_id=100', + 'title':'Buy Item' + }, + { + 'type':'postback', + 'title':'Bookmark Item', + 'payload':'USER_DEFINED_PAYLOAD_FOR_ITEM100' + } + ] + }, + { + 'title':'Classic Grey T-Shirt', + 'image_url':'http://petersapparel.parseapp.com/img/item101-thumb.png', + 'subtitle':'Soft gray cotton t-shirt is back in style', + 'buttons':[ + { + 'type':'web_url', + 'url':'https://petersapparel.parseapp.com/view_item?item_id=101', + 'title':'View Item' + }, + { + 'type':'web_url', + 'url':'https://petersapparel.parseapp.com/buy_item?item_id=101', + 'title':'Buy Item' + }, + { + 'type':'postback', + 'title':'Bookmark Item', + 'payload':'USER_DEFINED_PAYLOAD_FOR_ITEM101' + } + ] + } + ] + } + } + }); +}); +``` + ### Multi-message Replies to Incoming Messages For more complex commands, multiple messages may be necessary to send a response, @@ -453,7 +402,6 @@ multiple API calls into a single function. Messages sent as part of a conversation are sent no faster than one message per second, which roughly simulates the time it would take for the bot to "type" the message. -(It is possible to adjust this delay - see [special behaviors](#special-behaviors)) ### Start a Conversation @@ -470,22 +418,10 @@ Only the user who sent the original incoming message will be able to respond to #### bot.startPrivateConversation() | Argument | Description |--- |--- -| message | incoming message to which the conversation is in response +| message | message object containing {user: userId} of the user you would like to start a conversation with | callback | a callback function in the form of function(err,conversation) { ... } -`startPrivateConversation()` works juts like `startConversation()`, but the resulting -conversation that is created will occur in a private direct message channel between -the user and the bot. - -It is possible to initiate a private conversation by passing a message object, containing the user's Slack ID. - -```javascript -//assume var user_id has been defined -bot.startPrivateConversation({user: user_id}, function(response, convo){ - convo.say('Hello, I am your bot.') -}) -``` - +`startPrivateConversation()` is a function that initiates a conversation with a specific user. Note function is currently *Slack-only!* ### Control Conversation Flow @@ -497,7 +433,7 @@ bot.startPrivateConversation({user: user_id}, function(response, convo){ Call convo.say() several times in a row to queue messages inside the conversation. Only one message will be sent at a time, in the order they are queued. ```javascript -controller.hears(['hello world'],['direct_message','direct_mention','mention','ambient'],function(bot,message) { +controller.hears(['hello world'], 'message_received', function(bot,message) { // start a conversation to handle this response. bot.startConversation(message,function(err,convo) { @@ -505,25 +441,7 @@ controller.hears(['hello world'],['direct_message','direct_mention','mention','a convo.say('Hello!'); convo.say('Have a nice day!'); - //Using attachments - var message_with_attachments = { - 'username': 'My bot' , - 'text': 'this is a pre-text', - 'attachments': [ - { - 'fallback': 'To be useful, I need your to invite me in a channel.', - 'title': 'How can I help you?', - 'text': ' To be useful, I need your to invite me in a channel ', - 'color': '#7CD197' - } - ], - 'icon_url': 'http://lorempixel.com/48/48' - } - - convo.say(message_with_attachments); - }); - - }) + }); }); ``` @@ -561,7 +479,7 @@ This object can contain the following fields: ##### Using conversation.ask with a callback: ```javascript -controller.hears(['question me'],['direct_message','direct_mention','mention','ambient'],function(bot,message) { +controller.hears(['question me'], 'message_received', function(bot,message) { // start a conversation to handle this response. bot.startConversation(message,function(err,convo) { @@ -581,7 +499,7 @@ controller.hears(['question me'],['direct_message','direct_mention','mention','a ##### Using conversation.ask with an array of callbacks: ```javascript -controller.hears(['question me'],['direct_message','direct_mention','mention','ambient'],function(bot,message) { +controller.hears(['question me'], 'message_received', function(bot,message) { // start a conversation to handle this response. bot.startConversation(message,function(err,convo) { @@ -630,34 +548,34 @@ controller.hears(['question me'],['direct_message','direct_mention','mention','a ![multi-stage convo example](https://www.evernote.com/shard/s321/sh/7243cadf-be40-49cf-bfa2-b0f524176a65/f9257e2ff5ee6869/res/bc778282-64a5-429c-9f45-ea318c729225/screenshot.png?resizeSmall&width=832) -The recommended way to have multi-stage conversations is with multiple functions -which call eachother. Each function asks just one question. Example: +One way to have multi-stage conversations is with multiple functions +which call each other. Each function asks just one question. Example: ```javascript -controller.hears(['pizzatime'],['ambient'],function(bot,message) { - bot.startConversation(message, askFlavor); -}); +controller.hears(['pizzatime'], 'message_recieved', function(bot,message) { + askFlavor = function(response, convo) { + convo.ask('What flavor of pizza do you want?', function(response, convo) { + convo.say('Awesome.'); + askSize(response, convo); + convo.next(); + }); + } + askSize = function(response, convo) { + convo.ask('What size do you want?', function(response, convo) { + convo.say('Ok.') + askWhereDeliver(response, convo); + convo.next(); + }); + } + askWhereDeliver = function(response, convo) { + convo.ask('So where do you want it delivered?', function(response, convo) { + convo.say('Ok! Good bye.'); + convo.next(); + }); + } -askFlavor = function(response, convo) { - convo.ask('What flavor of pizza do you want?', function(response, convo) { - convo.say('Awesome.'); - askSize(response, convo); - convo.next(); - }); -} -askSize = function(response, convo) { - convo.ask('What size do you want?', function(response, convo) { - convo.say('Ok.') - askWhereDeliver(response, convo); - convo.next(); - }); -} -askWhereDeliver = function(response, convo) { - convo.ask('So where do you want it delivered?', function(response, convo) { - convo.say('Ok! Good by.'); - convo.next(); - }); -} + bot.startConversation(message, askFlavor); +}); ``` The full code for this example can be found in ```examples/convo_bot.js```. @@ -739,241 +657,131 @@ var value = convo.extractResponse('key'); | message | A message object | callback | _Optional_ Callback in the form function(err,response) { ... } -Note: If your primary need is to spontaneously send messages rather than -respond to incoming messages, you may want to use [Slack's incoming webhooks feature](#incoming-webhooks) rather than the real time API. - +Slack-specific Example: ```javascript bot.say( { text: 'my message text', - channel: 'C0H338YH4' + channel: 'C0H338YH4' // a valid slack channel, group, mpim, or im ID } ); ``` - -## Working with Slack Integrations - -There are a dizzying number of ways to integrate your application into Slack. -Up to this point, this document has mainly dealt with the real time / bot user -integration. In addition to this type of integration, Botkit also supports: - -* Incoming Webhooks - a way to send (but not receive) messages to Slack -* Outgoing Webhooks - a way to receive messages from Slack based on a keyword or phrase -* Slash Command - a way to add /slash commands to Slack -* Slack Web API - a full set of RESTful API tools to deal with Slack -* The Slack Button - a way to build Slack applications that can be used by multiple teams +Note: If your primary need is to spontaneously send messages rather than respond to incoming messages, you may want to use [Slack's incoming webhooks feature](readme-slack.md#incoming-webhooks) rather than the real time API. +Facebook-specific Example: ```javascript -var Botkit = require('botkit'); -var controller = Botkit.slackbot({}) - -var bot = controller.spawn({ - token: my_slack_bot_token -}); - -// use RTM -bot.startRTM(function(err,bot,payload) { - // handle errors... -}); - -// send webhooks -bot.configureIncomingWebhook({url: webhook_url}); -bot.sendWebhook({ - text: 'Hey!', - channel: '#testing', -},function(err,res) { - // handle error -}); - -// receive outgoing or slash commands -// if you are already using Express, you can use your own server instance... -// see "Use BotKit with an Express web server" -controller.setupWebserver(process.env.port,function(err,webserver) { - - controller.createWebhookEndpoints(controller.webserver); - -}); - -controller.on('slash_command',function(bot,message) { - - // reply to slash command - bot.replyPublic(message,'Everyone can see the results of this slash command'); - -}); +bot.say( + { + text: 'my message_text', + channel: '+1(###)###-####' // a valid facebook user id or phone number + } +); ``` +## Middleware +The functionality of Botkit can be extended using middleware +functions. These functions can plugin to the core bot running processes at +several useful places and make changes to both a bot's configuration and +the incoming or outgoing message. -### Incoming webhooks +### Middleware Endpoints -Incoming webhooks allow you to send data from your application into Slack. -To configure Botkit to send an incoming webhook, first set one up -via [Slack's integration page](https://my.slack.com/services/new/incoming-webhook/). +Botkit currently supports middleware insertion in three places: -Once configured, use the `sendWebhook` function to send messages to Slack. +* When receiving a message, before triggering any events +* When sending a message, before the message is sent to the API +* When hearing a message -[Read official docs](https://api.slack.com/incoming-webhooks) +Send and Receive middleware functions are added to Botkit using an Express-style "use" syntax. +Each function receives a bot parameter, a message parameter, and +a next function which must be called to continue processing the middleware stack. -#### bot.configureIncomingWebhook() -| Argument | Description -|--- |--- -| config | Configure a bot to send webhooks +Hear middleware functions are passed in to the `controller.hears` function, +and override the built in regular expression matching. -Add a webhook configuration to an already spawned bot. -It is preferable to spawn the bot pre-configured, but hey, sometimes -you need to do it later. +### Receive Middleware -#### bot.sendWebhook() -| Argument | Description -|--- |--- -| message | A message object -| callback | _Optional_ Callback in the form function(err,response) { ... } +Receive middleware can be used to do things like preprocess the message +content using external natural language processing services like Wit.ai. +Additional information can be added to the message object for use down the chain. -Pass `sendWebhook` an object that contains at least a `text` field. - This object may also contain other fields defined [by Slack](https://api.slack.com/incoming-webhooks) which can alter the - appearance of your message. +``` +controller.middleware.receive.use(function(bot, message, next) { -```javascript -var bot = controller.spawn({ - incoming_webhook: { - url: - } -}) + // do something... + // message.extrainfo = 'foo'; + next(); -bot.sendWebhook({ - text: 'This is an incoming webhook', - channel: '#general', -},function(err,res) { - if (err) { - // ... - } }); ``` -### Outgoing Webhooks and Slash commands - -Outgoing webhooks and Slash commands allow you to send data out of Slack. +### Send Middleware -Outgoing webhooks are used to match keywords or phrases in Slack. [Read Slack's official documentation here.](https://api.slack.com/outgoing-webhooks) +Send middleware can be used to do things like preprocess the message +content before it gets sent out to the messaging client. -Slash commands are special commands triggered by typing a "/" then a command. -[Read Slack's official documentation here.](https://api.slack.com/slash-commands) - -Though these integrations are subtly different, Botkit normalizes the details -so developers may focus on providing useful functionality rather than peculiarities -of the Slack API parameter names. - -Note that since these integrations use send webhooks from Slack to your application, -your application will have to be hosted at a public IP address or domain name, -and properly configured within Slack. - -[Set up an outgoing webhook](https://xoxco.slack.com/services/new/outgoing-webhook) +``` +controller.middleware.send.use(function(bot, message, next) { -[Set up a Slash command](https://xoxco.slack.com/services/new/slash-commands) + // do something useful... + if (message.intent == 'hi') { + message.text = 'Hello!!!'; + } + next(); -```javascript -controller.setupWebserver(port,function(err,express_webserver) { - controller.createWebhookEndpoints(express_webserver) }); - -controller.on('slash_command',function(bot,message) { - - // reply to slash command - bot.replyPublic(message,'Everyone can see this part of the slash command'); - bot.replyPrivate(message,'Only the person who used the slash command can see this.'); - -}) - -controller.on('outgoing_webhook',function(bot,message) { - - // reply to outgoing webhook command - bot.replyPublic(message,'Everyone can see the results of this webhook command'); - -}) ``` -#### controller.setupWebserver() -| Argument | Description -|--- |--- -| port | port for webserver -| callback | callback function - -Setup an [Express webserver](http://expressjs.com/en/index.html) for -use with `createwWebhookEndpoints()` - -If you need more than a simple webserver to receive webhooks, -you should by all means create your own Express webserver! - -The callback function receives the Express object as a parameter, -which may be used to add further web server routes. - -#### controller.createWebhookEndpoints() - -This function configures the route `http://_your_server_/slack/receive` -to receive webhooks from Slack. - -This url should be used when configuring Slack. - -When a slash command is received from Slack, Botkit fires the `slash_command` event. - -When an outgoing webhook is recieved from Slack, Botkit fires the `outgoing_webhook` event. - - -#### bot.replyPublic() -| Argument | Description -|--- |--- -| src | source message as received from slash or webhook -| reply | reply message (string or object) -| callback | optional callback - -When used with outgoing webhooks, this function sends an immediate response that is visible to everyone in the channel. -When used with slash commands, this function has the same functionality. However, -slash commands also support private, and delayed messages. See below. -[View Slack's docs here](https://api.slack.com/slash-commands) +### Hear Middleware -#### bot.replyPrivate() +Hear middleware can be used to change the way Botkit bots "hear" triggers. +It can be used to look for values in fields other than message.text, or use comparison methods other than regular expression matching. For example, a middleware function +could enable Botkit to "hear" intents added by an NLP classifier instead of string patterns. -| Argument | Description -|--- |--- -| src | source message as received from slash -| reply | reply message (string or object) -| callback | optional callback +Hear middleware is enabled by passing a function into the `hears()` method on the Botkit controller. +When specified, the middleware function will be used instead of the built in regular expression match. +These functions receive 2 parameters - `patterns` an array of patterns, and `message` the incoming +message. This function will be called _after_ any receive middlewares, so may use any additional +information that may have been added. A return value of `true` indicates the pattern has been +matched and the bot should respond. -#### bot.replyPublicDelayed() - -| Argument | Description -|--- |--- -| src | source message as received from slash -| reply | reply message (string or object) -| callback | optional callback - -#### bot.replyPrivateDelayed() +``` +// this example does a simple string match instead of using regular expressions +function custom_hear_middleware(patterns, message) { -| Argument | Description -|--- |--- -| src | source message as received from slash -| reply | reply message (string or object) -| callback | optional callback + for (var p = 0; p < patterns.length; p++) { + if (patterns[p] == message.text) { + return true; + } + } + return false; +} +controller.hears(['hello'],'direct_message',custom_hear_middleware,function(bot, message) { -### Using the Slack Web API + bot.reply(message, 'I heard the EXACT string match for "hello"'); -All (or nearly all - they change constantly!) of Slack's current web api methods are supported -using a syntax designed to match the endpoints themselves. +}); +``` -If your bot has the appropriate scope, it may call [any of these method](https://api.slack.com/methods) using this syntax: +It is possible to completely replace the built in regular expression match with +a middleware function by calling `controller.changeEars()`. This will replace the matching function used in `hears()` +as well as inside `convo.ask().` This would, for example, enable your bot to +hear only intents instead of strings. -```javascript -bot.api.channels.list({},function(err,response) { - //Do something... -}) ``` +controller.changeEars(function(patterns, message) { + // ... do something + // return true or false +}); +``` # Advanced Topics @@ -1050,123 +858,6 @@ var controller = Botkit.slackbot({ }); ``` -## Use the Slack Button - -The [Slack Button](https://api.slack.com/docs/slack-button) is a way to offer a Slack -integration as a service available to multiple teams. Botkit includes a framework -on top of which Slack Button applications can be built. - -Slack button applications can use one or more of the [real time API](), -[incoming webhook]() and [slash command]() integrations, which can be -added *automatically* to a team using a special oauth scope. - -If special oauth scopes sounds scary, this is probably not for you! -The Slack Button is useful for developers who want to offer a service -to multiple teams. - -How many teams can a Slack button app built using Botkit handle? -This will largely be dependent on the environment it is hosted in and the -type of integrations used. A reasonably well equipped host server should -be able to easily handle _at least one hundred_ real time connections at once. - -To handle more than one hundred bots at once, [consider speaking to the -creators of Botkit at Howdy.ai](http://howdy.ai) - -For Slack button applications, Botkit provides: - -* A simple webserver -* OAuth Endpoints for login via Slack -* Storage of API tokens and team data via built-in Storage -* Events for when a team joins, a new integration is added, and others... - -See the [included examples](#included-examples) for several ready to use example apps. - -#### controller.configureSlackApp() - -| Argument | Description -|--- |--- -| config | configuration object containing clientId, clientSecret, redirectUri and scopes - -Configure Botkit to work with a Slack application. - -Get a clientId and clientSecret from [Slack's API site](https://api.slack.com/applications). -Configure Slash command, incoming webhook, or bot user integrations on this site as well. - -Configuration must include: - -* clientId - Application clientId from Slack -* clientSecret - Application clientSecret from Slack -* redirectUri - the base url of your application -* scopes - an array of oauth permission scopes - -Slack has [_many, many_ oauth scopes](https://api.slack.com/docs/oauth-scopes) -that can be combined in different ways. There are also [_special oauth scopes_ -used when requesting Slack Button integrations](https://api.slack.com/docs/slack-button). -It is important to understand which scopes your application will need to function, -as without the proper permission, your API calls will fail. - -#### controller.createOauthEndpoints() -| Argument | Description -|--- |--- -| webserver | an Express webserver Object -| error_callback | function to handle errors that may occur during oauth - -Call this function to create two web urls that handle login via Slack. -Once called, the resulting webserver will have two new routes: `http://_your_server_/login` and `http://_your_server_/oauth`. The second url will be used when configuring -the "Redirect URI" field of your application on Slack's API site. - - -```javascript -var Botkit = require('botkit'); -var controller = Botkit.slackbot(); - -controller.configureSlackApp({ - clientId: process.env.clientId, - clientSecret: process.env.clientSecret, - redirectUri: 'http://localhost:3002', - scopes: ['incoming-webhook','team:read','users:read','channels:read','im:read','im:write','groups:read','emoji:read','chat:write:bot'] -}); - -controller.setupWebserver(process.env.port,function(err,webserver) { - - // set up web endpoints for oauth, receiving webhooks, etc. - controller - .createHomepageEndpoint(controller.webserver) - .createOauthEndpoints(controller.webserver,function(err,req,res) { ... }) - .createWebhookEndpoints(controller.webserver); - -}); - -``` - -### How to identify what team your message came from -```javascript -bot.identifyTeam(function(err,team_id) { - -}) -``` - - -### How to identify the bot itself (for RTM only) -```javascript -bot.identifyBot(function(err,identity) { - // identity contains... - // {name, id, team_id} -}) -``` - - -### Slack Button specific events: - -| Event | Description -|--- |--- -| create_incoming_webhook | -| create_bot | -| update_team | -| create_team | -| create_user | -| update_user | -| oauth_error | ##Use BotKit with an Express web server Instead of controller.setupWebserver(), it is possible to use a different web server to manage authentication flows, as well as serving web pages. diff --git a/slack_bot.js b/slack_bot.js new file mode 100644 index 000000000..d71da52d2 --- /dev/null +++ b/slack_bot.js @@ -0,0 +1,246 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ______ ______ ______ __ __ __ ______ + /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ + \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ + \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ + \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ + + +This is a sample Slack bot built with Botkit. + +This bot demonstrates many of the core features of Botkit: + +* Connect to Slack using the real time API +* Receive messages based on "spoken" patterns +* Reply to messages +* Use the conversation system to ask questions +* Use the built in storage system to store and retrieve information + for a user. + +# RUN THE BOT: + + Get a Bot token from Slack: + + -> http://my.slack.com/services/new/bot + + Run your bot from the command line: + + token= node slack_bot.js + +# USE THE BOT: + + Find your bot inside Slack to send it a direct message. + + Say: "Hello" + + The bot will reply "Hello!" + + Say: "who are you?" + + The bot will tell you its name, where it running, and for how long. + + Say: "Call me " + + Tell the bot your nickname. Now you are friends. + + Say: "who am I?" + + The bot will tell you your nickname, if it knows one for you. + + Say: "shutdown" + + The bot will ask if you are sure, and then shut itself down. + + Make sure to invite your bot into other channels using /invite @! + +# EXTEND THE BOT: + + Botkit has many features for building cool and useful bots! + + Read all about it here: + + -> http://howdy.ai/botkit + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ + + +if (!process.env.token) { + console.log('Error: Specify token in environment'); + process.exit(1); +} + +var Botkit = require('./lib/Botkit.js'); +var os = require('os'); + +var controller = Botkit.slackbot({ + debug: true, +}); + +var bot = controller.spawn({ + token: process.env.token +}).startRTM(); + + +controller.hears(['hello', 'hi'], 'direct_message,direct_mention,mention', function(bot, message) { + + bot.api.reactions.add({ + timestamp: message.ts, + channel: message.channel, + name: 'robot_face', + }, function(err, res) { + if (err) { + bot.botkit.log('Failed to add emoji reaction :(', err); + } + }); + + + controller.storage.users.get(message.user, function(err, user) { + if (user && user.name) { + bot.reply(message, 'Hello ' + user.name + '!!'); + } else { + bot.reply(message, 'Hello.'); + } + }); +}); + +controller.hears(['call me (.*)', 'my name is (.*)'], 'direct_message,direct_mention,mention', function(bot, message) { + var name = message.match[1]; + controller.storage.users.get(message.user, function(err, user) { + if (!user) { + user = { + id: message.user, + }; + } + user.name = name; + controller.storage.users.save(user, function(err, id) { + bot.reply(message, 'Got it. I will call you ' + user.name + ' from now on.'); + }); + }); +}); + +controller.hears(['what is my name', 'who am i'], 'direct_message,direct_mention,mention', function(bot, message) { + + controller.storage.users.get(message.user, function(err, user) { + if (user && user.name) { + bot.reply(message, 'Your name is ' + user.name); + } else { + bot.startConversation(message, function(err, convo) { + if (!err) { + convo.say('I do not know your name yet!'); + convo.ask('What should I call you?', function(response, convo) { + convo.ask('You want me to call you `' + response.text + '`?', [ + { + pattern: 'yes', + callback: function(response, convo) { + // since no further messages are queued after this, + // the conversation will end naturally with status == 'completed' + convo.next(); + } + }, + { + pattern: 'no', + callback: function(response, convo) { + // stop the conversation. this will cause it to end with status == 'stopped' + convo.stop(); + } + }, + { + default: true, + callback: function(response, convo) { + convo.repeat(); + convo.next(); + } + } + ]); + + convo.next(); + + }, {'key': 'nickname'}); // store the results in a field called nickname + + convo.on('end', function(convo) { + if (convo.status == 'completed') { + bot.reply(message, 'OK! I will update my dossier...'); + + controller.storage.users.get(message.user, function(err, user) { + if (!user) { + user = { + id: message.user, + }; + } + user.name = convo.extractResponse('nickname'); + controller.storage.users.save(user, function(err, id) { + bot.reply(message, 'Got it. I will call you ' + user.name + ' from now on.'); + }); + }); + + + + } else { + // this happens if the conversation ended prematurely for some reason + bot.reply(message, 'OK, nevermind!'); + } + }); + } + }); + } + }); +}); + + +controller.hears(['shutdown'], 'direct_message,direct_mention,mention', function(bot, message) { + + bot.startConversation(message, function(err, convo) { + + convo.ask('Are you sure you want me to shutdown?', [ + { + pattern: bot.utterances.yes, + callback: function(response, convo) { + convo.say('Bye!'); + convo.next(); + setTimeout(function() { + process.exit(); + }, 3000); + } + }, + { + pattern: bot.utterances.no, + default: true, + callback: function(response, convo) { + convo.say('*Phew!*'); + convo.next(); + } + } + ]); + }); +}); + + +controller.hears(['uptime', 'identify yourself', 'who are you', 'what is your name'], + 'direct_message,direct_mention,mention', function(bot, message) { + + var hostname = os.hostname(); + var uptime = formatUptime(process.uptime()); + + bot.reply(message, + ':robot_face: I am a bot named <@' + bot.identity.name + + '>. I have been running for ' + uptime + ' on ' + hostname + '.'); + + }); + +function formatUptime(uptime) { + var unit = 'second'; + if (uptime > 60) { + uptime = uptime / 60; + unit = 'minute'; + } + if (uptime > 60) { + uptime = uptime / 60; + unit = 'hour'; + } + if (uptime != 1) { + unit = unit + 's'; + } + + uptime = uptime + ' ' + unit; + return uptime; +} diff --git a/twilio_ipm_bot.js b/twilio_ipm_bot.js new file mode 100644 index 000000000..b8636f18a --- /dev/null +++ b/twilio_ipm_bot.js @@ -0,0 +1,130 @@ +var Botkit = require('./lib/Botkit.js'); +var os = require('os'); +var controller = Botkit.twilioipmbot({ + debug: false, +}); + +var bot = controller.spawn({ + TWILIO_IPM_SERVICE_SID: process.env.TWILIO_IPM_SERVICE_SID, + TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID, + TWILIO_API_KEY: process.env.TWILIO_API_KEY, + TWILIO_API_SECRET: process.env.TWILIO_API_SECRET, + identity: 'Botkit', + autojoin: true +}); + +controller.setupWebserver(process.env.port || 3000, function(err, server) { + + server.get('/', function(req, res) { + res.send(':)'); + }); + + controller.createWebhookEndpoints(server, bot); + +}); + +controller.on('bot_channel_join', function(bot, message) { + bot.reply(message, 'Here I am!'); +}); + +controller.on('user_channel_join', function(bot,message) { + bot.reply(message, 'Welcome, ' + message.user + '!'); +}); + +controller.on('user_channel_leave', function(bot,message) { + bot.reply(message, 'Bye, ' + message.user + '!'); +}); + + +controller.hears(['hello', 'hi'], 'message_received', function(bot, message) { + + controller.storage.users.get(message.user, function(err, user) { + if (user && user.name) { + bot.reply(message, 'Hello ' + user.name + '!!'); + } else { + bot.reply(message, 'Hello.'); + } + }); +}); + +controller.hears(['call me (.*)'], 'message_received', function(bot, message) { + var matches = message.text.match(/call me (.*)/i); + var name = matches[1]; + controller.storage.users.get(message.user, function(err, user) { + if (!user) { + user = { + id: message.user, + }; + } + user.name = name; + controller.storage.users.save(user, function(err, id) { + bot.reply(message, 'Got it. I will call you ' + user.name + ' from now on.'); + }); + }); +}); + +controller.hears(['what is my name', 'who am i'], 'message_received', function(bot, message) { + + controller.storage.users.get(message.user, function(err, user) { + if (user && user.name) { + bot.reply(message,'Your name is ' + user.name); + } else { + bot.reply(message,'I don\'t know yet!'); + } + }); +}); + + +controller.hears(['shutdown'],'message_received',function(bot, message) { + + bot.startConversation(message,function(err, convo) { + convo.ask('Are you sure you want me to shutdown?',[ + { + pattern: bot.utterances.yes, + callback: function(response, convo) { + convo.say('Bye!'); + convo.next(); + setTimeout(function() { + process.exit(); + },3000); + } + }, + { + pattern: bot.utterances.no, + default: true, + callback: function(response, convo) { + convo.say('*Phew!*'); + convo.next(); + } + } + ]); + }); +}); + + +controller.hears(['uptime','identify yourself','who are you','what is your name'],'message_received',function(bot, message) { + + var hostname = os.hostname(); + var uptime = formatUptime(process.uptime()); + + bot.reply(message,'I am a bot! I have been running for ' + uptime + ' on ' + hostname + '.'); + +}); + +function formatUptime(uptime) { + var unit = 'second'; + if (uptime > 60) { + uptime = uptime / 60; + unit = 'minute'; + } + if (uptime > 60) { + uptime = uptime / 60; + unit = 'hour'; + } + if (uptime != 1) { + unit = unit + 's'; + } + + uptime = uptime + ' ' + unit; + return uptime; +}