├── examples ├── slack_app.js ├── incoming_webhooks.js ├── team_outgoingwebhook.js ├── team_slashcommand.js ├── slackbutton_slashcommand.js ├── convo_bot.js ├── demo_bot.js ├── slackbutton_incomingwebhooks.js ├── slackbutton_bot.js ├── sentiment_analysis.js └── middleware_example.js ├── .editorconfig ├── .gitignore ├── lib ├── Botkit.js ├── console_logger.js ├── middleware │ └── slack_authentication.js ├── storage │ ├── simple_storage.js │ └── storage_test.js ├── Facebook.js ├── Slack_web_api.js ├── TwilioIPMBot.js ├── Slackbot_worker.js └── SlackBot.js ├── .jscsrc ├── LICENSE.md ├── package.json ├── CONTRIBUTING.md ├── changelog.md ├── tests └── Slack_web_api.js ├── twilio_ipm_bot.js ├── slack_bot.js ├── readme-facebook.md ├── facebook_bot.js ├── readme-twilioipm.md └── readme-slack.md /examples/slack_app.js: -------------------------------------------------------------------------------- 1 | /* TODO bot that demonstrates using slack on behalf of a user */ 2 | -------------------------------------------------------------------------------- /examples/incoming_webhooks.js: -------------------------------------------------------------------------------- 1 | /* TODO bot that demonstrates sending incmoing webhooks to one specific team */ 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [*.json] 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | start.sh 3 | start_button.sh 4 | db/ 5 | examples/db_slackbutton_bot/ 6 | examples/db_slackbutton_incomingwebhook/ 7 | examples/db_slackbutton_slashcommand/ 8 | examples/db_team_bot/ 9 | .DS_Store 10 | */.DS_Store 11 | .env 12 | -------------------------------------------------------------------------------- /lib/Botkit.js: -------------------------------------------------------------------------------- 1 | var CoreBot = require(__dirname + '/CoreBot.js'); 2 | var Slackbot = require(__dirname + '/SlackBot.js'); 3 | var Facebookbot = require(__dirname + '/Facebook.js'); 4 | var TwilioIPMbot = require(__dirname + '/TwilioIPMBot.js'); 5 | 6 | module.exports = { 7 | core: CoreBot, 8 | slackbot: Slackbot, 9 | facebookbot: Facebookbot, 10 | twilioipmbot: TwilioIPMbot, 11 | }; 12 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "disallowKeywords": ["with"], 4 | "disallowMultipleLineBreaks": null, 5 | "disallowMultipleVarDecl": null, 6 | "maximumLineLength": 120, 7 | "disallowSpacesInsideObjectBrackets": null, 8 | "requireCamelCaseOrUpperCaseIdentifiers": null, 9 | "requireCurlyBraces": null, 10 | "validateIndentation": 4, 11 | "requireSpaceAfterComma": true 12 | } 13 | -------------------------------------------------------------------------------- /examples/team_outgoingwebhook.js: -------------------------------------------------------------------------------- 1 | var Botkit = require('../lib/Botkit.js'); 2 | 3 | var controller = Botkit.slackbot({ 4 | debug: true 5 | }); 6 | 7 | 8 | controller.setupWebserver(3000, function(err, webserver) { 9 | controller.createWebhookEndpoints(webserver); 10 | }); 11 | 12 | controller.on('outgoing_webhook', function(bot, message) { 13 | 14 | bot.replyPublic(message, 'This is a public reply to the outgoing webhook!'); 15 | 16 | }); 17 | -------------------------------------------------------------------------------- /examples/team_slashcommand.js: -------------------------------------------------------------------------------- 1 | var Botkit = require('../lib/Botkit.js'); 2 | 3 | var controller = Botkit.slackbot({ 4 | debug: true 5 | }); 6 | 7 | 8 | controller.setupWebserver(3000, function(err, webserver) { 9 | controller.createWebhookEndpoints(webserver); 10 | }); 11 | 12 | controller.on('slash_command', function(bot, message) { 13 | // check message.command 14 | // and maybe message.text... 15 | // use EITHER replyPrivate or replyPublic... 16 | bot.replyPrivate(message, 'This is a private reply to the ' + message.command + ' slash command!'); 17 | 18 | // and then continue to use replyPublicDelayed or replyPrivateDelayed 19 | bot.replyPublicDelayed(message, 'This is a public reply to the ' + message.command + ' slash command!'); 20 | 21 | bot.replyPrivateDelayed(message, ':dash:'); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright XOXCO, Inc, http://xoxco.com, http://howdy.ai 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botkit", 3 | "version": "0.2.0", 4 | "description": "Building blocks for Building Bots", 5 | "main": "lib/Botkit.js", 6 | "dependencies": { 7 | "async": "^2.0.0-rc.5", 8 | "back": "^1.0.1", 9 | "body-parser": "^1.14.2", 10 | "command-line-args": "^2.1.6", 11 | "express": "^4.13.3", 12 | "https-proxy-agent": "^1.0.0", 13 | "jfs": "^0.2.6", 14 | "localtunnel": "^1.8.1", 15 | "mustache": "^2.2.1", 16 | "request": "^2.67.0", 17 | "twilio": "^2.9.1", 18 | "ware": "^1.3.0", 19 | "ws": "^1.0.1" 20 | }, 21 | "devDependencies": { 22 | "jscs": "^2.7.0", 23 | "mocha": "^2.4.5", 24 | "should": "^8.0.2", 25 | "winston": "^2.1.1" 26 | }, 27 | "scripts": { 28 | "pretest": "jscs ./lib/", 29 | "test": "mocha tests/*.js" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/howdyai/botkit.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/howdyai/botkit/issues" 37 | }, 38 | "homepage": "http://howdy.ai/botkit", 39 | "keywords": [ 40 | "bots", 41 | "chatbots", 42 | "slack" 43 | ], 44 | "author": "ben@xoxco.com", 45 | "license": "MIT" 46 | } 47 | -------------------------------------------------------------------------------- /lib/console_logger.js: -------------------------------------------------------------------------------- 1 | var slice = Array.prototype.slice; 2 | /** 3 | * RFC 5424 syslog severity levels, see 4 | * https://tools.ietf.org/html/rfc5424#section-6.2.1 5 | */ 6 | var levels = [ 7 | 'emergency', 8 | 'alert', 9 | 'critical', 10 | 'error', 11 | 'warning', 12 | 'notice', 13 | 'info', 14 | 'debug' 15 | ]; 16 | var levelsByName = levels.reduce(function(out, name, index) { 17 | out[name] = index; 18 | return out; 19 | }, {}); 20 | 21 | function normalizeLogLevel(level) { 22 | if (typeof level === 'string') { 23 | level = levelsByName[level]; 24 | } 25 | if (typeof level === 'number' && level >= 0 && level < levels.length) { 26 | return level; 27 | } 28 | return false; 29 | } 30 | 31 | function ConsoleLogger(_console, maxLevel, defaultLevel) { 32 | _console = _console || console; 33 | maxLevel = normalizeLogLevel(maxLevel) || 6; 34 | defaultLevel = normalizeLogLevel(defaultLevel) || 6; 35 | return { 36 | log: function(level, message) { 37 | var normalizedLevel = normalizeLogLevel(level); 38 | if (!normalizedLevel) { 39 | message = level; 40 | normalizedLevel = defaultLevel; 41 | } 42 | var levelName = levels[normalizedLevel]; 43 | if (normalizedLevel <= maxLevel) { 44 | _console.log.apply( 45 | _console, 46 | [levelName + ': ' + message].concat(slice.call(arguments, 2)) 47 | ); 48 | } 49 | } 50 | }; 51 | } 52 | 53 | ConsoleLogger.LogLevels = levelsByName; 54 | 55 | module.exports = ConsoleLogger; 56 | -------------------------------------------------------------------------------- /lib/middleware/slack_authentication.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Authentication module composed of an Express middleware used to validate 3 | * incoming requests from the Slack API for Slash commands and outgoing 4 | * webhooks. 5 | */ 6 | 7 | // Comparison constant 8 | var TOKEN_NOT_FOUND = -1; 9 | 10 | function init(tokens) { 11 | var authenticationTokens = flatten(tokens); 12 | 13 | if (authenticationTokens.length === 0) { 14 | console.warn('No auth tokens provided, webhook endpoints will always reply HTTP 401.'); 15 | } 16 | 17 | /** 18 | * Express middleware that verifies a Slack token is passed; 19 | * if the expected token value is not passed, end with request test 20 | * with a 401 HTTP status code. 21 | * 22 | * Note: Slack is totally wacky in that the auth token is sent in the body 23 | * of the request instead of a header value. 24 | * 25 | * @param {object} req - Express request object 26 | * @param {object} res - Express response object 27 | * @param {function} next - Express callback 28 | */ 29 | function authenticate(req, res, next) { 30 | if (!req.body || !req.body.token || authenticationTokens.indexOf(req.body.token) === TOKEN_NOT_FOUND) { 31 | res.status(401).send({ 32 | 'code': 401, 33 | 'message': 'Unauthorized' 34 | }); 35 | 36 | return; 37 | } 38 | 39 | slack_botkit.log( 40 | '** Requiring token authentication for webhook endpoints for Slash commands ' + 41 | 'and outgoing webhooks; configured ' + tokens.length + ' tokens' 42 | ); 43 | next(); 44 | } 45 | 46 | return authenticate; 47 | } 48 | /** 49 | * Function that flattens a series of arguments into an array. 50 | * 51 | * @param {Array} args - No token (null), single token (string), or token array (array) 52 | * @returns {Array} - Every element of the array is an authentication token 53 | */ 54 | function flatten(args) { 55 | var result = []; 56 | 57 | // convert a variable argument list to an array 58 | args.forEach(function(arg) { 59 | result = result.concat(arg); 60 | }); 61 | return result; 62 | } 63 | module.exports = init; 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Botkit 2 | 3 | The following is a set of guidelines for contributing to Botkit. 4 | These are just guidelines, not rules, use your best judgment and feel free to 5 | propose changes to this document in a pull request. 6 | 7 | ## Submitting Issues 8 | 9 | * You can create an issue [here](https://github.com/howdyai/botkit/issues/new), 10 | but before doing that please read the notes below and include as many details as 11 | possible with your report. If you can, please include: 12 | * The version of Botkit you are using 13 | * The operating system you are using 14 | * If applicable, what you were doing when the issue arose and what you 15 | expected to happen 16 | * Other things that will help resolve your issue: 17 | * Screenshots and animated GIFs 18 | * Error output that appears in your terminal, dev tools or as an alert 19 | * Perform a [cursory search](https://github.com/howdyai/botkit/issues?utf8=✓&q=is%3Aissue+) 20 | to see if a similar issue has already been submitted 21 | 22 | ## Submitting Pull Requests 23 | 24 | * Include screenshots and animated GIFs in your pull request whenever possible. 25 | * Follow the JavaScript coding style with details from `.jscsrc` and `.editorconfig` files and use necessary plugins for your text editor. 26 | * Write documentation in [Markdown](https://daringfireball.net/projects/markdown). 27 | * Please follow, [JSDoc](http://usejsdoc.org/) for proper documentation. 28 | * Use short, present tense commit messages. See [Commit Message Styleguide](#git-commit-messages). 29 | 30 | ## Styleguides 31 | 32 | ### General Code 33 | 34 | * End files with a newline. 35 | * Place requires in the following order: 36 | * Built in Node Modules (such as `path`) 37 | * Local Modules (using relative paths) 38 | * Avoid platform-dependent code: 39 | * Use `path.join()` to concatenate filenames. 40 | * Using a plain `return` when returning explicitly at the end of a function. 41 | * Not `return null`, `return undefined`, `null`, or `undefined` 42 | 43 | ### Git Commit Messages 44 | 45 | * Use the present tense ("Add feature" not "Added feature") 46 | * Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 47 | * Limit the first line to 72 characters or less 48 | * Reference issues and pull requests liberally 49 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.2 4 | 5 | Adds support for Twilio IP Messenging bots 6 | 7 | Add example bot: twilio_ipm_bot.js 8 | 9 | ## 0.1.2 10 | 11 | *Slack changes:* 12 | 13 | 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)] 14 | 15 | Improves support for direct_mentions of bots in Slack (Merged [PR #189](https://github.com/howdyai/botkit/pull/189)) 16 | 17 | 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)) 18 | 19 | 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)) 20 | 21 | Add [team_slashcommand.js](/examples/team_slashcommand.js) and [team_outgoingwebhook.js](/examples/team_outgoingwebhook.js) to the examples folder. 22 | 23 | 24 | 25 | *Facebook changes:* 26 | 27 | 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)] 28 | 29 | Include attachments field in incoming Facebook messages (Merged [PR #231](https://github.com/howdyai/botkit/pull/231)) 30 | 31 | 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)) 32 | 33 | ## 0.1.1 34 | 35 | Fix issue with over-zealous try/catch in Slack_web_api.js 36 | 37 | ## 0.1.0 38 | 39 | Adds support for Facebook Messenger bots. 40 | 41 | Rename example bot: bot.js became slack_bot.js 42 | 43 | Add example bot: facebook_bot.js 44 | 45 | ## 0.0.15 46 | 47 | Changes conversation.ask to use the same pattern matching function as 48 | is used in `hears()` 49 | 50 | Adds `controller.changeEars()` Developers can now globally change the 51 | way Botkit matches patterns. 52 | 53 | 54 | ## 0.0.14 55 | 56 | Add new middleware hooks. Developers can now change affect a message 57 | as it is received or sent, and can also change the way Botkit matches 58 | patterns in the `hears()` handler. 59 | 60 | ## 0.0.~ 61 | 62 | Next time I promise to start a change log at v0.0.0 63 | -------------------------------------------------------------------------------- /examples/slackbutton_slashcommand.js: -------------------------------------------------------------------------------- 1 | /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | ______ ______ ______ __ __ __ ______ 3 | /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ 4 | \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ 5 | \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ 6 | \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ 7 | 8 | 9 | This is a sample Slack Button application that provides a custom 10 | Slash command. 11 | 12 | This bot demonstrates many of the core features of Botkit: 13 | 14 | * 15 | * Authenticate users with Slack using OAuth 16 | * Receive messages using the slash_command event 17 | * Reply to Slash command both publicly and privately 18 | 19 | # RUN THE BOT: 20 | 21 | Create a Slack app. Make sure to configure at least one Slash command! 22 | 23 | -> https://api.slack.com/applications/new 24 | 25 | Run your bot from the command line: 26 | 27 | clientId= clientSecret= port=3000 node bot.js 28 | 29 | Note: you can test your oauth authentication locally, but to use Slash commands 30 | in Slack, the app must be hosted at a publicly reachable IP or host. 31 | 32 | 33 | # EXTEND THE BOT: 34 | 35 | Botkit has many features for building cool and useful bots! 36 | 37 | Read all about it here: 38 | 39 | -> http://howdy.ai/botkit 40 | 41 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ 42 | var Botkit = require('../lib/Botkit.js'); 43 | 44 | if (!process.env.clientId || !process.env.clientSecret || !process.env.port) { 45 | console.log('Error: Specify clientId clientSecret and port in environment'); 46 | process.exit(1); 47 | } 48 | 49 | var controller = Botkit.slackbot({ 50 | json_file_store: './db_slackbutton_slashcommand/', 51 | }).configureSlackApp({ 52 | clientId: process.env.clientId, 53 | clientSecret: process.env.clientSecret, 54 | scopes: ['commands'], 55 | }); 56 | 57 | 58 | controller.setupWebserver(process.env.port,function(err,webserver) { 59 | 60 | controller.createWebhookEndpoints(controller.webserver); 61 | 62 | controller.createOauthEndpoints(controller.webserver,function(err,req,res) { 63 | if (err) { 64 | res.status(500).send('ERROR: ' + err); 65 | } else { 66 | res.send('Success!'); 67 | } 68 | }); 69 | }); 70 | 71 | 72 | controller.on('slash_command',function(bot,message) { 73 | 74 | bot.replyPublic(message,'<@' + message.user + '> is cool!'); 75 | bot.replyPrivate(message,'*nudge nudge wink wink*'); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /lib/storage/simple_storage.js: -------------------------------------------------------------------------------- 1 | /* 2 | Storage module for bots. 3 | 4 | Supports storage of data on a team-by-team, user-by-user, and chnnel-by-channel basis. 5 | 6 | save can be used to store arbitrary object. 7 | These objects must include an id by which they can be looked up. 8 | It is recommended to use the team/user/channel id for this purpose. 9 | Example usage of save: 10 | controller.storage.teams.save({id: message.team, foo:"bar"}, function(err){ 11 | if (err) 12 | console.log(err) 13 | }); 14 | 15 | get looks up an object by id. 16 | Example usage of get: 17 | controller.storage.teams.get(message.team, function(err, team_data){ 18 | if (err) 19 | console.log(err) 20 | else 21 | console.log(team_data) 22 | }); 23 | */ 24 | 25 | var Store = require('jfs'); 26 | 27 | module.exports = function(config) { 28 | 29 | if (!config) { 30 | config = { 31 | path: './', 32 | }; 33 | } 34 | 35 | var teams_db = new Store(config.path + '/teams', {saveId: 'id'}); 36 | var users_db = new Store(config.path + '/users', {saveId: 'id'}); 37 | var channels_db = new Store(config.path + '/channels', {saveId: 'id'}); 38 | 39 | var objectsToList = function(cb) { 40 | return function(err, data) { 41 | if (err) { 42 | cb(err, data); 43 | } else { 44 | cb(err, Object.keys(data).map(function(key) { 45 | return data[key]; 46 | })); 47 | } 48 | }; 49 | }; 50 | 51 | var storage = { 52 | teams: { 53 | get: function(team_id, cb) { 54 | teams_db.get(team_id, cb); 55 | }, 56 | save: function(team_data, cb) { 57 | teams_db.save(team_data.id, team_data, cb); 58 | }, 59 | all: function(cb) { 60 | teams_db.all(objectsToList(cb)); 61 | } 62 | }, 63 | users: { 64 | get: function(user_id, cb) { 65 | users_db.get(user_id, cb); 66 | }, 67 | save: function(user, cb) { 68 | users_db.save(user.id, user, cb); 69 | }, 70 | all: function(cb) { 71 | users_db.all(objectsToList(cb)); 72 | } 73 | }, 74 | channels: { 75 | get: function(channel_id, cb) { 76 | channels_db.get(channel_id, cb); 77 | }, 78 | save: function(channel, cb) { 79 | channels_db.save(channel.id, channel, cb); 80 | }, 81 | all: function(cb) { 82 | channels_db.all(objectsToList(cb)); 83 | } 84 | } 85 | }; 86 | 87 | return storage; 88 | }; 89 | -------------------------------------------------------------------------------- /lib/storage/storage_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Tests for storage modules. 3 | This file currently test simple_storage.js, redis_storage, and firebase_storage. 4 | 5 | If you build a new storage module, 6 | you must add it to this test file before your PR will be considered. 7 | How to add it to this test file: 8 | 9 | Add the following to the bottom of this file: 10 | 11 | // Test 12 | = require('./.js')(); 13 | check(.users); 14 | check(.channels); 15 | check(.teams); 16 | */ 17 | 18 | var test = require('unit.js'); 19 | 20 | testObj0 = {id: 'TEST0', foo: 'bar0'}; 21 | testObj1 = {id: 'TEST1', foo: 'bar1'}; 22 | 23 | var testStorageMethod = function(storageMethod) { 24 | storageMethod.save(testObj0, function(err) { 25 | test.assert(!err); 26 | storageMethod.save(testObj1, function(err) { 27 | test.assert(!err); 28 | storageMethod.get(testObj0.id, function(err, data) { 29 | test.assert(!err); 30 | console.log(data); 31 | test.assert(data.foo === testObj0.foo); 32 | }); 33 | storageMethod.get('shouldnt-be-here', function(err, data) { 34 | test.assert(err.displayName === 'NotFound'); 35 | test.assert(!data); 36 | }); 37 | storageMethod.all(function(err, data) { 38 | test.assert(!err); 39 | console.log(data); 40 | test.assert( 41 | data[0].foo === testObj0.foo && data[1].foo === testObj1.foo || 42 | data[0].foo === testObj1.foo && data[1].foo === testObj0.foo 43 | ); 44 | }); 45 | }); 46 | }); 47 | }; 48 | 49 | console.log('If no asserts failed then the test has passed!'); 50 | 51 | // Test simple_storage 52 | var simple_storage = require('./simple_storage.js')(); 53 | testStorageMethod(simple_storage.users); 54 | testStorageMethod(simple_storage.channels); 55 | testStorageMethod(simple_storage.teams); 56 | 57 | // Test redis_storage 58 | var redis_storage = require('./redis_storage.js')({ 59 | url: 'redis://redistogo:d175f29259bd73e442eefcaeff8e78aa@tarpon.redistogo.com:11895/' 60 | }); 61 | testStorageMethod(redis_storage.users); 62 | testStorageMethod(redis_storage.channels); 63 | testStorageMethod(redis_storage.teams); 64 | 65 | // Test firebase_storage 66 | var firebase_storage = require('./firebase_storage.js')({ 67 | firebase_uri: 'https://botkit-example.firebaseio.com' 68 | }); 69 | testStorageMethod(firebase_storage.users); 70 | testStorageMethod(firebase_storage.channels); 71 | testStorageMethod(firebase_storage.teams); 72 | -------------------------------------------------------------------------------- /examples/convo_bot.js: -------------------------------------------------------------------------------- 1 | /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | ______ ______ ______ __ __ __ ______ 3 | /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ 4 | \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ 5 | \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ 6 | \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ 7 | 8 | 9 | This is a sample Slack bot built with Botkit. 10 | 11 | This bot demonstrates a multi-stage conversation 12 | 13 | # RUN THE BOT: 14 | 15 | Get a Bot token from Slack: 16 | 17 | -> http://my.slack.com/services/new/bot 18 | 19 | Run your bot from the command line: 20 | 21 | token= node demo_bot.js 22 | 23 | # USE THE BOT: 24 | 25 | Find your bot inside Slack 26 | 27 | Say: "pizzatime" 28 | 29 | The bot will reply "What flavor of pizza do you want?" 30 | 31 | Say what flavor you want. 32 | 33 | The bot will reply "Awesome" "What size do you want?" 34 | 35 | Say what size you want. 36 | 37 | The bot will reply "Ok." "So where do you want it delivered?" 38 | 39 | Say where you want it delivered. 40 | 41 | The bot will reply "Ok! Goodbye." 42 | 43 | ...and will refrain from billing your card because this is just a demo :P 44 | 45 | # EXTEND THE BOT: 46 | 47 | Botkit has many features for building cool and useful bots! 48 | 49 | Read all about it here: 50 | 51 | -> http://howdy.ai/botkit 52 | 53 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ 54 | 55 | var Botkit = require('../lib/Botkit.js'); 56 | 57 | if (!process.env.token) { 58 | console.log('Error: Specify token in environment'); 59 | process.exit(1); 60 | } 61 | 62 | var controller = Botkit.slackbot({ 63 | debug: false 64 | }); 65 | 66 | controller.spawn({ 67 | token: process.env.token 68 | }).startRTM(function(err) { 69 | if (err) { 70 | throw new Error(err); 71 | } 72 | }); 73 | 74 | controller.hears(['pizzatime'],['ambient'],function(bot,message) { 75 | bot.startConversation(message, askFlavor); 76 | }); 77 | 78 | askFlavor = function(response, convo) { 79 | convo.ask("What flavor of pizza do you want?", function(response, convo) { 80 | convo.say("Awesome."); 81 | askSize(response, convo); 82 | convo.next(); 83 | }); 84 | } 85 | askSize = function(response, convo) { 86 | convo.ask("What size do you want?", function(response, convo) { 87 | convo.say("Ok.") 88 | askWhereDeliver(response, convo); 89 | convo.next(); 90 | }); 91 | } 92 | askWhereDeliver = function(response, convo) { 93 | convo.ask("So where do you want it delivered?", function(response, convo) { 94 | convo.say("Ok! Goodbye."); 95 | convo.next(); 96 | }); 97 | } 98 | -------------------------------------------------------------------------------- /tests/Slack_web_api.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var Botkit = require('../'); 3 | var path = require('path'); 4 | var tmpdir = require('os').tmpdir(); 5 | var fs = require('fs'); 6 | var winston = require('winston'); 7 | 8 | var token = process.env.TOKEN; 9 | 10 | describe('Test', function() { 11 | it('should have a token', function(done) { 12 | should.exist(token); 13 | done(); 14 | }); 15 | 16 | it('should have Botkit instance', function(done) { 17 | should.exist(Botkit); 18 | should.exist(Botkit.core); 19 | should.exist(Botkit.slackbot); 20 | done(); 21 | }); 22 | }); 23 | 24 | describe('Botkit', function() { 25 | this.timeout(5000); 26 | 27 | it('should start and then stop', function(done) { 28 | var controller = Botkit.slackbot({debug: false}); 29 | var openIsCalled = false; 30 | 31 | controller.on('rtm_open', function(bot) { 32 | should.exist(bot); 33 | openIsCalled = true; 34 | }); 35 | 36 | controller.on('rtm_close', function(bot) { 37 | should.exist(bot); 38 | openIsCalled.should.be.true; 39 | controller.shutdown(); 40 | done(); 41 | }); 42 | 43 | controller 44 | .spawn({ 45 | token: token 46 | }) 47 | .startRTM(function(err, bot, payload) { 48 | (err === null).should.be.true; 49 | should.exist(bot); 50 | bot.closeRTM(); 51 | }); 52 | }); 53 | 54 | it('should have fail with false token', function(done) { 55 | this.timeout(5000); 56 | 57 | var controller = Botkit.slackbot({debug: false}); 58 | 59 | controller 60 | .spawn({ 61 | token: '1234' 62 | }) 63 | .startRTM(function(err, bot, payload) { 64 | should.exist(err); 65 | 66 | controller.shutdown(); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | 72 | describe('Log', function() { 73 | it('should use an external logging provider', function(done) { 74 | var logFile = path.join(tmpdir, 'botkit.log'); 75 | var logger = new winston.Logger({ 76 | transports: [ 77 | new (winston.transports.File)({ filename: logFile }) 78 | ] 79 | }); 80 | 81 | logger.cli(); 82 | 83 | var controller = Botkit.slackbot({ 84 | debug: true, 85 | logger: logger 86 | }); 87 | 88 | controller 89 | .spawn({ 90 | token: '1234' 91 | }) 92 | .startRTM(function(err, bot, payload) { 93 | should.exist(err); 94 | 95 | controller.shutdown(); 96 | 97 | fs.readFile(logFile, 'utf8', function(err, res) { 98 | (err === null).should.be.true; 99 | should.exist(res); 100 | done(); 101 | }); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /examples/demo_bot.js: -------------------------------------------------------------------------------- 1 | /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | ______ ______ ______ __ __ __ ______ 3 | /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ 4 | \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ 5 | \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ 6 | \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ 7 | 8 | 9 | This is a sample Slack bot built with Botkit. 10 | 11 | This bot demonstrates many of the core features of Botkit: 12 | 13 | * Connect to Slack using the real time API 14 | * Receive messages based on "spoken" patterns 15 | * Send a message with attachments 16 | * Send a message via direct message (instead of in a public channel) 17 | 18 | # RUN THE BOT: 19 | 20 | Get a Bot token from Slack: 21 | 22 | -> http://my.slack.com/services/new/bot 23 | 24 | Run your bot from the command line: 25 | 26 | token= node demo_bot.js 27 | 28 | # USE THE BOT: 29 | 30 | Find your bot inside Slack to send it a direct message. 31 | 32 | Say: "Hello" 33 | 34 | The bot will reply "Hello!" 35 | 36 | Say: "Attach" 37 | 38 | The bot will send a message with a multi-field attachment. 39 | 40 | Send: "dm me" 41 | 42 | The bot will reply with a direct message. 43 | 44 | Make sure to invite your bot into other channels using /invite @! 45 | 46 | # EXTEND THE BOT: 47 | 48 | Botkit has many features for building cool and useful bots! 49 | 50 | Read all about it here: 51 | 52 | -> http://howdy.ai/botkit 53 | 54 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ 55 | 56 | var Botkit = require('../lib/Botkit.js'); 57 | 58 | 59 | if (!process.env.token) { 60 | console.log('Error: Specify token in environment'); 61 | process.exit(1); 62 | } 63 | 64 | var controller = Botkit.slackbot({ 65 | debug: false 66 | }); 67 | 68 | controller.spawn({ 69 | token: process.env.token 70 | }).startRTM(function(err) { 71 | if (err) { 72 | throw new Error(err); 73 | } 74 | }); 75 | 76 | 77 | controller.hears(['hello','hi'],['direct_message','direct_mention','mention'],function(bot,message) { 78 | bot.reply(message,"Hello."); 79 | }); 80 | 81 | controller.hears(['attach'],['direct_message','direct_mention'],function(bot,message) { 82 | 83 | var attachments = []; 84 | var attachment = { 85 | title: 'This is an attachment', 86 | color: '#FFCC99', 87 | fields: [], 88 | }; 89 | 90 | attachment.fields.push({ 91 | label: 'Field', 92 | value: 'A longish value', 93 | short: false, 94 | }); 95 | 96 | attachment.fields.push({ 97 | label: 'Field', 98 | value: 'Value', 99 | short: true, 100 | }); 101 | 102 | attachment.fields.push({ 103 | label: 'Field', 104 | value: 'Value', 105 | short: true, 106 | }); 107 | 108 | attachments.push(attachment); 109 | 110 | bot.reply(message,{ 111 | text: 'See below...', 112 | attachments: attachments, 113 | },function(err,resp) { 114 | console.log(err,resp); 115 | }); 116 | }); 117 | 118 | controller.hears(['dm me'],['direct_message','direct_mention'],function(bot,message) { 119 | bot.startConversation(message,function(err,convo) { 120 | convo.say('Heard ya'); 121 | }); 122 | 123 | bot.startPrivateConversation(message,function(err,dm) { 124 | dm.say('Private reply!'); 125 | }); 126 | 127 | }); 128 | -------------------------------------------------------------------------------- /examples/slackbutton_incomingwebhooks.js: -------------------------------------------------------------------------------- 1 | /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | ______ ______ ______ __ __ __ ______ 3 | /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ 4 | \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ 5 | \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ 6 | \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ 7 | 8 | 9 | This is a sample Slack Button application that allows the application 10 | to post messages into Slack. 11 | 12 | This bot demonstrates many of the core features of Botkit: 13 | 14 | * Authenticate users with Slack using OAuth 15 | * Receive messages using the slash_command event 16 | * Reply to Slash command both publicly and privately 17 | 18 | # RUN THE APP: 19 | 20 | Create a Slack app. Make sure to configure at least one Slash command! 21 | 22 | -> https://api.slack.com/applications/new 23 | 24 | Run your bot from the command line: 25 | 26 | clientId= clientSecret= port=3000 node bot.js 27 | 28 | # USE THE APP 29 | 30 | Add the app to your Slack by visiting the login page: 31 | 32 | -> http://localhost:3000/login 33 | 34 | After you've added the app, send a message using the SUPER INSECURE FORM. 35 | This form is included as an example only, and should definitely not be 36 | left in place if you use this code to start your own project. 37 | 38 | Send a message to every team who has added your sample app: 39 | 40 | -> http://localhost:3000/ 41 | 42 | 43 | # EXTEND THE APP: 44 | 45 | Botkit has many features for building cool and useful bots! 46 | 47 | Read all about it here: 48 | 49 | -> http://howdy.ai/botkit 50 | 51 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ 52 | var Botkit = require('../lib/Botkit.js'); 53 | 54 | if (!process.env.clientId || !process.env.clientSecret || !process.env.port) { 55 | console.log('Error: Specify clientId clientSecret and port in environment'); 56 | process.exit(1); 57 | } 58 | 59 | var controller = Botkit.slackbot({ 60 | json_file_store: './db_slackbutton_incomingwebhook/', 61 | }).configureSlackApp( 62 | { 63 | clientId: process.env.clientId, 64 | clientSecret: process.env.clientSecret, 65 | scopes: ['incoming-webhook'], 66 | } 67 | ); 68 | 69 | 70 | controller.setupWebserver(process.env.port,function(err,webserver) { 71 | 72 | 73 | webserver.get('/',function(req,res) { 74 | 75 | var html = '

Super Insecure Form

Put text below and hit send - it will be sent to every team who has added your integration.

'; 76 | res.send(html); 77 | 78 | }); 79 | 80 | // This is a completely insecure form which would enable 81 | // anyone on the internet who found your node app to 82 | // broadcast to all teams who have added your integration. 83 | // it is included for demonstration purposes only!!! 84 | webserver.post('/unsafe_endpoint',function(req,res) { 85 | var text = req.body.text; 86 | text = text.trim(); 87 | 88 | controller.storage.teams.all(function(err,teams) { 89 | var count = 0; 90 | for (var t in teams) { 91 | if (teams[t].incoming_webhook) { 92 | count++; 93 | controller.spawn(teams[t]).sendWebhook({ 94 | text: text 95 | },function(err) { 96 | if(err) { 97 | console.log(err); 98 | } 99 | }); 100 | } 101 | } 102 | 103 | res.send('Message sent to ' + count + ' teams!'); 104 | }); 105 | }); 106 | 107 | controller.createOauthEndpoints(controller.webserver,function(err,req,res) { 108 | if (err) { 109 | res.status(500).send('ERROR: ' + err); 110 | } else { 111 | res.send('Success!'); 112 | } 113 | }); 114 | }); 115 | 116 | 117 | controller.on('create_incoming_webhook',function(bot,webhook_config) { 118 | bot.sendWebhook({ 119 | text: ':thumbsup: Incoming webhook successfully configured' 120 | }); 121 | }) 122 | -------------------------------------------------------------------------------- /twilio_ipm_bot.js: -------------------------------------------------------------------------------- 1 | var Botkit = require('./lib/Botkit.js'); 2 | var os = require('os'); 3 | var controller = Botkit.twilioipmbot({ 4 | debug: false, 5 | }); 6 | 7 | var bot = controller.spawn({ 8 | TWILIO_IPM_SERVICE_SID: process.env.TWILIO_IPM_SERVICE_SID, 9 | TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID, 10 | TWILIO_API_KEY: process.env.TWILIO_API_KEY, 11 | TWILIO_API_SECRET: process.env.TWILIO_API_SECRET, 12 | identity: 'Botkit', 13 | autojoin: true 14 | }); 15 | 16 | controller.setupWebserver(process.env.port || 3000, function(err, server) { 17 | 18 | server.get('/', function(req, res) { 19 | res.send(':)'); 20 | }); 21 | 22 | controller.createWebhookEndpoints(server, bot); 23 | 24 | }); 25 | 26 | controller.on('bot_channel_join', function(bot, message) { 27 | bot.reply(message, 'Here I am!'); 28 | }); 29 | 30 | controller.on('user_channel_join', function(bot,message) { 31 | bot.reply(message, 'Welcome, ' + message.user + '!'); 32 | }); 33 | 34 | controller.on('user_channel_leave', function(bot,message) { 35 | bot.reply(message, 'Bye, ' + message.user + '!'); 36 | }); 37 | 38 | 39 | controller.hears(['hello', 'hi'], 'message_received', function(bot, message) { 40 | 41 | controller.storage.users.get(message.user, function(err, user) { 42 | if (user && user.name) { 43 | bot.reply(message, 'Hello ' + user.name + '!!'); 44 | } else { 45 | bot.reply(message, 'Hello.'); 46 | } 47 | }); 48 | }); 49 | 50 | controller.hears(['call me (.*)'], 'message_received', function(bot, message) { 51 | var matches = message.text.match(/call me (.*)/i); 52 | var name = matches[1]; 53 | controller.storage.users.get(message.user, function(err, user) { 54 | if (!user) { 55 | user = { 56 | id: message.user, 57 | }; 58 | } 59 | user.name = name; 60 | controller.storage.users.save(user, function(err, id) { 61 | bot.reply(message, 'Got it. I will call you ' + user.name + ' from now on.'); 62 | }); 63 | }); 64 | }); 65 | 66 | controller.hears(['what is my name', 'who am i'], 'message_received', function(bot, message) { 67 | 68 | controller.storage.users.get(message.user, function(err, user) { 69 | if (user && user.name) { 70 | bot.reply(message,'Your name is ' + user.name); 71 | } else { 72 | bot.reply(message,'I don\'t know yet!'); 73 | } 74 | }); 75 | }); 76 | 77 | 78 | controller.hears(['shutdown'],'message_received',function(bot, message) { 79 | 80 | bot.startConversation(message,function(err, convo) { 81 | convo.ask('Are you sure you want me to shutdown?',[ 82 | { 83 | pattern: bot.utterances.yes, 84 | callback: function(response, convo) { 85 | convo.say('Bye!'); 86 | convo.next(); 87 | setTimeout(function() { 88 | process.exit(); 89 | },3000); 90 | } 91 | }, 92 | { 93 | pattern: bot.utterances.no, 94 | default: true, 95 | callback: function(response, convo) { 96 | convo.say('*Phew!*'); 97 | convo.next(); 98 | } 99 | } 100 | ]); 101 | }); 102 | }); 103 | 104 | 105 | controller.hears(['uptime','identify yourself','who are you','what is your name'],'message_received',function(bot, message) { 106 | 107 | var hostname = os.hostname(); 108 | var uptime = formatUptime(process.uptime()); 109 | 110 | bot.reply(message,'I am a bot! I have been running for ' + uptime + ' on ' + hostname + '.'); 111 | 112 | }); 113 | 114 | function formatUptime(uptime) { 115 | var unit = 'second'; 116 | if (uptime > 60) { 117 | uptime = uptime / 60; 118 | unit = 'minute'; 119 | } 120 | if (uptime > 60) { 121 | uptime = uptime / 60; 122 | unit = 'hour'; 123 | } 124 | if (uptime != 1) { 125 | unit = unit + 's'; 126 | } 127 | 128 | uptime = uptime + ' ' + unit; 129 | return uptime; 130 | } 131 | -------------------------------------------------------------------------------- /examples/slackbutton_bot.js: -------------------------------------------------------------------------------- 1 | /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | ______ ______ ______ __ __ __ ______ 3 | /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ 4 | \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ 5 | \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ 6 | \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ 7 | 8 | 9 | This is a sample Slack Button application that adds a bot to one or many slack teams. 10 | 11 | # RUN THE APP: 12 | Create a Slack app. Make sure to configure the bot user! 13 | -> https://api.slack.com/applications/new 14 | -> Add the Redirect URI: http://localhost:3000/oauth 15 | Run your bot from the command line: 16 | clientId= clientSecret= port=3000 node slackbutton_bot.js 17 | # USE THE APP 18 | Add the app to your Slack by visiting the login page: 19 | -> http://localhost:3000/login 20 | After you've added the app, try talking to your bot! 21 | # EXTEND THE APP: 22 | Botkit has many features for building cool and useful bots! 23 | Read all about it here: 24 | -> http://howdy.ai/botkit 25 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ 26 | 27 | /* Uses the slack button feature to offer a real time bot to multiple teams */ 28 | var Botkit = require('../lib/Botkit.js'); 29 | 30 | if (!process.env.clientId || !process.env.clientSecret || !process.env.port) { 31 | console.log('Error: Specify clientId clientSecret and port in environment'); 32 | process.exit(1); 33 | } 34 | 35 | 36 | var controller = Botkit.slackbot({ 37 | json_file_store: './db_slackbutton_bot/', 38 | }).configureSlackApp( 39 | { 40 | clientId: process.env.clientId, 41 | clientSecret: process.env.clientSecret, 42 | scopes: ['bot'], 43 | } 44 | ); 45 | 46 | controller.setupWebserver(process.env.port,function(err,webserver) { 47 | controller.createWebhookEndpoints(controller.webserver); 48 | 49 | controller.createOauthEndpoints(controller.webserver,function(err,req,res) { 50 | if (err) { 51 | res.status(500).send('ERROR: ' + err); 52 | } else { 53 | res.send('Success!'); 54 | } 55 | }); 56 | }); 57 | 58 | 59 | // just a simple way to make sure we don't 60 | // connect to the RTM twice for the same team 61 | var _bots = {}; 62 | function trackBot(bot) { 63 | _bots[bot.config.token] = bot; 64 | } 65 | 66 | controller.on('create_bot',function(bot,config) { 67 | 68 | if (_bots[bot.config.token]) { 69 | // already online! do nothing. 70 | } else { 71 | bot.startRTM(function(err) { 72 | 73 | if (!err) { 74 | trackBot(bot); 75 | } 76 | 77 | bot.startPrivateConversation({user: config.createdBy},function(err,convo) { 78 | if (err) { 79 | console.log(err); 80 | } else { 81 | convo.say('I am a bot that has just joined your team'); 82 | convo.say('You must now /invite me to a channel so that I can be of use!'); 83 | } 84 | }); 85 | 86 | }); 87 | } 88 | 89 | }); 90 | 91 | 92 | // Handle events related to the websocket connection to Slack 93 | controller.on('rtm_open',function(bot) { 94 | console.log('** The RTM api just connected!'); 95 | }); 96 | 97 | controller.on('rtm_close',function(bot) { 98 | console.log('** The RTM api just closed'); 99 | // you may want to attempt to re-open 100 | }); 101 | 102 | controller.hears('hello','direct_message',function(bot,message) { 103 | bot.reply(message,'Hello!'); 104 | }); 105 | 106 | controller.hears('^stop','direct_message',function(bot,message) { 107 | bot.reply(message,'Goodbye'); 108 | bot.rtm.close(); 109 | }); 110 | 111 | controller.on(['direct_message','mention','direct_mention'],function(bot,message) { 112 | bot.api.reactions.add({ 113 | timestamp: message.ts, 114 | channel: message.channel, 115 | name: 'robot_face', 116 | },function(err) { 117 | if (err) { console.log(err) } 118 | bot.reply(message,'I heard you loud and clear boss.'); 119 | }); 120 | }); 121 | 122 | controller.storage.teams.all(function(err,teams) { 123 | 124 | if (err) { 125 | throw new Error(err); 126 | } 127 | 128 | // connect all teams with bots up to slack! 129 | for (var t in teams) { 130 | if (teams[t].bot) { 131 | controller.spawn(teams[t]).startRTM(function(err, bot) { 132 | if (err) { 133 | console.log('Error connecting bot to Slack:',err); 134 | } else { 135 | trackBot(bot); 136 | } 137 | }); 138 | } 139 | } 140 | 141 | }); 142 | -------------------------------------------------------------------------------- /examples/sentiment_analysis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* "dependencies": { 3 | "botkit": "0.0.7", 4 | "escape-string-regexp": "^1.0.5", 5 | "lodash": "^4.5.1", 6 | "mongodb": "^2.1.7", 7 | "sentiment": "^1.0.6", 8 | } 9 | */ 10 | var _ = require('lodash'); 11 | var escapeStringRegexp = require('escape-string-regexp'); 12 | var botkit = require('botkit'); 13 | var mongodb = require('mongodb'); 14 | var sentiment = require('sentiment'); 15 | 16 | function connectToDb() { 17 | mongodb.MongoClient.connect('mongodb://localhost:27017/sentiment', function (err, db) { 18 | if (err) { 19 | throw err; 20 | } 21 | console.log('Connection established to mongodb'); 22 | startBot(db); 23 | }); 24 | } 25 | 26 | function startBot(db) { 27 | var collection = db.collection('sentiment'); 28 | 29 | var INSTANCE_PUBLIC_SHAMING = true; 30 | var INSTANCE_PUBLIC_SHAMING_THRESHOLD = -4; 31 | 32 | var INSTANCE_PRIVATE_SHAMING = true; 33 | var INSTANCE_PRIVATE_SHAMING_THRESHOLD = -4; 34 | 35 | var COUNT_POSITIVE_SCORES = true; 36 | var COUNT_NEGATIVE_SCORES = true; 37 | 38 | var INSTANCE_PUBLIC_SHAMING_MESSAGES = [ 39 | 'Remember to keep up the DoublePlusGood GoodThink vibes for our SafeSpace.', 40 | 'Remember, we\'re all in this together for the benefit of our Company.', 41 | 'Let\'s stay positive! Remember: There\'s no I in team but there\'s an "eye" in ' + 42 | 'this team. ;)', 43 | 'We wouldn\'t want this to stay on our permanent record. Let\'s speak more positively' ]; 44 | var INSTANCE_PRIVATE_SHAMING_MESSAGES = [ 45 | 'Please remember to be civil. This will be on your HR file.', 46 | 'Only Happy fun times are allowed here. Remember GoodThink and PositiveVibes.', 47 | 'Let\'s stay positive! Remember: There\'s no I in team but there\'s an "eye" in this ' + 48 | 'team. ;). Watching you.', 49 | 'Upbeat messages only. This has been logged to keep everyone safe.' ]; 50 | 51 | var afinn = require('sentiment/build/AFINN.json'); 52 | 53 | var botkitController = botkit.slackbot({ 54 | debug: false 55 | }); 56 | 57 | botkitController.spawn({ 58 | token: process.env.token 59 | }).startRTM(function (err) { 60 | if (err) { 61 | throw err; 62 | } 63 | }); 64 | 65 | botkitController.hears([ 'hello', 'hi' ], [ 'direct_mention' ], function (bot, message) { 66 | bot.reply(message, 'Hello. I\'m watching you.'); 67 | }); 68 | 69 | var formatReportList = function formatReportList(result) { 70 | return result.map(function (i) { 71 | return '<@' + i._id + '>: ' + i.score; 72 | }); 73 | }; 74 | 75 | botkitController.hears([ 'report' ], [ 'direct_message', 'direct_mention' ], function (bot, message) { 76 | collection.aggregate([ { $sort: { score: 1 } }, { $limit: 10 } ]).toArray( 77 | function (err, result) { 78 | if (err) { 79 | throw err; 80 | } 81 | 82 | var topList = formatReportList(result); 83 | bot.reply(message, 'Top 10 Scores:\n' + topList.join('"\n"')); 84 | }); 85 | collection.aggregate([ { $sort: { score: -1 } }, { $limit: 10 } ]).toArray( 86 | function (err, result) { 87 | if (err) { 88 | throw err; 89 | } 90 | 91 | var bottomList = formatReportList(result); 92 | bot.reply(message, 'Bottom 10 Scores:\n' + bottomList.join('\n')); 93 | }); 94 | }); 95 | 96 | var listeningFor = '^' + Object.keys(afinn).map(escapeStringRegexp).join('|') + '$'; 97 | botkitController.hears([ listeningFor ], [ 'ambient' ], function (bot, message) { 98 | var sentimentAnalysis = sentiment(message.text); 99 | console.log({ sentimentAnalysis: sentimentAnalysis }); 100 | if (COUNT_POSITIVE_SCORES == false && sentimentAnalysis.score > 0) { 101 | return; 102 | } 103 | 104 | if (COUNT_NEGATIVE_SCORES == false && sentimentAnalysis.score < 0) { 105 | return; 106 | } 107 | 108 | collection.findAndModify({ _id: message.user }, [ [ '_id', 1 ] ], { 109 | $inc: { score: sentimentAnalysis.score } 110 | }, { 'new': true, upsert: true }, function (err, result) { 111 | if (err) { 112 | throw err; 113 | } 114 | 115 | // full doc is available in result object: 116 | // console.log(result) 117 | var shamed = false; 118 | if (INSTANCE_PUBLIC_SHAMING && 119 | sentimentAnalysis.score <= INSTANCE_PUBLIC_SHAMING_THRESHOLD) { 120 | shamed = true; 121 | bot.startConversation(message, function (err, convo) { 122 | if (err) { 123 | throw err; 124 | } 125 | 126 | var publicShamingMessage = _.sample(INSTANCE_PUBLIC_SHAMING_MESSAGES); 127 | console.log({ publicShamingMessage: publicShamingMessage }); 128 | convo.say(publicShamingMessage); 129 | }); 130 | } 131 | 132 | if (!shamed && INSTANCE_PRIVATE_SHAMING && 133 | sentimentAnalysis.score <= INSTANCE_PRIVATE_SHAMING_THRESHOLD) { 134 | bot.startPrivateConversation(message, function (err, dm) { 135 | if (err) { 136 | throw err; 137 | } 138 | 139 | var privateShamingMessage = _.sample(INSTANCE_PRIVATE_SHAMING_MESSAGES); 140 | console.log({ privateShamingMessage: privateShamingMessage }); 141 | dm.say(privateShamingMessage); 142 | }); 143 | } 144 | }); 145 | }); 146 | } 147 | 148 | if (!process.env.token) { 149 | console.log('Error: Specify token in environment'); 150 | process.exit(1); 151 | } 152 | 153 | connectToDb(); 154 | -------------------------------------------------------------------------------- /examples/middleware_example.js: -------------------------------------------------------------------------------- 1 | /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | ______ ______ ______ __ __ __ ______ 3 | /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ 4 | \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ 5 | \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ 6 | \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ 7 | 8 | 9 | This is a sample Slack bot built with Botkit. 10 | 11 | This bot demonstrates many of the core features of Botkit: 12 | 13 | * Connect to Slack using the real time API 14 | * Receive messages based on "spoken" patterns 15 | * Reply to messages 16 | * Use the conversation system to ask questions 17 | * Use the built in storage system to store and retrieve information 18 | for a user. 19 | 20 | # RUN THE BOT: 21 | 22 | Get a Bot token from Slack: 23 | 24 | -> http://my.slack.com/services/new/bot 25 | 26 | Run your bot from the command line: 27 | 28 | token= node bot.js 29 | 30 | # USE THE BOT: 31 | 32 | Find your bot inside Slack to send it a direct message. 33 | 34 | Say: "Hello" 35 | 36 | The bot will reply "Hello!" 37 | 38 | Say: "who are you?" 39 | 40 | The bot will tell you its name, where it running, and for how long. 41 | 42 | Say: "Call me " 43 | 44 | Tell the bot your nickname. Now you are friends. 45 | 46 | Say: "who am I?" 47 | 48 | The bot will tell you your nickname, if it knows one for you. 49 | 50 | Say: "shutdown" 51 | 52 | The bot will ask if you are sure, and then shut itself down. 53 | 54 | Make sure to invite your bot into other channels using /invite @! 55 | 56 | # EXTEND THE BOT: 57 | 58 | Botkit has many features for building cool and useful bots! 59 | 60 | Read all about it here: 61 | 62 | -> http://howdy.ai/botkit 63 | 64 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ 65 | 66 | 67 | if (!process.env.token) { 68 | console.log('Error: Specify token in environment'); 69 | process.exit(1); 70 | } 71 | 72 | var Botkit = require('../lib/Botkit.js'); 73 | var os = require('os'); 74 | 75 | var controller = Botkit.slackbot({ 76 | debug: true, 77 | }); 78 | 79 | var bot = controller.spawn({ 80 | token: process.env.token 81 | }).startRTM(); 82 | 83 | 84 | // Example receive middleware. 85 | // for example, recognize several common variations on "hello" and add an intent field to the message 86 | // see below for example hear_intent function 87 | controller.middleware.receive.use(function(bot, message, next) { 88 | 89 | console.log('Receive middleware!'); 90 | // make changes to bot or message here before calling next 91 | if (message.text == 'hello' || message.text == 'hi' || message.text == 'howdy' || message.text == 'hey') { 92 | message.intent = 'hello'; 93 | } 94 | 95 | next(); 96 | 97 | }); 98 | 99 | // Example send middleware 100 | // make changes to bot or message here before calling next 101 | // for example, do formatting or add additional information to the message 102 | controller.middleware.send.use(function(bot, message, next) { 103 | 104 | console.log('Send middleware!'); 105 | next(); 106 | 107 | }); 108 | 109 | 110 | // Example hear middleware 111 | // Return true if one of [patterns] matches message 112 | // In this example, listen for an intent field, and match using that instead of the text field 113 | function hear_intent(patterns, message) { 114 | 115 | for (var p = 0; p < patterns.length; p++) { 116 | if (message.intent == patterns[p]) { 117 | return true; 118 | } 119 | } 120 | 121 | return false; 122 | } 123 | 124 | 125 | /* note this uses example middlewares defined above */ 126 | controller.hears(['hello'],'direct_message,direct_mention,mention',hear_intent, function(bot, message) { 127 | 128 | bot.api.reactions.add({ 129 | timestamp: message.ts, 130 | channel: message.channel, 131 | name: 'robot_face', 132 | },function(err, res) { 133 | if (err) { 134 | bot.botkit.log('Failed to add emoji reaction :(',err); 135 | } 136 | }); 137 | 138 | 139 | controller.storage.users.get(message.user,function(err, user) { 140 | if (user && user.name) { 141 | bot.reply(message, 'Hello ' + user.name + '!!'); 142 | } else { 143 | bot.reply(message, 'Hello.'); 144 | } 145 | }); 146 | }); 147 | 148 | controller.hears(['call me (.*)','my name is (.*)'],'direct_message,direct_mention,mention',function(bot, message) { 149 | 150 | // the name will be stored in the message.match field 151 | var name = message.match[1]; 152 | controller.storage.users.get(message.user,function(err, user) { 153 | if (!user) { 154 | user = { 155 | id: message.user, 156 | }; 157 | } 158 | user.name = name; 159 | controller.storage.users.save(user,function(err, id) { 160 | bot.reply(message,'Got it. I will call you ' + user.name + ' from now on.'); 161 | }); 162 | }); 163 | }); 164 | 165 | controller.hears(['what is my name','who am i'],'direct_message,direct_mention,mention',function(bot, message) { 166 | 167 | controller.storage.users.get(message.user,function(err, user) { 168 | if (user && user.name) { 169 | bot.reply(message,'Your name is ' + user.name); 170 | } else { 171 | bot.reply(message,'I don\'t know yet!'); 172 | } 173 | }); 174 | }); 175 | 176 | 177 | controller.hears(['shutdown'],'direct_message,direct_mention,mention',function(bot, message) { 178 | 179 | bot.startConversation(message,function(err, convo) { 180 | 181 | convo.ask('Are you sure you want me to shutdown?',[ 182 | { 183 | pattern: bot.utterances.yes, 184 | callback: function(response, convo) { 185 | convo.say('Bye!'); 186 | convo.next(); 187 | setTimeout(function() { 188 | process.exit(); 189 | },3000); 190 | } 191 | }, 192 | { 193 | pattern: bot.utterances.no, 194 | default: true, 195 | callback: function(response, convo) { 196 | convo.say('*Phew!*'); 197 | convo.next(); 198 | } 199 | } 200 | ]); 201 | }); 202 | }); 203 | 204 | 205 | controller.hears(['uptime','identify yourself','who are you','what is your name'],'direct_message,direct_mention,mention',function(bot, message) { 206 | 207 | var hostname = os.hostname(); 208 | var uptime = formatUptime(process.uptime()); 209 | 210 | bot.reply(message,':robot_face: I am a bot named <@' + bot.identity.name + '>. I have been running for ' + uptime + ' on ' + hostname + '.'); 211 | 212 | }); 213 | 214 | function formatUptime(uptime) { 215 | var unit = 'second'; 216 | if (uptime > 60) { 217 | uptime = uptime / 60; 218 | unit = 'minute'; 219 | } 220 | if (uptime > 60) { 221 | uptime = uptime / 60; 222 | unit = 'hour'; 223 | } 224 | if (uptime != 1) { 225 | unit = unit + 's'; 226 | } 227 | 228 | uptime = uptime + ' ' + unit; 229 | return uptime; 230 | } 231 | -------------------------------------------------------------------------------- /slack_bot.js: -------------------------------------------------------------------------------- 1 | /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | ______ ______ ______ __ __ __ ______ 3 | /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ 4 | \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ 5 | \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ 6 | \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ 7 | 8 | 9 | This is a sample Slack bot built with Botkit. 10 | 11 | This bot demonstrates many of the core features of Botkit: 12 | 13 | * Connect to Slack using the real time API 14 | * Receive messages based on "spoken" patterns 15 | * Reply to messages 16 | * Use the conversation system to ask questions 17 | * Use the built in storage system to store and retrieve information 18 | for a user. 19 | 20 | # RUN THE BOT: 21 | 22 | Get a Bot token from Slack: 23 | 24 | -> http://my.slack.com/services/new/bot 25 | 26 | Run your bot from the command line: 27 | 28 | token= node slack_bot.js 29 | 30 | # USE THE BOT: 31 | 32 | Find your bot inside Slack to send it a direct message. 33 | 34 | Say: "Hello" 35 | 36 | The bot will reply "Hello!" 37 | 38 | Say: "who are you?" 39 | 40 | The bot will tell you its name, where it running, and for how long. 41 | 42 | Say: "Call me " 43 | 44 | Tell the bot your nickname. Now you are friends. 45 | 46 | Say: "who am I?" 47 | 48 | The bot will tell you your nickname, if it knows one for you. 49 | 50 | Say: "shutdown" 51 | 52 | The bot will ask if you are sure, and then shut itself down. 53 | 54 | Make sure to invite your bot into other channels using /invite @! 55 | 56 | # EXTEND THE BOT: 57 | 58 | Botkit has many features for building cool and useful bots! 59 | 60 | Read all about it here: 61 | 62 | -> http://howdy.ai/botkit 63 | 64 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ 65 | 66 | 67 | if (!process.env.token) { 68 | console.log('Error: Specify token in environment'); 69 | process.exit(1); 70 | } 71 | 72 | var Botkit = require('./lib/Botkit.js'); 73 | var os = require('os'); 74 | 75 | var controller = Botkit.slackbot({ 76 | debug: true, 77 | }); 78 | 79 | var bot = controller.spawn({ 80 | token: process.env.token 81 | }).startRTM(); 82 | 83 | 84 | controller.hears(['hello', 'hi'], 'direct_message,direct_mention,mention', function(bot, message) { 85 | 86 | bot.api.reactions.add({ 87 | timestamp: message.ts, 88 | channel: message.channel, 89 | name: 'robot_face', 90 | }, function(err, res) { 91 | if (err) { 92 | bot.botkit.log('Failed to add emoji reaction :(', err); 93 | } 94 | }); 95 | 96 | 97 | controller.storage.users.get(message.user, function(err, user) { 98 | if (user && user.name) { 99 | bot.reply(message, 'Hello ' + user.name + '!!'); 100 | } else { 101 | bot.reply(message, 'Hello.'); 102 | } 103 | }); 104 | }); 105 | 106 | controller.hears(['call me (.*)', 'my name is (.*)'], 'direct_message,direct_mention,mention', function(bot, message) { 107 | var name = message.match[1]; 108 | controller.storage.users.get(message.user, function(err, user) { 109 | if (!user) { 110 | user = { 111 | id: message.user, 112 | }; 113 | } 114 | user.name = name; 115 | controller.storage.users.save(user, function(err, id) { 116 | bot.reply(message, 'Got it. I will call you ' + user.name + ' from now on.'); 117 | }); 118 | }); 119 | }); 120 | 121 | controller.hears(['what is my name', 'who am i'], 'direct_message,direct_mention,mention', function(bot, message) { 122 | 123 | controller.storage.users.get(message.user, function(err, user) { 124 | if (user && user.name) { 125 | bot.reply(message, 'Your name is ' + user.name); 126 | } else { 127 | bot.startConversation(message, function(err, convo) { 128 | if (!err) { 129 | convo.say('I do not know your name yet!'); 130 | convo.ask('What should I call you?', function(response, convo) { 131 | convo.ask('You want me to call you `' + response.text + '`?', [ 132 | { 133 | pattern: 'yes', 134 | callback: function(response, convo) { 135 | // since no further messages are queued after this, 136 | // the conversation will end naturally with status == 'completed' 137 | convo.next(); 138 | } 139 | }, 140 | { 141 | pattern: 'no', 142 | callback: function(response, convo) { 143 | // stop the conversation. this will cause it to end with status == 'stopped' 144 | convo.stop(); 145 | } 146 | }, 147 | { 148 | default: true, 149 | callback: function(response, convo) { 150 | convo.repeat(); 151 | convo.next(); 152 | } 153 | } 154 | ]); 155 | 156 | convo.next(); 157 | 158 | }, {'key': 'nickname'}); // store the results in a field called nickname 159 | 160 | convo.on('end', function(convo) { 161 | if (convo.status == 'completed') { 162 | bot.reply(message, 'OK! I will update my dossier...'); 163 | 164 | controller.storage.users.get(message.user, function(err, user) { 165 | if (!user) { 166 | user = { 167 | id: message.user, 168 | }; 169 | } 170 | user.name = convo.extractResponse('nickname'); 171 | controller.storage.users.save(user, function(err, id) { 172 | bot.reply(message, 'Got it. I will call you ' + user.name + ' from now on.'); 173 | }); 174 | }); 175 | 176 | 177 | 178 | } else { 179 | // this happens if the conversation ended prematurely for some reason 180 | bot.reply(message, 'OK, nevermind!'); 181 | } 182 | }); 183 | } 184 | }); 185 | } 186 | }); 187 | }); 188 | 189 | 190 | controller.hears(['shutdown'], 'direct_message,direct_mention,mention', function(bot, message) { 191 | 192 | bot.startConversation(message, function(err, convo) { 193 | 194 | convo.ask('Are you sure you want me to shutdown?', [ 195 | { 196 | pattern: bot.utterances.yes, 197 | callback: function(response, convo) { 198 | convo.say('Bye!'); 199 | convo.next(); 200 | setTimeout(function() { 201 | process.exit(); 202 | }, 3000); 203 | } 204 | }, 205 | { 206 | pattern: bot.utterances.no, 207 | default: true, 208 | callback: function(response, convo) { 209 | convo.say('*Phew!*'); 210 | convo.next(); 211 | } 212 | } 213 | ]); 214 | }); 215 | }); 216 | 217 | 218 | controller.hears(['uptime', 'identify yourself', 'who are you', 'what is your name'], 219 | 'direct_message,direct_mention,mention', function(bot, message) { 220 | 221 | var hostname = os.hostname(); 222 | var uptime = formatUptime(process.uptime()); 223 | 224 | bot.reply(message, 225 | ':robot_face: I am a bot named <@' + bot.identity.name + 226 | '>. I have been running for ' + uptime + ' on ' + hostname + '.'); 227 | 228 | }); 229 | 230 | function formatUptime(uptime) { 231 | var unit = 'second'; 232 | if (uptime > 60) { 233 | uptime = uptime / 60; 234 | unit = 'minute'; 235 | } 236 | if (uptime > 60) { 237 | uptime = uptime / 60; 238 | unit = 'hour'; 239 | } 240 | if (uptime != 1) { 241 | unit = unit + 's'; 242 | } 243 | 244 | uptime = uptime + ' ' + unit; 245 | return uptime; 246 | } 247 | -------------------------------------------------------------------------------- /readme-facebook.md: -------------------------------------------------------------------------------- 1 | # Botkit and Facebook 2 | 3 | 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. 4 | 5 | 6 | Botkit features a comprehensive set of tools 7 | to deal with [Facebooks's Messenger platform](https://developers.facebook.com/docs/messenger-platform/implementation), and allows 8 | 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). 9 | 10 | This document covers the Facebook-specific implementation details only. [Start here](readme.md) if you want to learn about to develop with Botkit. 11 | 12 | Table of Contents 13 | 14 | * [Getting Started](#getting-started) 15 | * [Facebook-specific Events](#facebook-specific-events) 16 | * [Working with Facebook Webhooks](#working-with-facebook-messenger) 17 | * [Using Structured Messages and Postbacks](#using-structured-messages-and-postbacks) 18 | 19 | ## Getting Started 20 | 21 | 1) Install Botkit [more info here](readme.md#installation) 22 | 23 | 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. 24 | 25 | 3) [Get a page access token for your app](https://developers.facebook.com/docs/messenger-platform/implementation#page_access_token) 26 | 27 | Copy this token, you'll need it! 28 | 29 | 4) Define your own "verify token" - this a string that you control that Facebook will use to verify your web hook endpoint. 30 | 31 | 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. 32 | 33 | ``` 34 | page_token= verify_token= node facebook_bot.js [--lt [--ltsubdomain CUSTOM_SUBDOMAIN]] 35 | ``` 36 | 37 | 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! 38 | 39 | 7) Your bot should be online! Within Facebook, find your page, and click the "Message" button in the header. 40 | 41 | Try: 42 | * who are you? 43 | * call me Bob 44 | * shutdown 45 | 46 | 47 | ### Things to note 48 | 49 | 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. 50 | 51 | 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. 52 | 53 | ## Facebook-specific Events 54 | 55 | Once connected to Facebook, bots receive a constant stream of events. 56 | 57 | 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. 58 | 59 | | Event | Description 60 | |--- |--- 61 | | message_received | a message was received by the bot 62 | | facebook_postback | user clicked a button in an attachment and triggered a webhook postback 63 | | message_delivered | a confirmation from Facebook that a message has been received 64 | | facebook_optin | a user has clicked the [Send-to-Messenger plugin](https://developers.facebook.com/docs/messenger-platform/implementation#send_to_messenger_plugin) 65 | 66 | All incoming events will contain the fields `user` and `channel`, both of which represent the Facebook user's ID, and a `timestamp` field. 67 | 68 | `message_received` events will also contain either a `text` field or an `attachment` field. 69 | 70 | `facebook_postback` events will contain a `payload` field. 71 | 72 | More information about the data found in these fields can be found [here](https://developers.facebook.com/docs/messenger-platform/webhook-reference). 73 | 74 | ## Working with Facebook Messenger 75 | 76 | 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. 77 | 78 | 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. 79 | 80 | Here is the complete code for a basic Facebook bot: 81 | 82 | ```javascript 83 | var Botkit = require('botkit'); 84 | var controller = Botkit.facebookbot({ 85 | access_token: process.env.access_token, 86 | verify_token: process.env.verify_token, 87 | }) 88 | 89 | var bot = controller.spawn({ 90 | }); 91 | 92 | // if you are already using Express, you can use your own server instance... 93 | // see "Use BotKit with an Express web server" 94 | controller.setupWebserver(process.env.port,function(err,webserver) { 95 | controller.createWebhookEndpoints(controller.webserver, bot, function() { 96 | console.log('This bot is online!!!'); 97 | }); 98 | }); 99 | 100 | // this is triggered when a user clicks the send-to-messenger plugin 101 | controller.on('facebook_optin', function(bot, message) { 102 | 103 | bot.reply(message, 'Welcome to my app!'); 104 | 105 | }); 106 | 107 | // user said hello 108 | controller.hears(['hello'], 'message_received', function(bot, message) { 109 | 110 | bot.reply(message, 'Hey there.'); 111 | 112 | }); 113 | 114 | controller.hears(['cookies'], 'message_received', function(bot, message) { 115 | 116 | bot.startConversation(message, function(err, convo) { 117 | 118 | convo.say('Did someone say cookies!?!!'); 119 | convo.ask('What is your favorite type of cookie?', function(response, convo) { 120 | convo.say('Golly, I love ' + response.text + ' too!!!'); 121 | convo.next(); 122 | }); 123 | }); 124 | }); 125 | ``` 126 | 127 | 128 | #### controller.setupWebserver() 129 | | Argument | Description 130 | |--- |--- 131 | | port | port for webserver 132 | | callback | callback function 133 | 134 | Setup an [Express webserver](http://expressjs.com/en/index.html) for 135 | use with `createWebhookEndpoints()` 136 | 137 | If you need more than a simple webserver to receive webhooks, 138 | you should by all means create your own Express webserver! 139 | 140 | The callback function receives the Express object as a parameter, 141 | which may be used to add further web server routes. 142 | 143 | #### controller.createWebhookEndpoints() 144 | 145 | This function configures the route `https://_your_server_/facebook/receive` 146 | to receive webhooks from Facebook. 147 | 148 | This url should be used when configuring Facebook. 149 | 150 | ## Using Structured Messages and Postbacks 151 | 152 | You can attach little bubbles 153 | 154 | And in those bubbles can be buttons 155 | and when a user clicks the button, it sends a postback with the value. 156 | 157 | ```javascript 158 | controller.hears('test', 'message_received', function(bot, message) { 159 | 160 | var attachment = { 161 | 'type':'template', 162 | 'payload':{ 163 | 'template_type':'generic', 164 | 'elements':[ 165 | { 166 | 'title':'Chocolate Cookie', 167 | 'image_url':'http://cookies.com/cookie.png', 168 | 'subtitle':'A delicious chocolate cookie', 169 | 'buttons':[ 170 | { 171 | 'type':'postback', 172 | 'title':'Eat Cookie', 173 | 'payload':'chocolate' 174 | } 175 | ] 176 | }, 177 | ] 178 | } 179 | }; 180 | 181 | bot.reply(message, { 182 | attachment: attachment, 183 | }); 184 | 185 | }); 186 | 187 | controller.on('facebook_postback', function(bot, message) { 188 | 189 | if (message.payload == 'chocolate') { 190 | bot.reply(message, 'You ate the chocolate cookie!') 191 | } 192 | 193 | }); 194 | ``` 195 | -------------------------------------------------------------------------------- /lib/Facebook.js: -------------------------------------------------------------------------------- 1 | var Botkit = require(__dirname + '/CoreBot.js'); 2 | var request = require('request'); 3 | var express = require('express'); 4 | var bodyParser = require('body-parser'); 5 | 6 | function Facebookbot(configuration) { 7 | 8 | // Create a core botkit bot 9 | var facebook_botkit = Botkit(configuration || {}); 10 | 11 | // customize the bot definition, which will be used when new connections 12 | // spawn! 13 | facebook_botkit.defineBot(function(botkit, config) { 14 | 15 | var bot = { 16 | botkit: botkit, 17 | config: config || {}, 18 | utterances: botkit.utterances, 19 | }; 20 | 21 | bot.startConversation = function(message, cb) { 22 | botkit.startConversation(this, message, cb); 23 | }; 24 | 25 | bot.send = function(message, cb) { 26 | 27 | var facebook_message = { 28 | recipient: {}, 29 | message: {} 30 | }; 31 | 32 | if (typeof(message.channel) == 'string' && message.channel.match(/\+\d+\(\d\d\d\)\d\d\d\-\d\d\d\d/)) { 33 | facebook_message.recipient.phone_number = message.channel; 34 | } else { 35 | facebook_message.recipient.id = message.channel; 36 | } 37 | 38 | if (message.text) { 39 | facebook_message.message.text = message.text; 40 | } 41 | 42 | if (message.attachment) { 43 | facebook_message.message.attachment = message.attachment; 44 | } 45 | 46 | request.post('https://graph.facebook.com/me/messages?access_token=' + configuration.access_token, 47 | function(err, res, body) { 48 | if (err) { 49 | botkit.debug('WEBHOOK ERROR', err); 50 | return cb && cb(err); 51 | } 52 | 53 | try { 54 | 55 | var json = JSON.parse(body); 56 | 57 | } catch (err) { 58 | 59 | botkit.debug('JSON Parse error: ', err); 60 | return cb && cb(err); 61 | 62 | } 63 | 64 | if (json.error) { 65 | botkit.debug('API ERROR', json.error); 66 | return cb && cb(json.error.message); 67 | } 68 | 69 | botkit.debug('WEBHOOK SUCCESS', body); 70 | cb && cb(null, body); 71 | }).form(facebook_message); 72 | }; 73 | 74 | bot.reply = function(src, resp, cb) { 75 | var msg = {}; 76 | 77 | if (typeof(resp) == 'string') { 78 | msg.text = resp; 79 | } else { 80 | msg = resp; 81 | } 82 | 83 | msg.channel = src.channel; 84 | 85 | bot.say(msg, cb); 86 | }; 87 | 88 | bot.findConversation = function(message, cb) { 89 | botkit.debug('CUSTOM FIND CONVO', message.user, message.channel); 90 | for (var t = 0; t < botkit.tasks.length; t++) { 91 | for (var c = 0; c < botkit.tasks[t].convos.length; c++) { 92 | if ( 93 | botkit.tasks[t].convos[c].isActive() && 94 | botkit.tasks[t].convos[c].source_message.user == message.user 95 | ) { 96 | botkit.debug('FOUND EXISTING CONVO!'); 97 | cb(botkit.tasks[t].convos[c]); 98 | return; 99 | } 100 | } 101 | } 102 | 103 | cb(); 104 | }; 105 | 106 | return bot; 107 | 108 | }); 109 | 110 | 111 | // set up a web route for receiving outgoing webhooks and/or slash commands 112 | 113 | facebook_botkit.createWebhookEndpoints = function(webserver, bot, cb) { 114 | 115 | facebook_botkit.log( 116 | '** Serving webhook endpoints for Slash commands and outgoing ' + 117 | 'webhooks at: http://MY_HOST:' + facebook_botkit.config.port + '/facebook/receive'); 118 | webserver.post('/facebook/receive', function(req, res) { 119 | 120 | facebook_botkit.debug('GOT A MESSAGE HOOK'); 121 | var obj = req.body; 122 | if (obj.entry) { 123 | for (var e = 0; e < obj.entry.length; e++) { 124 | for (var m = 0; m < obj.entry[e].messaging.length; m++) { 125 | var facebook_message = obj.entry[e].messaging[m]; 126 | if (facebook_message.message) { 127 | 128 | var message = { 129 | text: facebook_message.message.text, 130 | user: facebook_message.sender.id, 131 | channel: facebook_message.sender.id, 132 | timestamp: facebook_message.timestamp, 133 | seq: facebook_message.message.seq, 134 | mid: facebook_message.message.mid, 135 | attachments: facebook_message.message.attachments, 136 | }; 137 | 138 | facebook_botkit.receiveMessage(bot, message); 139 | } else if (facebook_message.postback) { 140 | 141 | // trigger BOTH a facebook_postback event 142 | // and a normal message received event. 143 | // this allows developers to receive postbacks as part of a conversation. 144 | var message = { 145 | payload: facebook_message.postback.payload, 146 | user: facebook_message.sender.id, 147 | channel: facebook_message.sender.id, 148 | timestamp: facebook_message.timestamp, 149 | }; 150 | 151 | facebook_botkit.trigger('facebook_postback', [bot, message]); 152 | 153 | var message = { 154 | text: facebook_message.postback.payload, 155 | user: facebook_message.sender.id, 156 | channel: facebook_message.sender.id, 157 | timestamp: facebook_message.timestamp, 158 | }; 159 | 160 | facebook_botkit.receiveMessage(bot, message); 161 | 162 | } else if (facebook_message.optin) { 163 | 164 | var message = { 165 | optin: facebook_message.optin, 166 | user: facebook_message.sender.id, 167 | channel: facebook_message.sender.id, 168 | timestamp: facebook_message.timestamp, 169 | }; 170 | 171 | facebook_botkit.trigger('facebook_optin', [bot, message]); 172 | } else if (facebook_message.delivery) { 173 | 174 | var message = { 175 | optin: facebook_message.delivery, 176 | user: facebook_message.sender.id, 177 | channel: facebook_message.sender.id, 178 | timestamp: facebook_message.timestamp, 179 | }; 180 | 181 | facebook_botkit.trigger('message_delivered', [bot, message]); 182 | 183 | } else { 184 | botkit.log('Got an unexpected message from Facebook: ', facebook_message); 185 | } 186 | } 187 | } 188 | } 189 | res.send('ok'); 190 | }); 191 | 192 | webserver.get('/facebook/receive', function(req, res) { 193 | console.log(req.query); 194 | if (req.query['hub.mode'] == 'subscribe') { 195 | if (req.query['hub.verify_token'] == configuration.verify_token) { 196 | res.send(req.query['hub.challenge']); 197 | } else { 198 | res.send('OK'); 199 | } 200 | } 201 | }); 202 | 203 | if (cb) { 204 | cb(); 205 | } 206 | 207 | return facebook_botkit; 208 | }; 209 | 210 | facebook_botkit.setupWebserver = function(port, cb) { 211 | 212 | if (!port) { 213 | throw new Error('Cannot start webserver without a port'); 214 | } 215 | if (isNaN(port)) { 216 | throw new Error('Specified port is not a valid number'); 217 | } 218 | 219 | facebook_botkit.config.port = port; 220 | 221 | facebook_botkit.webserver = express(); 222 | facebook_botkit.webserver.use(bodyParser.json()); 223 | facebook_botkit.webserver.use(bodyParser.urlencoded({ extended: true })); 224 | facebook_botkit.webserver.use(express.static(__dirname + '/public')); 225 | 226 | var server = facebook_botkit.webserver.listen( 227 | facebook_botkit.config.port, 228 | function() { 229 | facebook_botkit.log('** Starting webserver on port ' + 230 | facebook_botkit.config.port); 231 | if (cb) { cb(null, facebook_botkit.webserver); } 232 | }); 233 | 234 | 235 | request.post('https://graph.facebook.com/me/subscribed_apps?access_token=' + configuration.access_token, 236 | function(err, res, body) { 237 | if (err) { 238 | facebook_botkit.log('Could not subscribe to page messages'); 239 | } else { 240 | facebook_botkit.debug('Successfully subscribed to Facebook events:', body); 241 | facebook_botkit.startTicking(); 242 | } 243 | }); 244 | 245 | return facebook_botkit; 246 | 247 | }; 248 | 249 | return facebook_botkit; 250 | }; 251 | 252 | module.exports = Facebookbot; 253 | -------------------------------------------------------------------------------- /lib/Slack_web_api.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | 3 | module.exports = function(bot, config) { 4 | 5 | // create a nice wrapper for the Slack API 6 | var slack_api = { 7 | api_url: 'https://slack.com/api/', 8 | // this is a simple function used to call the slack web API 9 | callAPI: function(command, options, cb) { 10 | bot.log('** API CALL: ' + slack_api.api_url + command); 11 | if (!options.token) { 12 | options.token = config.token; 13 | } 14 | bot.debug(command, options); 15 | request.post(this.api_url + command, function(error, response, body) { 16 | bot.debug('Got response', error, body); 17 | if (!error && response.statusCode == 200) { 18 | var json; 19 | try { 20 | json = JSON.parse(body); 21 | } catch (err) { 22 | if (cb) return cb(err || 'Invalid JSON'); 23 | return; 24 | } 25 | 26 | if (json.ok) { 27 | if (cb) cb(null, json); 28 | } else { 29 | if (cb) cb(json.error, json); 30 | } 31 | } else { 32 | if (cb) cb(error || 'Invalid response'); 33 | } 34 | }).form(options); 35 | }, 36 | callAPIWithoutToken: function(command, options, cb) { 37 | bot.log('** API CALL: ' + slack_api.api_url + command); 38 | if (!options.client_id) { 39 | options.client_id = bot.config.clientId; 40 | } 41 | if (!options.client_secret) { 42 | options.client_secret = bot.config.clientSecret; 43 | } 44 | if (!options.redirect_uri) { 45 | options.redirect_uri = bot.config.redirectUri; 46 | } 47 | request.post(this.api_url + command, function(error, response, body) { 48 | bot.debug('Got response', error, body); 49 | if (!error && response.statusCode == 200) { 50 | var json; 51 | try { 52 | json = JSON.parse(body); 53 | } catch (err) { 54 | if (cb) return cb(err || 'Invalid JSON'); 55 | return; 56 | } 57 | 58 | if (json.ok) { 59 | if (cb) cb(null, json); 60 | } else { 61 | if (cb) cb(json.error, json); 62 | } 63 | } else { 64 | if (cb) cb(error || 'Invalid response'); 65 | } 66 | }).form(options); 67 | }, 68 | auth: { 69 | test: function(options, cb) { 70 | slack_api.callAPI('auth.test', options, cb); 71 | } 72 | }, 73 | oauth: { 74 | access: function(options, cb) { 75 | slack_api.callAPIWithoutToken('oauth.access', options, cb); 76 | } 77 | }, 78 | channels: { 79 | archive: function(options, cb) { 80 | slack_api.callAPI('channels.archive', options, cb); 81 | }, 82 | create: function(options, cb) { 83 | slack_api.callAPI('channels.create', options, cb); 84 | }, 85 | history: function(options, cb) { 86 | slack_api.callAPI('channels.history', options, cb); 87 | }, 88 | info: function(options, cb) { 89 | slack_api.callAPI('channels.info', options, cb); 90 | }, 91 | invite: function(options, cb) { 92 | slack_api.callAPI('channels.invite', options, cb); 93 | }, 94 | join: function(options, cb) { 95 | slack_api.callAPI('channels.join', options, cb); 96 | }, 97 | kick: function(options, cb) { 98 | slack_api.callAPI('channels.kick', options, cb); 99 | }, 100 | leave: function(options, cb) { 101 | slack_api.callAPI('channels.leave', options, cb); 102 | }, 103 | list: function(options, cb) { 104 | slack_api.callAPI('channels.list', options, cb); 105 | }, 106 | mark: function(options, cb) { 107 | slack_api.callAPI('channels.mark', options, cb); 108 | }, 109 | rename: function(options, cb) { 110 | slack_api.callAPI('channels.rename', options, cb); 111 | }, 112 | setPurpose: function(options, cb) { 113 | slack_api.callAPI('channels.setPurpose', options, cb); 114 | }, 115 | setTopic: function(options, cb) { 116 | slack_api.callAPI('channels.setTopic', options, cb); 117 | }, 118 | unarchive: function(options, cb) { 119 | slack_api.callAPI('channels.unarchive', options, cb); 120 | } 121 | }, 122 | chat: { 123 | delete: function(options, cb) { 124 | slack_api.callAPI('chat.delete', options, cb); 125 | }, 126 | postMessage: function(options, cb) { 127 | if (options.attachments && typeof(options.attachments) != 'string') { 128 | options.attachments = JSON.stringify(options.attachments); 129 | } 130 | slack_api.callAPI('chat.postMessage', options, cb); 131 | }, 132 | update: function(options, cb) { 133 | slack_api.callAPI('chat.update', options, cb); 134 | } 135 | }, 136 | emoji: { 137 | list: function(options, cb) { 138 | slack_api.callAPI('emoji.list', options, cb); 139 | } 140 | }, 141 | files: { 142 | delete: function(options, cb) { 143 | slack_api.callAPI('files.delete', options, cb); 144 | }, 145 | info: function(options, cb) { 146 | slack_api.callAPI('files.info', options, cb); 147 | }, 148 | list: function(options, cb) { 149 | slack_api.callAPI('files.list', options, cb); 150 | }, 151 | upload: function(options, cb) { 152 | slack_api.callAPI('files.upload', options, cb); 153 | }, 154 | }, 155 | groups: { 156 | archive: function(options, cb) { 157 | slack_api.callAPI('groups.archive', options, cb); 158 | }, 159 | close: function(options, cb) { 160 | slack_api.callAPI('groups.close', options, cb); 161 | }, 162 | create: function(options, cb) { 163 | slack_api.callAPI('groups.create', options, cb); 164 | }, 165 | createChild: function(options, cb) { 166 | slack_api.callAPI('groups.createChild', options, cb); 167 | }, 168 | history: function(options, cb) { 169 | slack_api.callAPI('groups.history', options, cb); 170 | }, 171 | info: function(options, cb) { 172 | slack_api.callAPI('groups.info', options, cb); 173 | }, 174 | invite: function(options, cb) { 175 | slack_api.callAPI('groups.invite', options, cb); 176 | }, 177 | kick: function(options, cb) { 178 | slack_api.callAPI('groups.kick', options, cb); 179 | }, 180 | leave: function(options, cb) { 181 | slack_api.callAPI('groups.leave', options, cb); 182 | }, 183 | list: function(options, cb) { 184 | slack_api.callAPI('groups.list', options, cb); 185 | }, 186 | mark: function(options, cb) { 187 | slack_api.callAPI('groups.mark', options, cb); 188 | }, 189 | open: function(options, cb) { 190 | slack_api.callAPI('groups.open', options, cb); 191 | }, 192 | rename: function(options, cb) { 193 | slack_api.callAPI('groups.rename', options, cb); 194 | }, 195 | setPurpose: function(options, cb) { 196 | slack_api.callAPI('groups.setPurpose', options, cb); 197 | }, 198 | setTopic: function(options, cb) { 199 | slack_api.callAPI('groups.setTopic', options, cb); 200 | }, 201 | unarchive: function(options, cb) { 202 | slack_api.callAPI('groups.unarchive', options, cb); 203 | }, 204 | }, 205 | im: { 206 | close: function(options, cb) { 207 | slack_api.callAPI('im.close', options, cb); 208 | }, 209 | history: function(options, cb) { 210 | slack_api.callAPI('im.history', options, cb); 211 | }, 212 | list: function(options, cb) { 213 | slack_api.callAPI('im.list', options, cb); 214 | }, 215 | mark: function(options, cb) { 216 | slack_api.callAPI('im.mark', options, cb); 217 | }, 218 | open: function(options, cb) { 219 | slack_api.callAPI('im.open', options, cb); 220 | } 221 | }, 222 | mpim: { 223 | close: function(options, cb) { 224 | slack_api.callAPI('mpim.close', options, cb); 225 | }, 226 | history: function(options, cb) { 227 | slack_api.callAPI('mpim.history', options, cb); 228 | }, 229 | list: function(options, cb) { 230 | slack_api.callAPI('mpim.list', options, cb); 231 | }, 232 | mark: function(options, cb) { 233 | slack_api.callAPI('mpim.mark', options, cb); 234 | }, 235 | open: function(options, cb) { 236 | slack_api.callAPI('mpim.open', options, cb); 237 | } 238 | }, 239 | pins: { 240 | add: function(options, cb) { 241 | slack_api.callAPI('pins.add', options, cb); 242 | }, 243 | list: function(options, cb) { 244 | slack_api.callAPI('pins.list', options, cb); 245 | }, 246 | remove: function(options, cb) { 247 | slack_api.callAPI('pins.remove', options, cb); 248 | } 249 | }, 250 | reactions: { 251 | add: function(options, cb) { 252 | slack_api.callAPI('reactions.add', options, cb); 253 | }, 254 | get: function(options, cb) { 255 | slack_api.callAPI('reactions.get', options, cb); 256 | }, 257 | list: function(options, cb) { 258 | slack_api.callAPI('reactions.list', options, cb); 259 | }, 260 | remove: function(options, cb) { 261 | slack_api.callAPI('reactions.remove', options, cb); 262 | }, 263 | }, 264 | rtm: { 265 | start: function(options, cb) { 266 | slack_api.callAPI('rtm.start', options, cb); 267 | }, 268 | }, 269 | search: { 270 | all: function(options, cb) { 271 | slack_api.callAPI('search.all', options, cb); 272 | }, 273 | files: function(options, cb) { 274 | slack_api.callAPI('search.files', options, cb); 275 | }, 276 | messages: function(options, cb) { 277 | slack_api.callAPI('search.messages', options, cb); 278 | }, 279 | }, 280 | stars: { 281 | list: function(options, cb) { 282 | slack_api.callAPI('stars.list', options, cb); 283 | }, 284 | }, 285 | team: { 286 | accessLogs: function(options, cb) { 287 | slack_api.callAPI('team.accessLogs', options, cb); 288 | }, 289 | info: function(options, cb) { 290 | slack_api.callAPI('team.info', options, cb); 291 | }, 292 | }, 293 | users: { 294 | getPresence: function(options, cb) { 295 | slack_api.callAPI('users.getPresence', options, cb); 296 | }, 297 | info: function(options, cb) { 298 | slack_api.callAPI('users.info', options, cb); 299 | }, 300 | list: function(options, cb) { 301 | slack_api.callAPI('users.list', options, cb); 302 | }, 303 | setActive: function(options, cb) { 304 | slack_api.callAPI('users.setActive', options, cb); 305 | }, 306 | setPresence: function(options, cb) { 307 | slack_api.callAPI('users.setPresence', options, cb); 308 | }, 309 | } 310 | }; 311 | 312 | return slack_api; 313 | 314 | }; 315 | -------------------------------------------------------------------------------- /facebook_bot.js: -------------------------------------------------------------------------------- 1 | /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | ______ ______ ______ __ __ __ ______ 3 | /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ 4 | \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ 5 | \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ 6 | \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ 7 | 8 | 9 | This is a sample Facebook bot built with Botkit. 10 | 11 | This bot demonstrates many of the core features of Botkit: 12 | 13 | * Connect to Facebook's Messenger APIs 14 | * Receive messages based on "spoken" patterns 15 | * Reply to messages 16 | * Use the conversation system to ask questions 17 | * Use the built in storage system to store and retrieve information 18 | for a user. 19 | 20 | # RUN THE BOT: 21 | 22 | Follow the instructions here to set up your Facebook app and page: 23 | 24 | -> https://developers.facebook.com/docs/messenger-platform/implementation 25 | 26 | Run your bot from the command line: 27 | 28 | page_token= verify_token= node facebook_bot.js [--lt [--ltsubdomain LOCALTUNNEL_SUBDOMAIN]] 29 | 30 | Use the --lt option to make your bot available on the web through localtunnel.me. 31 | 32 | # USE THE BOT: 33 | 34 | Find your bot inside Facebook to send it a direct message. 35 | 36 | Say: "Hello" 37 | 38 | The bot will reply "Hello!" 39 | 40 | Say: "who are you?" 41 | 42 | The bot will tell you its name, where it running, and for how long. 43 | 44 | Say: "Call me " 45 | 46 | Tell the bot your nickname. Now you are friends. 47 | 48 | Say: "who am I?" 49 | 50 | The bot will tell you your nickname, if it knows one for you. 51 | 52 | Say: "shutdown" 53 | 54 | The bot will ask if you are sure, and then shut itself down. 55 | 56 | Make sure to invite your bot into other channels using /invite @! 57 | 58 | # EXTEND THE BOT: 59 | 60 | Botkit has many features for building cool and useful bots! 61 | 62 | Read all about it here: 63 | 64 | -> http://howdy.ai/botkit 65 | 66 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ 67 | 68 | 69 | if (!process.env.page_token) { 70 | console.log('Error: Specify page_token in environment'); 71 | process.exit(1); 72 | } 73 | 74 | if (!process.env.verify_token) { 75 | console.log('Error: Specify verify_token in environment'); 76 | process.exit(1); 77 | } 78 | 79 | var Botkit = require('./lib/Botkit.js'); 80 | var os = require('os'); 81 | var commandLineArgs = require('command-line-args'); 82 | var localtunnel = require('localtunnel'); 83 | 84 | const cli = commandLineArgs([ 85 | {name: 'lt', alias: 'l', args: 1, description: 'Use localtunnel.me to make your bot available on the web.', 86 | type: Boolean, defaultValue: false}, 87 | {name: 'ltsubdomain', alias: 's', args: 1, 88 | description: 'Custom subdomain for the localtunnel.me URL. This option can only be used together with --lt.', 89 | type: String, defaultValue: null}, 90 | ]); 91 | 92 | const ops = cli.parse(); 93 | if(ops.lt === false && ops.ltsubdomain !== null) { 94 | console.log("error: --ltsubdomain can only be used together with --lt."); 95 | process.exit(); 96 | } 97 | 98 | var controller = Botkit.facebookbot({ 99 | debug: true, 100 | access_token: process.env.page_token, 101 | verify_token: process.env.verify_token, 102 | }); 103 | 104 | var bot = controller.spawn({ 105 | }); 106 | 107 | controller.setupWebserver(process.env.port || 3000, function(err, webserver) { 108 | controller.createWebhookEndpoints(webserver, bot, function() { 109 | console.log('ONLINE!'); 110 | if(ops.lt) { 111 | var tunnel = localtunnel(process.env.port || 3000, {subdomain: ops.ltsubdomain}, function(err, tunnel) { 112 | if (err) { 113 | console.log(err); 114 | process.exit(); 115 | } 116 | console.log("Your bot is available on the web at the following URL: " + tunnel.url + '/facebook/receive'); 117 | }); 118 | 119 | tunnel.on('close', function() { 120 | console.log("Your bot is no longer available on the web at the localtunnnel.me URL."); 121 | process.exit(); 122 | }); 123 | } 124 | }); 125 | }); 126 | 127 | 128 | controller.hears(['hello', 'hi'], 'message_received', function(bot, message) { 129 | controller.storage.users.get(message.user, function(err, user) { 130 | if (user && user.name) { 131 | bot.reply(message, 'Hello ' + user.name + '!!'); 132 | } else { 133 | bot.reply(message, 'Hello.'); 134 | } 135 | }); 136 | }); 137 | 138 | 139 | controller.hears(['structured'], 'message_received', function(bot, message) { 140 | 141 | bot.startConversation(message, function(err, convo) { 142 | convo.ask({ 143 | attachment: { 144 | 'type': 'template', 145 | 'payload': { 146 | 'template_type': 'generic', 147 | 'elements': [ 148 | { 149 | 'title': 'Classic White T-Shirt', 150 | 'image_url': 'http://petersapparel.parseapp.com/img/item100-thumb.png', 151 | 'subtitle': 'Soft white cotton t-shirt is back in style', 152 | 'buttons': [ 153 | { 154 | 'type': 'web_url', 155 | 'url': 'https://petersapparel.parseapp.com/view_item?item_id=100', 156 | 'title': 'View Item' 157 | }, 158 | { 159 | 'type': 'web_url', 160 | 'url': 'https://petersapparel.parseapp.com/buy_item?item_id=100', 161 | 'title': 'Buy Item' 162 | }, 163 | { 164 | 'type': 'postback', 165 | 'title': 'Bookmark Item', 166 | 'payload': 'White T-Shirt' 167 | } 168 | ] 169 | }, 170 | { 171 | 'title': 'Classic Grey T-Shirt', 172 | 'image_url': 'http://petersapparel.parseapp.com/img/item101-thumb.png', 173 | 'subtitle': 'Soft gray cotton t-shirt is back in style', 174 | 'buttons': [ 175 | { 176 | 'type': 'web_url', 177 | 'url': 'https://petersapparel.parseapp.com/view_item?item_id=101', 178 | 'title': 'View Item' 179 | }, 180 | { 181 | 'type': 'web_url', 182 | 'url': 'https://petersapparel.parseapp.com/buy_item?item_id=101', 183 | 'title': 'Buy Item' 184 | }, 185 | { 186 | 'type': 'postback', 187 | 'title': 'Bookmark Item', 188 | 'payload': 'Grey T-Shirt' 189 | } 190 | ] 191 | } 192 | ] 193 | } 194 | } 195 | }, function(response, convo) { 196 | // whoa, I got the postback payload as a response to my convo.ask! 197 | convo.next(); 198 | }); 199 | }); 200 | }); 201 | 202 | controller.on('facebook_postback', function(bot, message) { 203 | 204 | bot.reply(message, 'Great Choice!!!! (' + message.payload + ')'); 205 | 206 | }); 207 | 208 | 209 | controller.hears(['call me (.*)', 'my name is (.*)'], 'message_received', function(bot, message) { 210 | var name = message.match[1]; 211 | controller.storage.users.get(message.user, function(err, user) { 212 | if (!user) { 213 | user = { 214 | id: message.user, 215 | }; 216 | } 217 | user.name = name; 218 | controller.storage.users.save(user, function(err, id) { 219 | bot.reply(message, 'Got it. I will call you ' + user.name + ' from now on.'); 220 | }); 221 | }); 222 | }); 223 | 224 | controller.hears(['what is my name', 'who am i'], 'message_received', function(bot, message) { 225 | controller.storage.users.get(message.user, function(err, user) { 226 | if (user && user.name) { 227 | bot.reply(message, 'Your name is ' + user.name); 228 | } else { 229 | bot.startConversation(message, function(err, convo) { 230 | if (!err) { 231 | convo.say('I do not know your name yet!'); 232 | convo.ask('What should I call you?', function(response, convo) { 233 | convo.ask('You want me to call you `' + response.text + '`?', [ 234 | { 235 | pattern: 'yes', 236 | callback: function(response, convo) { 237 | // since no further messages are queued after this, 238 | // the conversation will end naturally with status == 'completed' 239 | convo.next(); 240 | } 241 | }, 242 | { 243 | pattern: 'no', 244 | callback: function(response, convo) { 245 | // stop the conversation. this will cause it to end with status == 'stopped' 246 | convo.stop(); 247 | } 248 | }, 249 | { 250 | default: true, 251 | callback: function(response, convo) { 252 | convo.repeat(); 253 | convo.next(); 254 | } 255 | } 256 | ]); 257 | 258 | convo.next(); 259 | 260 | }, {'key': 'nickname'}); // store the results in a field called nickname 261 | 262 | convo.on('end', function(convo) { 263 | if (convo.status == 'completed') { 264 | bot.reply(message, 'OK! I will update my dossier...'); 265 | 266 | controller.storage.users.get(message.user, function(err, user) { 267 | if (!user) { 268 | user = { 269 | id: message.user, 270 | }; 271 | } 272 | user.name = convo.extractResponse('nickname'); 273 | controller.storage.users.save(user, function(err, id) { 274 | bot.reply(message, 'Got it. I will call you ' + user.name + ' from now on.'); 275 | }); 276 | }); 277 | 278 | 279 | 280 | } else { 281 | // this happens if the conversation ended prematurely for some reason 282 | bot.reply(message, 'OK, nevermind!'); 283 | } 284 | }); 285 | } 286 | }); 287 | } 288 | }); 289 | }); 290 | 291 | controller.hears(['shutdown'], 'message_received', function(bot, message) { 292 | 293 | bot.startConversation(message, function(err, convo) { 294 | 295 | convo.ask('Are you sure you want me to shutdown?', [ 296 | { 297 | pattern: bot.utterances.yes, 298 | callback: function(response, convo) { 299 | convo.say('Bye!'); 300 | convo.next(); 301 | setTimeout(function() { 302 | process.exit(); 303 | }, 3000); 304 | } 305 | }, 306 | { 307 | pattern: bot.utterances.no, 308 | default: true, 309 | callback: function(response, convo) { 310 | convo.say('*Phew!*'); 311 | convo.next(); 312 | } 313 | } 314 | ]); 315 | }); 316 | }); 317 | 318 | 319 | controller.hears(['uptime', 'identify yourself', 'who are you', 'what is your name'], 'message_received', 320 | function(bot, message) { 321 | 322 | var hostname = os.hostname(); 323 | var uptime = formatUptime(process.uptime()); 324 | 325 | bot.reply(message, 326 | ':|] I am a bot. I have been running for ' + uptime + ' on ' + hostname + '.'); 327 | }); 328 | 329 | 330 | 331 | controller.on('message_received', function(bot, message) { 332 | bot.reply(message, 'Try: `what is my name` or `structured` or `call me captain`'); 333 | return false; 334 | }); 335 | 336 | 337 | function formatUptime(uptime) { 338 | var unit = 'second'; 339 | if (uptime > 60) { 340 | uptime = uptime / 60; 341 | unit = 'minute'; 342 | } 343 | if (uptime > 60) { 344 | uptime = uptime / 60; 345 | unit = 'hour'; 346 | } 347 | if (uptime != 1) { 348 | unit = unit + 's'; 349 | } 350 | 351 | uptime = uptime + ' ' + unit; 352 | return uptime; 353 | } 354 | -------------------------------------------------------------------------------- /readme-twilioipm.md: -------------------------------------------------------------------------------- 1 | # Botkit and Twilio IP Messaging 2 | 3 | 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. 4 | 5 | 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 6 | developers to build interactive bots and applications that send and receive messages just like real humans. 7 | 8 | This document covers the Twilio-IPM implementation details only. [Start here](readme.md) if you want to learn about to develop with Botkit. 9 | 10 | Table of Contents 11 | 12 | * [Getting Started](#getting-started) 13 | * [Twilio IPM Events](#twilio-ipm-specific-events) 14 | * [Working with Twilio IPM](#working-with-twilio-ip-messaging) 15 | * [System Bots vs User Bots](#system-bots-vs-user-bots) 16 | * [Using Twilio's API](#using-the-twilio-api) 17 | 18 | ## Getting Started 19 | 20 | 1) Install Botkit [more info here](readme.md#installation) 21 | 22 | 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!! 23 | 24 | 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! 25 | 26 | ##### Twilio Account SID and Auth Token 27 | 28 | These values are available on your [Twilio Account page](https://www.twilio.com/user/account/settings). Copy both the SID and token values. 29 | 30 | ##### API Key and API Secret 31 | 32 | 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.' 33 | 34 | ##### Service SID 35 | 36 | To generate a Twilio service SID, [go here](https://www.twilio.com/user/account/ip-messaging/services) and click 'Create an IP Messaging Service'. 37 | 38 | 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! 39 | 40 | *Keep this tab open!* You'll come back here in step 7 to specify your bot's webhook endpoint URL. 41 | 42 | 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) 43 | 44 | Follow the instructions to get your IP Messaging Demo client up and running using the credentials you collected above. 45 | 46 | 5) Start up the sample Twilio IPM Bot. From inside your cloned Botkit repo, run: 47 | ``` 48 | TWILIO_ACCOUNT_SID= TWILIO_AUTH_TOKEN= TWILIO_IPM_SERVICE_SID= TWILIO_API_KEY= TWILIO_API_SECRET= node twilio_ipm_bot.js 49 | ``` 50 | 51 | 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` 52 | 53 | 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! 54 | 55 | 6) Load your IP Messaging client, and talk to your bot! 56 | 57 | Try: 58 | 59 | * hello 60 | * who am i? 61 | * call me Bob 62 | * shutdown 63 | 64 | ### Things to note 65 | 66 | 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. 67 | 68 | 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: 69 | 70 | ``` 71 | #!/bin/bash 72 | echo 'please enter the service uri' 73 | read servuri 74 | 75 | echo 'please enter the service sid' 76 | read servsid 77 | 78 | echo 'please enter the account sid' 79 | read accsid 80 | 81 | echo 'please enter the auth token' 82 | read authtok 83 | 84 | 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'" 85 | eval $onChannelDestroyedCurl 86 | 87 | 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'" 88 | eval $onChannelAddedCurl 89 | 90 | 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'" 91 | eval $onMemberRemovedCurl 92 | 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'" 93 | eval $onMessageRemovedCurl 94 | 95 | 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'" 96 | eval $onMessageUpdatedCurl 97 | 98 | 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'" 99 | eval $onChannelUpdatedCurl 100 | 101 | 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'" 102 | eval $onMemberAddedCurl 103 | ``` 104 | 105 | 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. 106 | 107 | ## Twilio IPM Specific Events 108 | 109 | Once connected to your Twilio IPM service, bots receive a constant stream of events. 110 | 111 | Normal messages will be sent to your bot using the `message_received` event. In addition, Botkit will trigger these Botkit-specific events: 112 | 113 | | Event | Description 114 | |--- |--- 115 | | bot_channel_join| The bot has joined a channel 116 | | bot_channel_leave | The bot has left a channel 117 | | user_channel_join | A user (not the bot) has joined a channel 118 | | user_channel_leave | A user (not the bot) has left a channel 119 | 120 | 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: 121 | 122 | | Event | Description 123 | |--- |--- 124 | | onMessageSent | Message sent 125 | | onMessageRemoved | Message removed/deleted 126 | | onMessageUpdated | Message edited 127 | | onChannelAdded | Channel created 128 | | onChannelUpdated | Channel FriendlyName or Attributes updated 129 | | onChannelDestroyed | Channel Deleted/Destroyed 130 | | onMemberAdded | Channel Member Joined or Added 131 | | onMemberRemoved | Channel Member Removed or Left 132 | 133 | 134 | ## Working with Twilio IP Messaging 135 | 136 | 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. 137 | 138 | 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. 139 | 140 | Here is the complete code for a basic Twilio bot: 141 | 142 | ```javascript 143 | var Botkit = require('botkit'); 144 | var controller = Botkit.twilioipmbot({ 145 | debug: false 146 | }) 147 | 148 | var bot = controller.spawn({ 149 | TWILIO_IPM_SERVICE_SID: process.env.TWILIO_IPM_SERVICE_SID, 150 | TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID, 151 | TWILIO_API_KEY: process.env.TWILIO_API_KEY, 152 | TWILIO_API_SECRET: process.env.TWILIO_API_SECRET, 153 | identity: 'Botkit', 154 | autojoin: true 155 | }); 156 | 157 | // if you are already using Express, you can use your own server instance... 158 | // see "Use BotKit with an Express web server" 159 | controller.setupWebserver(process.env.port,function(err,webserver) { 160 | controller.createWebhookEndpoints(controller.webserver, bot, function() { 161 | console.log('This bot is online!!!'); 162 | }); 163 | }); 164 | 165 | // user said hello 166 | controller.hears(['hello'], 'message_received', function(bot, message) { 167 | 168 | bot.reply(message, 'Hey there.'); 169 | 170 | }); 171 | 172 | controller.hears(['cookies'], 'message_received', function(bot, message) { 173 | 174 | bot.startConversation(message, function(err, convo) { 175 | 176 | convo.say('Did someone say cookies!?!!'); 177 | convo.ask('What is your favorite type of cookie?', function(response, convo) { 178 | convo.say('Golly, I love ' + response.text + ' too!!!'); 179 | convo.next(); 180 | }); 181 | }); 182 | }); 183 | ``` 184 | 185 | 186 | #### controller.setupWebserver() 187 | | Argument | Description 188 | |--- |--- 189 | | port | port for webserver 190 | | callback | callback function 191 | 192 | Setup an [Express webserver](http://expressjs.com/en/index.html) for 193 | use with `createWebhookEndpoints()` 194 | 195 | If you need more than a simple webserver to receive webhooks, 196 | you should by all means create your own Express webserver! 197 | 198 | The callback function receives the Express object as a parameter, 199 | which may be used to add further web server routes. 200 | 201 | #### controller.createWebhookEndpoints() 202 | 203 | This function configures the route `https://_your_server_/twilio/receive` 204 | to receive webhooks from twilio. 205 | 206 | This url should be used when configuring Twilio. 207 | 208 | ## System Bots vs User Bots 209 | 210 | Bots inside a Twilio IPM environment can run in one of two ways: as the "system" user, 211 | ever present and automatically available in all channels, OR, as a specific "bot" user 212 | who must be added to channels in order to interact. 213 | 214 | By default, bots are "system" users, and can be configured as below: 215 | 216 | ``` 217 | var bot = controller.spawn({ 218 | TWILIO_IPM_SERVICE_SID: process.env.TWILIO_IPM_SERVICE_SID, 219 | TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID, 220 | TWILIO_API_KEY: process.env.TWILIO_API_KEY, 221 | TWILIO_API_SECRET: process.env.TWILIO_API_SECRET, 222 | }); 223 | ``` 224 | 225 | To connect as a "bot" user, pass in an `identity` field: 226 | 227 | ``` 228 | var bot = controller.spawn({ 229 | TWILIO_IPM_SERVICE_SID: process.env.TWILIO_IPM_SERVICE_SID, 230 | TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID, 231 | TWILIO_API_KEY: process.env.TWILIO_API_KEY, 232 | TWILIO_API_SECRET: process.env.TWILIO_API_SECRET, 233 | identity: 'My Bot Name', 234 | }); 235 | ``` 236 | 237 | To have your bot automatically join every channel as they are created and removed, 238 | pass in `autojoin`: 239 | 240 | ``` 241 | var bot = controller.spawn({ 242 | TWILIO_IPM_SERVICE_SID: process.env.TWILIO_IPM_SERVICE_SID, 243 | TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID, 244 | TWILIO_API_KEY: process.env.TWILIO_API_KEY, 245 | TWILIO_API_SECRET: process.env.TWILIO_API_SECRET, 246 | identity: 'Botkit', 247 | autojoin: true 248 | }); 249 | ``` 250 | 251 | ## Using the Twilio API 252 | 253 | 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/). 254 | 255 | 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: 256 | 257 | ```javascript 258 | service.channels('CHANNEL_SID').members('MEMBER_SID').get().then(function(response) { 259 | console.log(response); 260 | }).fail(function(error) { 261 | console.log(error); 262 | }); 263 | ``` 264 | 265 | In Botkit, this can be accomplished by simply replacing the reference to a `service` object, with the `bot.api` object, as shown here: 266 | 267 | ```javascript 268 | bot.api.channels('CHANNEL_SID').members('MEMBER_SID').get().then(function(response) { 269 | console.log(response); 270 | }).fail(function(error) { 271 | console.log(error); 272 | }); 273 | ``` 274 | This gives you full access to all of the Twilio API methods so that you can use them in your Bot. 275 | 276 | 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. 277 | 278 | ```javascript 279 | controller.on('onChannelAdded', function(bot, message){ 280 | // whenever a channel gets added, join it! 281 | bot.api.channels(message.channel).members.create({ 282 | identity: bot.identity 283 | }).then(function(response) { 284 | 285 | }).fail(function(error) { 286 | console.log(error); 287 | }); 288 | }); 289 | ``` 290 | -------------------------------------------------------------------------------- /lib/TwilioIPMBot.js: -------------------------------------------------------------------------------- 1 | var Botkit = require(__dirname + '/CoreBot.js'); 2 | var request = require('request'); 3 | var express = require('express'); 4 | var bodyParser = require('body-parser'); 5 | var twilio = require('twilio'); 6 | var async = require('async'); 7 | 8 | var AccessToken = twilio.AccessToken; 9 | var IpMessagingGrant = AccessToken.IpMessagingGrant; 10 | 11 | function Twiliobot(configuration) { 12 | 13 | // Create a core botkit bot 14 | var twilio_botkit = Botkit(configuration || {}); 15 | 16 | // customize the bot definition, which will be used when new connections 17 | // spawn! 18 | twilio_botkit.defineBot(function(botkit, config) { 19 | var bot = { 20 | botkit: botkit, 21 | config: config || {}, 22 | utterances: botkit.utterances, 23 | }; 24 | 25 | bot.startConversation = function(message, cb) { 26 | botkit.startConversation(this, message, cb); 27 | }; 28 | 29 | bot.send = function(message, cb) { 30 | botkit.debug('SEND ', message); 31 | 32 | if (bot.identity === null || bot.identity === '') { 33 | bot.api.channels(message.channel).messages.create({ 34 | body: message.text, 35 | }).then(function(response) { 36 | cb(null, response); 37 | }).catch(function(err) { 38 | cb(err); 39 | }); 40 | } else { 41 | bot.api.channels(message.channel).messages.create({ 42 | body: message.text, 43 | from: bot.identity 44 | }).then(function(response) { 45 | cb(null, response); 46 | }).catch(function(err) { 47 | cb(err); 48 | }); 49 | } 50 | }; 51 | 52 | bot.reply = function(src, resp, cb) { 53 | var msg = {}; 54 | 55 | if (typeof(resp) == 'string') { 56 | msg.text = resp; 57 | } else { 58 | msg = resp; 59 | } 60 | 61 | msg.user = src.user; 62 | msg.channel = src.channel; 63 | 64 | bot.say(msg, cb); 65 | }; 66 | 67 | bot.autoJoinChannels = function() { 68 | bot.api.channels.list().then(function(full_channel_list) { 69 | if (bot.config.autojoin === true) { 70 | bot.channels = full_channel_list; 71 | bot.channels.channels.forEach(function(chan) { 72 | bot.api.channels(chan.sid).members.create({ 73 | identity: bot.identity 74 | }).then(function(response) { 75 | botkit.debug('added ' + 76 | bot.identity + ' as a member of the ' + chan.friendly_name); 77 | }).fail(function(error) { 78 | botkit.debug('Couldn\'t join the channel: ' + 79 | chan.friendly_name + ': ' + error); 80 | }); 81 | }); 82 | } else if (bot.identity) { 83 | 84 | // load up a list of all the channels that the bot is currently 85 | 86 | bot.channels = { 87 | channels: [] 88 | }; 89 | 90 | async.each(full_channel_list.channels, function(chan, next) { 91 | bot.api.channels(chan.sid).members.list().then(function(members) { 92 | for (var x = 0; x < members.members.length; x++) { 93 | if (members.members[x].identity == bot.identity) { 94 | bot.channels.channels.push(chan); 95 | } 96 | } 97 | next(); 98 | }).fail(function(error) { 99 | botkit.log('Error loading channel member list: ', error); 100 | next(); 101 | }); 102 | }); 103 | } 104 | }).fail(function(error) { 105 | botkit.log('Error loading channel list: ' + error); 106 | // fails if no channels exist 107 | // set the channels to empty 108 | bot.channels = { channels: [] }; 109 | }); 110 | 111 | }; 112 | 113 | bot.configureBotIdentity = function() { 114 | if (bot.identity !== null || bot.identity !== '') { 115 | var userRespIter = 0; 116 | var existingIdentity = null; 117 | 118 | // try the get by identity thing 119 | bot.api.users(bot.identity).get().then(function(response) { 120 | bot.autoJoinChannels(); 121 | }).fail(function(error) { 122 | // if not make the new user and see if they need to be added to all the channels 123 | bot.api.users.create({ 124 | identity: bot.identity 125 | }).then(function(response) { 126 | bot.autoJoinChannels(); 127 | }).fail(function(error) { 128 | botkit.log('Could not get Bot Identity:'); 129 | botkit.log(error); 130 | process.exit(1); 131 | }); 132 | }); 133 | } 134 | }; 135 | 136 | /** 137 | * This handles the particulars of finding an existing conversation or 138 | * topic to fit the message into... 139 | */ 140 | bot.findConversation = function(message, cb) { 141 | botkit.debug('CUSTOM FIND CONVO', message.user, message.channel); 142 | for (var t = 0; t < botkit.tasks.length; t++) { 143 | for (var c = 0; c < botkit.tasks[t].convos.length; c++) { 144 | if ( 145 | botkit.tasks[t].convos[c].isActive() && 146 | botkit.tasks[t].convos[c].source_message.user == message.user && 147 | botkit.tasks[t].convos[c].source_message.channel == message.channel 148 | ) { 149 | botkit.debug('FOUND EXISTING CONVO!'); 150 | cb(botkit.tasks[t].convos[c]); 151 | return; 152 | } 153 | } 154 | } 155 | 156 | cb(); 157 | }; 158 | 159 | 160 | bot.client = new twilio.IpMessagingClient(config.TWILIO_ACCOUNT_SID, config.TWILIO_AUTH_TOKEN); 161 | bot.api = bot.client.services(config.TWILIO_IPM_SERVICE_SID); 162 | 163 | if (config.identity) { 164 | bot.identity = config.identity; 165 | bot.configureBotIdentity(); 166 | } 167 | 168 | return bot; 169 | 170 | }); 171 | 172 | 173 | twilio_botkit.setupWebserver = function(port, cb) { 174 | 175 | if (!port) { 176 | throw new Error('Cannot start webserver without a port'); 177 | } 178 | if (isNaN(port)) { 179 | throw new Error('Specified port is not a valid number'); 180 | } 181 | 182 | twilio_botkit.config.port = port; 183 | 184 | twilio_botkit.webserver = express(); 185 | twilio_botkit.webserver.use(bodyParser.json()); 186 | twilio_botkit.webserver.use(bodyParser.urlencoded({ extended: true })); 187 | twilio_botkit.webserver.use(express.static(__dirname + '/public')); 188 | 189 | var server = twilio_botkit.webserver.listen( 190 | twilio_botkit.config.port, 191 | function() { 192 | twilio_botkit.log('** Starting webserver on port ' + 193 | twilio_botkit.config.port); 194 | if (cb) { cb(null, twilio_botkit.webserver); } 195 | }); 196 | 197 | return twilio_botkit; 198 | 199 | }; 200 | 201 | 202 | 203 | 204 | // set up a web route for receiving outgoing webhooks and/or slash commands 205 | twilio_botkit.createWebhookEndpoints = function(webserver, bot) { 206 | 207 | twilio_botkit.log( 208 | '** Serving webhook endpoints for receiving messages ' + 209 | 'webhooks at: http://MY_HOST:' + twilio_botkit.config.port + '/twilio/receive'); 210 | webserver.post('/twilio/receive', function(req, res) { 211 | // ensure all messages 212 | // have a user & channel 213 | var message = req.body; 214 | if (req.body.EventType == 'onMessageSent') { 215 | 216 | // customize fields to be compatible with Botkit 217 | message.text = req.body.Body; 218 | message.from = req.body.From; 219 | message.to = req.body.To; 220 | message.user = req.body.From; 221 | message.channel = req.body.ChannelSid; 222 | 223 | twilio_botkit.receiveMessage(bot, message); 224 | 225 | }else if (req.body.EventType == 'onChannelAdded' || req.body.EventType == 'onChannelAdd') { 226 | // this event has a channel sid but not a user 227 | message.channel = req.body.ChannelSid; 228 | twilio_botkit.trigger(req.body.EventType, [bot, message]); 229 | 230 | }else if (req.body.EventType == 'onChannelDestroyed' || req.body.EventType == 'onChannelDestroy') { 231 | // this event has a channel sid but not a user 232 | message.channel = req.body.ChannelSid; 233 | twilio_botkit.trigger(req.body.EventType, [bot, message]); 234 | 235 | }else if (req.body.EventType == 'onMemberAdded' || req.body.EventType == 'onMemberAdd') { 236 | // should user be MemberSid the The Member Sid of the newly added Member 237 | message.user = req.body.Identity; 238 | message.channel = req.body.ChannelSid; 239 | twilio_botkit.trigger(req.body.EventType, [bot, message]); 240 | } else if (req.body.EventType == 'onMemberRemoved' || req.body.EventType == 'onMemberRemove') { 241 | message.user = req.body.Identity; 242 | message.channel = req.body.ChannelSid; 243 | twilio_botkit.trigger(req.body.EventType, [bot, message]); 244 | 245 | if (req.body.EventType == 'onMemberRemoved') { 246 | 247 | } 248 | } else { 249 | twilio_botkit.trigger(req.body.EventType, [bot, message]); 250 | } 251 | 252 | res.status(200); 253 | res.send('ok'); 254 | 255 | 256 | }); 257 | 258 | twilio_botkit.startTicking(); 259 | 260 | return twilio_botkit; 261 | }; 262 | 263 | 264 | 265 | // handle events here 266 | twilio_botkit.handleTwilioEvents = function() { 267 | twilio_botkit.log('** Setting up custom handlers for processing Twilio messages'); 268 | twilio_botkit.on('message_received', function(bot, message) { 269 | 270 | 271 | 272 | if (bot.identity && message.from == bot.identity) { 273 | return false; 274 | } 275 | 276 | if (!message.text) { 277 | // message without text is probably an edit 278 | return false; 279 | } 280 | 281 | if (bot.identity) { 282 | var channels = bot.channels.channels; 283 | 284 | // if its not in a channel with the bot 285 | var apprChan = channels.filter(function(ch) { 286 | return ch.sid == message.channel; 287 | }); 288 | 289 | if (apprChan.length === 0) { 290 | return false; 291 | } 292 | } 293 | }); 294 | 295 | 296 | // if a member is removed from a channel, check to see if it matches the bot's identity 297 | // and if so remove it from the list of channels the bot listens to 298 | twilio_botkit.on('onMemberRemoved', function(bot, message) { 299 | if (bot.identity && message.user == bot.identity) { 300 | // remove that channel from bot.channels.channels 301 | var chan_to_rem = bot.channels.channels.map(function(ch) { 302 | return ch.sid; 303 | }).indexOf(message.channel); 304 | 305 | if (chan_to_rem != -1) { 306 | bot.channels.channels.splice(chan_to_rem, 1); 307 | twilio_botkit.debug('Unsubscribing from channel because of memberremove.'); 308 | 309 | } 310 | } else if (bot.identity) { 311 | var channels = bot.channels.channels; 312 | 313 | // if its not in a channel with the bot 314 | var apprChan = channels.filter(function(ch) { 315 | return ch.sid == message.channel; 316 | }); 317 | 318 | if (apprChan.length === 0) { 319 | return false; 320 | } 321 | } 322 | 323 | if (bot.identity && bot.identity == message.user) { 324 | twilio_botkit.trigger('bot_channel_leave', [bot, message]); 325 | } else { 326 | twilio_botkit.trigger('user_channel_leave', [bot, message]); 327 | } 328 | }); 329 | 330 | twilio_botkit.on('onMemberAdded', function(bot, message) { 331 | if (bot.identity && message.user == bot.identity) { 332 | bot.api.channels(message.channel).get().then(function(response) { 333 | bot.channels.channels.push(response); 334 | twilio_botkit.debug('Subscribing to channel because of memberadd.'); 335 | 336 | }).fail(function(error) { 337 | botkit.log(error); 338 | }); 339 | } else if (bot.identity) { 340 | var channels = bot.channels.channels; 341 | 342 | // if its not in a channel with the bot 343 | var apprChan = channels.filter(function(ch) { 344 | return ch.sid == message.channel; 345 | }); 346 | 347 | if (apprChan.length === 0) { 348 | return false; 349 | } 350 | } 351 | 352 | if (bot.identity && bot.identity == message.user) { 353 | twilio_botkit.trigger('bot_channel_join', [bot, message]); 354 | } else { 355 | twilio_botkit.trigger('user_channel_join', [bot, message]); 356 | } 357 | 358 | }); 359 | 360 | 361 | // if a channel is destroyed, remove it from the list of channels this bot listens to 362 | twilio_botkit.on('onChannelDestroyed', function(bot, message) { 363 | if (bot.identity) { 364 | var chan_to_rem = bot.channels.channels.map(function(ch) { 365 | return ch.sid; 366 | }).indexOf(message.channel); 367 | if (chan_to_rem != -1) { 368 | bot.channels.channels.splice(chan_to_rem, 1); 369 | twilio_botkit.debug('Unsubscribing from destroyed channel.'); 370 | } 371 | } 372 | }); 373 | 374 | // if a channel is created, and the bot is set in autojoin mode, join the channel 375 | twilio_botkit.on('onChannelAdded', function(bot, message) { 376 | if (bot.identity && bot.config.autojoin === true) { 377 | // join the channel 378 | bot.api.channels(message.channel).members.create({ 379 | identity: bot.identity 380 | }).then(function(response) { 381 | bot.api.channels(message.channel).get().then(function(response) { 382 | bot.channels.channels.push(response); 383 | twilio_botkit.debug('Subscribing to new channel.'); 384 | 385 | }).fail(function(error) { 386 | botkit.log(error); 387 | }); 388 | }).fail(function(error) { 389 | botkit.log(error); 390 | }); 391 | } 392 | }); 393 | 394 | }; 395 | 396 | twilio_botkit.handleTwilioEvents(); 397 | 398 | return twilio_botkit; 399 | 400 | } 401 | 402 | module.exports = Twiliobot; 403 | -------------------------------------------------------------------------------- /lib/Slackbot_worker.js: -------------------------------------------------------------------------------- 1 | var Ws = require('ws'); 2 | var request = require('request'); 3 | var slackWebApi = require(__dirname + '/Slack_web_api.js'); 4 | var HttpsProxyAgent = require('https-proxy-agent'); 5 | var Back = require('back'); 6 | 7 | 8 | module.exports = function(botkit, config) { 9 | var bot = { 10 | botkit: botkit, 11 | config: config || {}, 12 | utterances: botkit.utterances, 13 | api: slackWebApi(botkit, config || {}), 14 | identity: { // default identity values 15 | id: null, 16 | name: '', 17 | } 18 | }; 19 | 20 | var pingIntervalId = null; 21 | var lastPong = 0; 22 | var retryBackoff = null; 23 | 24 | // config.retry, can be Infinity too 25 | var retryEnabled = bot.config.retry ? true : false; 26 | var maxRetry = isNaN(bot.config.retry) || bot.config.retry <= 0 ? 3 : bot.config.retry; 27 | 28 | /** 29 | * Set up API to send incoming webhook 30 | */ 31 | bot.configureIncomingWebhook = function(options) { 32 | if (!options.url) 33 | throw new Error('No incoming webhook URL specified!'); 34 | 35 | bot.config.incoming_webhook = options; 36 | 37 | return bot; 38 | }; 39 | 40 | bot.sendWebhook = function(options, cb) { 41 | if (!bot.config.incoming_webhook || !bot.config.incoming_webhook.url) { 42 | botkit.debug('CANNOT SEND WEBHOOK!!'); 43 | 44 | return cb && cb('No webhook url specified'); 45 | } 46 | 47 | request.post(bot.config.incoming_webhook.url, function(err, res, body) { 48 | if (err) { 49 | botkit.debug('WEBHOOK ERROR', err); 50 | return cb && cb(err); 51 | } 52 | botkit.debug('WEBHOOK SUCCESS', body); 53 | cb && cb(null, body); 54 | }).form({ payload: JSON.stringify(options) }); 55 | }; 56 | 57 | bot.configureRTM = function(config) { 58 | bot.config.token = config.token; 59 | return bot; 60 | }; 61 | 62 | bot.closeRTM = function(err) { 63 | if (bot.rtm) { 64 | bot.rtm.removeAllListeners(); 65 | bot.rtm.close(); 66 | } 67 | 68 | if (pingIntervalId) { 69 | clearInterval(pingIntervalId); 70 | } 71 | 72 | lastPong = 0; 73 | botkit.trigger('rtm_close', [bot, err]); 74 | 75 | // only retry, if enabled, when there was an error 76 | if (err && retryEnabled) { 77 | reconnect(); 78 | } 79 | }; 80 | 81 | 82 | function reconnect(err) { 83 | var options = { 84 | minDelay: 1000, 85 | maxDelay: 30000, 86 | retries: maxRetry 87 | }; 88 | var back = retryBackoff || (retryBackoff = new Back(options)); 89 | return back.backoff(function(fail) { 90 | if (fail) { 91 | botkit.log.error('** BOT ID:', bot.identity.name, '...reconnect failed after #' + 92 | back.settings.attempt + ' attempts and ' + back.settings.timeout + 'ms'); 93 | botkit.trigger('rtm_reconnect_failed', [bot, err]); 94 | return; 95 | } 96 | 97 | botkit.log.notice('** BOT ID:', bot.identity.name, '...reconnect attempt #' + 98 | back.settings.attempt + ' of ' + options.retries + ' being made after ' + back.settings.timeout + 'ms'); 99 | bot.startRTM(function(err) { 100 | if (err) { 101 | return reconnect(err); 102 | } 103 | retryBackoff = null; 104 | }); 105 | }); 106 | } 107 | 108 | /** 109 | * Shutdown and cleanup the spawned worker 110 | */ 111 | bot.destroy = function() { 112 | if (retryBackoff) { 113 | retryBackoff.close(); 114 | retryBackoff = null; 115 | } 116 | bot.closeRTM(); 117 | botkit.shutdown(); 118 | }; 119 | 120 | bot.startRTM = function(cb) { 121 | bot.api.rtm.start({ 122 | no_unreads: true, 123 | simple_latest: true, 124 | }, function(err, res) { 125 | if (err) { 126 | return cb && cb(err); 127 | } 128 | 129 | if (!res) { 130 | return cb && cb('Invalid response from rtm.start'); 131 | } 132 | 133 | bot.identity = res.self; 134 | bot.team_info = res.team; 135 | 136 | /** 137 | * Also available: 138 | * res.users, res.channels, res.groups, res.ims, 139 | * res.bots 140 | * 141 | * Could be stored & cached for later use. 142 | */ 143 | 144 | botkit.log.notice('** BOT ID:', bot.identity.name, '...attempting to connect to RTM!'); 145 | 146 | var agent = null; 147 | var proxyUrl = process.env.https_proxy || process.env.http_proxy; 148 | if (proxyUrl) { 149 | agent = new HttpsProxyAgent(proxyUrl); 150 | } 151 | 152 | bot.rtm = new Ws(res.url, null, {agent: agent}); 153 | bot.msgcount = 1; 154 | 155 | bot.rtm.on('pong', function(obj) { 156 | lastPong = Date.now(); 157 | botkit.debug('PONG received'); 158 | }); 159 | 160 | bot.rtm.on('open', function() { 161 | 162 | pingIntervalId = setInterval(function() { 163 | if (lastPong && lastPong + 12000 < Date.now()) { 164 | var err = new Error('Stale RTM connection, closing RTM'); 165 | bot.closeRTM(err); 166 | return; 167 | } 168 | 169 | botkit.debug('PING sent'); 170 | bot.rtm.ping(null, null, true); 171 | }, 5000); 172 | 173 | botkit.trigger('rtm_open', [bot]); 174 | 175 | bot.rtm.on('message', function(data, flags) { 176 | 177 | var message = null; 178 | try { 179 | message = JSON.parse(data); 180 | } catch (err) { 181 | console.log('** RECEIVED BAD JSON FROM SLACK'); 182 | } 183 | /** 184 | * Lets construct a nice quasi-standard botkit message 185 | * it leaves the main slack message at the root 186 | * but adds in additional fields for internal use! 187 | * (including the teams api details) 188 | */ 189 | if (message != null) { 190 | botkit.receiveMessage(bot, message); 191 | } 192 | }); 193 | 194 | botkit.startTicking(); 195 | 196 | cb && cb(null, bot, res); 197 | }); 198 | 199 | bot.rtm.on('error', function(err) { 200 | botkit.log.error('RTM websocket error!', err); 201 | if (pingIntervalId) { 202 | clearInterval(pingIntervalId); 203 | } 204 | botkit.trigger('rtm_close', [bot, err]); 205 | }); 206 | 207 | bot.rtm.on('close', function() { 208 | if (pingIntervalId) { 209 | clearInterval(pingIntervalId); 210 | } 211 | botkit.trigger('rtm_close', [bot]); 212 | }); 213 | }); 214 | 215 | return bot; 216 | }; 217 | 218 | bot.identifyBot = function(cb) { 219 | if (bot.identity) { 220 | bot.identifyTeam(function(err, team) { 221 | cb(null, { 222 | name: bot.identity.name, 223 | id: bot.identity.id, 224 | team_id: team 225 | }); 226 | }); 227 | } else { 228 | /** 229 | * Note: Are there scenarios other than the RTM 230 | * where we might pull identity info, perhaps from 231 | * bot.api.auth.test on a given token? 232 | */ 233 | cb('Identity Unknown: Not using RTM api'); 234 | }; 235 | }; 236 | 237 | bot.identifyTeam = function(cb) { 238 | if (bot.team_info) 239 | return cb(null, bot.team_info.id); 240 | 241 | /** 242 | * Note: Are there scenarios other than the RTM 243 | * where we might pull identity info, perhaps from 244 | * bot.api.auth.test on a given token? 245 | */ 246 | cb('Unknown Team!'); 247 | }; 248 | 249 | /** 250 | * Convenience method for creating a DM convo. 251 | */ 252 | bot.startPrivateConversation = function(message, cb) { 253 | botkit.startTask(this, message, function(task, convo) { 254 | bot._startDM(task, message.user, function(err, dm) { 255 | convo.stop(); 256 | cb(err, dm); 257 | }); 258 | }); 259 | }; 260 | 261 | bot.startConversation = function(message, cb) { 262 | botkit.startConversation(this, message, cb); 263 | }; 264 | 265 | /** 266 | * Convenience method for creating a DM convo. 267 | */ 268 | bot._startDM = function(task, user_id, cb) { 269 | bot.api.im.open({ user: user_id }, function(err, channel) { 270 | if (err) return cb(err); 271 | 272 | cb(null, task.startConversation({ 273 | channel: channel.channel.id, 274 | user: user_id 275 | })); 276 | }); 277 | }; 278 | 279 | bot.send = function(message, cb) { 280 | botkit.debug('SAY', message); 281 | 282 | /** 283 | * Construct a valid slack message. 284 | */ 285 | var slack_message = { 286 | type: message.type || 'message', 287 | channel: message.channel, 288 | text: message.text || null, 289 | username: message.username || null, 290 | parse: message.parse || null, 291 | link_names: message.link_names || null, 292 | attachments: message.attachments ? 293 | JSON.stringify(message.attachments) : null, 294 | unfurl_links: typeof message.unfurl_links !== 'undefined' ? message.unfurl_links : null, 295 | unfurl_media: typeof message.unfurl_media !== 'undefined' ? message.unfurl_media : null, 296 | icon_url: message.icon_url || null, 297 | icon_emoji: message.icon_emoji || null, 298 | }; 299 | bot.msgcount++; 300 | 301 | if (message.icon_url || message.icon_emoji || message.username) { 302 | slack_message.as_user = false; 303 | } else { 304 | slack_message.as_user = message.as_user || true; 305 | } 306 | 307 | /** 308 | * These options are not supported by the RTM 309 | * so if they are specified, we use the web API to send messages. 310 | */ 311 | if (message.attachments || message.icon_emoji || 312 | message.username || message.icon_url) { 313 | 314 | if (!bot.config.token) { 315 | throw new Error('Cannot use web API to send messages.'); 316 | } 317 | 318 | bot.api.chat.postMessage(slack_message, function(err, res) { 319 | if (err) { 320 | cb && cb(err); 321 | } else { 322 | cb && cb(null, res); 323 | } 324 | }); 325 | 326 | } else { 327 | if (!bot.rtm) 328 | throw new Error('Cannot use the RTM API to send messages.'); 329 | 330 | slack_message.id = message.id || bot.msgcount; 331 | 332 | 333 | try { 334 | bot.rtm.send(JSON.stringify(slack_message), function(err) { 335 | if (err) { 336 | cb && cb(err); 337 | } else { 338 | cb && cb(); 339 | } 340 | }); 341 | } catch (err) { 342 | /** 343 | * The RTM failed and for some reason it didn't get caught 344 | * elsewhere. This happens sometimes when the rtm has closed but 345 | * We are sending messages anyways. 346 | * Bot probably needs to reconnect! 347 | */ 348 | cb && cb(err); 349 | } 350 | } 351 | }; 352 | 353 | bot.replyPublic = function(src, resp, cb) { 354 | if (!bot.res) { 355 | cb && cb('No web response object found'); 356 | } else { 357 | var msg = {}; 358 | 359 | if (typeof(resp) == 'string') { 360 | msg.text = resp; 361 | } else { 362 | msg = resp; 363 | } 364 | 365 | msg.channel = src.channel; 366 | 367 | msg.response_type = 'in_channel'; 368 | bot.res.json(msg); 369 | cb && cb(); 370 | } 371 | }; 372 | 373 | bot.replyPublicDelayed = function(src, resp, cb) { 374 | if (!src.response_url) { 375 | cb && cb('No response_url found'); 376 | } else { 377 | var msg = {}; 378 | 379 | if (typeof(resp) == 'string') { 380 | msg.text = resp; 381 | } else { 382 | msg = resp; 383 | } 384 | 385 | msg.channel = src.channel; 386 | 387 | msg.response_type = 'in_channel'; 388 | var requestOptions = { 389 | uri: src.response_url, 390 | method: 'POST', 391 | json: msg 392 | }; 393 | request(requestOptions, function(err, resp, body) { 394 | /** 395 | * Do something? 396 | */ 397 | if (err) { 398 | botkit.log.error('Error sending slash command response:', err); 399 | cb && cb(err); 400 | } else { 401 | cb && cb(); 402 | } 403 | }); 404 | } 405 | }; 406 | 407 | bot.replyPrivate = function(src, resp, cb) { 408 | if (!bot.res) { 409 | cb && cb('No web response object found'); 410 | } else { 411 | var msg = {}; 412 | 413 | if (typeof(resp) == 'string') { 414 | msg.text = resp; 415 | } else { 416 | msg = resp; 417 | } 418 | 419 | msg.channel = src.channel; 420 | 421 | msg.response_type = 'ephemeral'; 422 | bot.res.json(msg); 423 | 424 | cb && cb(); 425 | } 426 | }; 427 | 428 | bot.replyPrivateDelayed = function(src, resp, cb) { 429 | if (!src.response_url) { 430 | cb && cb('No response_url found'); 431 | } else { 432 | var msg = {}; 433 | 434 | if (typeof(resp) == 'string') { 435 | msg.text = resp; 436 | } else { 437 | msg = resp; 438 | } 439 | 440 | msg.channel = src.channel; 441 | 442 | msg.response_type = 'ephemeral'; 443 | 444 | var requestOptions = { 445 | uri: src.response_url, 446 | method: 'POST', 447 | json: msg 448 | }; 449 | request(requestOptions, function(err, resp, body) { 450 | /** 451 | * Do something? 452 | */ 453 | if (err) { 454 | botkit.log.error('Error sending slash command response:', err); 455 | cb && cb(err); 456 | } else { 457 | cb && cb(); 458 | } 459 | }); 460 | } 461 | }; 462 | 463 | bot.reply = function(src, resp, cb) { 464 | var msg = {}; 465 | 466 | if (typeof(resp) == 'string') { 467 | msg.text = resp; 468 | } else { 469 | msg = resp; 470 | } 471 | 472 | msg.channel = src.channel; 473 | 474 | bot.say(msg, cb); 475 | }; 476 | 477 | /** 478 | * sends a typing message to the source channel 479 | * 480 | * @param {Object} src message source 481 | */ 482 | bot.startTyping = function(src) { 483 | bot.reply(src, { type: 'typing' }); 484 | }; 485 | 486 | /** 487 | * replies with message after typing delay 488 | * 489 | * @param {Object} src message source 490 | * @param {(string|Object)} resp string or object 491 | * @param {function} cb optional request callback 492 | */ 493 | bot.replyWithTyping = function(src, resp, cb) { 494 | var text; 495 | 496 | if (typeof(resp) == 'string') { 497 | text = resp; 498 | } else { 499 | text = resp.text; 500 | } 501 | 502 | var typingLength = 1200 / 60 * text.length; 503 | typingLength = typingLength > 2000 ? 2000 : typingLength; 504 | 505 | bot.startTyping(src); 506 | 507 | setTimeout(function() { 508 | bot.reply(src, resp, cb); 509 | }, typingLength); 510 | }; 511 | 512 | /** 513 | * This handles the particulars of finding an existing conversation or 514 | * topic to fit the message into... 515 | */ 516 | bot.findConversation = function(message, cb) { 517 | botkit.debug('CUSTOM FIND CONVO', message.user, message.channel); 518 | if (message.type == 'message' || message.type == 'slash_command' || 519 | message.type == 'outgoing_webhook') { 520 | for (var t = 0; t < botkit.tasks.length; t++) { 521 | for (var c = 0; c < botkit.tasks[t].convos.length; c++) { 522 | if ( 523 | botkit.tasks[t].convos[c].isActive() && 524 | botkit.tasks[t].convos[c].source_message.user == message.user && 525 | botkit.tasks[t].convos[c].source_message.channel == message.channel 526 | ) { 527 | botkit.debug('FOUND EXISTING CONVO!'); 528 | 529 | // modify message text to prune off the bot's name (@bot hey -> hey) 530 | // and trim whitespace that is sometimes added 531 | // this would otherwise happen in the handleSlackEvents function 532 | // which does not get called for messages attached to conversations. 533 | 534 | if (message.text) { 535 | message.text = message.text.trim(); 536 | } 537 | 538 | var direct_mention = new RegExp('^\<\@' + bot.identity.id + '\>', 'i'); 539 | 540 | message.text = message.text.replace(direct_mention, '') 541 | .replace(/^\s+/, '').replace(/^\:\s+/, '').replace(/^\s+/, ''); 542 | 543 | cb(botkit.tasks[t].convos[c]); 544 | return; 545 | } 546 | } 547 | } 548 | } 549 | 550 | cb(); 551 | }; 552 | 553 | if (bot.config.incoming_webhook) 554 | bot.configureIncomingWebhook(config.incoming_webhook); 555 | 556 | if (bot.config.bot) 557 | bot.configureRTM(config.bot); 558 | 559 | return bot; 560 | }; 561 | -------------------------------------------------------------------------------- /readme-slack.md: -------------------------------------------------------------------------------- 1 | # Botkit and Slack 2 | 3 | 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. 4 | 5 | Botkit features a comprehensive set of tools 6 | to deal with [Slack's integration platform](http://api.slack.com), and allows 7 | developers to build both custom integrations for their 8 | team, as well as public "Slack Button" applications that can be 9 | run from a central location, and be used by many teams at the same time. 10 | 11 | This document covers the Slack-specific implementation details only. [Start here](readme.md) if you want to learn about to develop with Botkit. 12 | 13 | Table of Contents 14 | 15 | * [Getting Started](#getting-started) 16 | * [Connecting Your Bot To Slack](#connecting-your-bot-to-slack) 17 | * [Slack-specific Events](#slack-specific-events) 18 | * [Working with Slack Custom Integrations](#working-with-slack-integrations) 19 | * [Using the Slack Button](#use-the-slack-button) 20 | 21 | --- 22 | ## Getting Started 23 | 24 | 1) Install Botkit [more info here](readme.md#installation) 25 | 26 | 2) First make a bot integration inside of your Slack channel. Go here: 27 | 28 | https://my.slack.com/services/new/bot 29 | 30 | Enter a name for your bot. 31 | Make it something fun and friendly, but avoid a single task specific name. 32 | Bots can do lots! Let's not pigeonhole them. 33 | 34 | 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. 35 | 36 | Copy the API token that Slack gives you. You'll need it. 37 | 38 | 4) Run the example bot app, using the token you just copied: 39 | ​ 40 | ``` 41 | token=REPLACE_THIS_WITH_YOUR_TOKEN node bot.js 42 | ``` 43 | ​ 44 | 5) Your bot should be online! Within Slack, send it a quick direct message to say hello. It should say hello back! 45 | 46 | Try: 47 | * who are you? 48 | * call me Bob 49 | * shutdown 50 | ​ 51 | 52 | ### Things to note 53 | ​ 54 | Much like a vampire, a bot has to be invited into a channel. DO NOT WORRY bots are not vampires. 55 | 56 | Type: `/invite @` to invite your bot into another channel. 57 | 58 | 59 | ## Connecting Your Bot to Slack 60 | 61 | Bot users connect to Slack using a real time API based on web sockets. 62 | The bot connects to Slack using the same protocol that the native Slack clients use! 63 | 64 | To connect a bot to Slack, [get a Bot API token from the Slack integrations page](https://my.slack.com/services/new/bot). 65 | 66 | 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. 67 | This is particularly true if you store and use API tokens on behalf of users other than yourself! 68 | 69 | [Read Slack's Bot User documentation](https://api.slack.com/bot-users) 70 | 71 | 72 | #### controller.spawn() 73 | | Argument | Description 74 | |--- |--- 75 | | config | Incoming message object 76 | 77 | Spawn an instance of your bot and connect it to Slack. 78 | This function takes a configuration object which should contain 79 | at least one method of talking to the Slack API. 80 | 81 | To use the real time / bot user API, pass in a token. 82 | 83 | Controllers can also spawn bots that use [incoming webhooks](#incoming-webhooks). 84 | 85 | Spawn `config` object accepts these properties: 86 | 87 | | Name | Value | Description 88 | |--- |--- |--- 89 | | token | String | Slack bot token 90 | | 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 91 | 92 | 93 | 94 | #### bot.startRTM() 95 | | Argument | Description 96 | |--- |--- 97 | | callback | _Optional_ Callback in the form function(err,bot,payload) { ... } 98 | 99 | Opens a connection to Slack's real time API. This connection will remain 100 | open until it fails or is closed using `closeRTM()`. 101 | 102 | The optional callback function receives: 103 | 104 | * Any error that occurred while connecting to Slack 105 | * An updated bot object 106 | * The resulting JSON payload of the Slack API command [rtm.start](https://api.slack.com/methods/rtm.start) 107 | 108 | The payload that this callback function receives contains a wealth of information 109 | about the bot and its environment, including a complete list of the users 110 | and channels visible to the bot. This information should be cached and used 111 | when possible instead of calling Slack's API. 112 | 113 | A successful connection the API will also cause a `rtm_open` event to be 114 | fired on the `controller` object. 115 | 116 | 117 | #### bot.closeRTM() 118 | 119 | Close the connection to the RTM. Once closed, an `rtm_close` event is fired 120 | on the `controller` object. 121 | 122 | 123 | ```javascript 124 | var Botkit = require('Botkit'); 125 | 126 | var controller = Botkit.slackbot(); 127 | 128 | var bot = controller.spawn({ 129 | token: my_slack_bot_token 130 | }) 131 | 132 | bot.startRTM(function(err,bot,payload) { 133 | if (err) { 134 | throw new Error('Could not connect to Slack'); 135 | } 136 | 137 | // close the RTM for the sake of it in 5 seconds 138 | setTimeout(function() { 139 | bot.closeRTM(); 140 | }, 5000); 141 | }); 142 | ``` 143 | 144 | #### bot.destroy() 145 | 146 | Completely shutdown and cleanup the spawned worker. Use `bot.closeRTM()` only to disconnect 147 | but not completely tear down the worker. 148 | 149 | 150 | ```javascript 151 | var Botkit = require('Botkit'); 152 | var controller = Botkit.slackbot(); 153 | var bot = controller.spawn({ 154 | token: my_slack_bot_token 155 | }) 156 | 157 | bot.startRTM(function(err, bot, payload) { 158 | if (err) { 159 | throw new Error('Could not connect to Slack'); 160 | } 161 | }); 162 | 163 | // some time later (e.g. 10s) when finished with the RTM connection and worker 164 | setTimeout(bot.destroy.bind(bot), 10000) 165 | ``` 166 | 167 | ### Slack-Specific Events 168 | 169 | 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. 170 | 171 | Botkit's message parsing and event system does a great deal of filtering on this 172 | real time stream so developers do not need to parse every message. See [Receiving Messages](readme.md#receiving-messages) 173 | for more information about listening for and responding to messages. 174 | 175 | 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. 176 | 177 | You can receive and handle any of the [native events thrown by slack](https://api.slack.com/events). 178 | 179 | ```javascript 180 | controller.on('channel_joined',function(bot,message) { 181 | 182 | // message contains data sent by slack 183 | // in this case: 184 | // https://api.slack.com/events/channel_joined 185 | 186 | }); 187 | ``` 188 | 189 | You can also receive and handle a long list of additional events caused 190 | by messages that contain a subtype field, [as listed here](https://api.slack.com/events/message) 191 | 192 | ```javascript 193 | controller.on('channel_leave',function(bot,message) { 194 | 195 | // message format matches this: 196 | // https://api.slack.com/events/message/channel_leave 197 | 198 | }) 199 | ``` 200 | 201 | Finally, Botkit throws a handful of its own events! 202 | Events related to the general operation of bots are below. 203 | When used in conjunction with the Slack Button, Botkit also fires 204 | a [few additional events](#use-the-slack-button). 205 | 206 | 207 | #### User Activity Events: 208 | 209 | | Event | Description 210 | |--- |--- 211 | | message_received | a message was received by the bot 212 | | bot_channel_join | the bot has joined a channel 213 | | user_channel_join | a user has joined a channel 214 | | bot_group_join | the bot has joined a group 215 | | user_group_join | a user has joined a group 216 | 217 | #### Message Received Events 218 | | Event | Description 219 | |--- |--- 220 | | direct_message | the bot received a direct message from a user 221 | | direct_mention | the bot was addressed directly in a channel 222 | | mention | the bot was mentioned by someone in a message 223 | | ambient | the message received had no mention of the bot 224 | 225 | #### Websocket Events: 226 | 227 | | Event | Description 228 | |--- |--- 229 | | rtm_open | a connection has been made to the RTM api 230 | | rtm_close | a connection to the RTM api has closed 231 | | rtm_reconnect_failed | if retry enabled, retry attempts have been exhausted 232 | 233 | 234 | ## Working with Slack Integrations 235 | 236 | There are a dizzying number of ways to integrate your application into Slack. 237 | Up to this point, this document has mainly dealt with the real time / bot user 238 | integration. In addition to this type of integration, Botkit also supports: 239 | 240 | * Incoming Webhooks - a way to send (but not receive) messages to Slack 241 | * Outgoing Webhooks - a way to receive messages from Slack based on a keyword or phrase 242 | * Slash Command - a way to add /slash commands to Slack 243 | * Slack Web API - a full set of RESTful API tools to deal with Slack 244 | * The Slack Button - a way to build Slack applications that can be used by multiple teams 245 | 246 | 247 | ```javascript 248 | var Botkit = require('botkit'); 249 | var controller = Botkit.slackbot({}) 250 | 251 | var bot = controller.spawn({ 252 | token: my_slack_bot_token 253 | }); 254 | 255 | // use RTM 256 | bot.startRTM(function(err,bot,payload) { 257 | // handle errors... 258 | }); 259 | 260 | // send webhooks 261 | bot.configureIncomingWebhook({url: webhook_url}); 262 | bot.sendWebhook({ 263 | text: 'Hey!', 264 | channel: '#testing', 265 | },function(err,res) { 266 | // handle error 267 | }); 268 | 269 | // receive outgoing or slash commands 270 | // if you are already using Express, you can use your own server instance... 271 | // see "Use BotKit with an Express web server" 272 | controller.setupWebserver(process.env.port,function(err,webserver) { 273 | 274 | controller.createWebhookEndpoints(controller.webserver); 275 | 276 | }); 277 | 278 | controller.on('slash_command',function(bot,message) { 279 | 280 | // reply to slash command 281 | bot.replyPublic(message,'Everyone can see the results of this slash command'); 282 | 283 | }); 284 | ``` 285 | 286 | ### Incoming webhooks 287 | 288 | Incoming webhooks allow you to send data from your application into Slack. 289 | To configure Botkit to send an incoming webhook, first set one up 290 | via [Slack's integration page](https://my.slack.com/services/new/incoming-webhook/). 291 | 292 | Once configured, use the `sendWebhook` function to send messages to Slack. 293 | 294 | [Read official docs](https://api.slack.com/incoming-webhooks) 295 | 296 | #### bot.configureIncomingWebhook() 297 | 298 | | Argument | Description 299 | |--- |--- 300 | | config | Configure a bot to send webhooks 301 | 302 | Add a webhook configuration to an already spawned bot. 303 | It is preferable to spawn the bot pre-configured, but hey, sometimes 304 | you need to do it later. 305 | 306 | #### bot.sendWebhook() 307 | 308 | | Argument | Description 309 | |--- |--- 310 | | message | A message object 311 | | callback | _Optional_ Callback in the form function(err,response) { ... } 312 | 313 | Pass `sendWebhook` an object that contains at least a `text` field. 314 | This object may also contain other fields defined [by Slack](https://api.slack.com/incoming-webhooks) which can alter the 315 | appearance of your message. 316 | 317 | ```javascript 318 | var bot = controller.spawn({ 319 | incoming_webhook: { 320 | url: 321 | } 322 | }) 323 | 324 | bot.sendWebhook({ 325 | text: 'This is an incoming webhook', 326 | channel: '#general', 327 | },function(err,res) { 328 | if (err) { 329 | // ... 330 | } 331 | }); 332 | ``` 333 | 334 | ### Outgoing Webhooks and Slash commands 335 | 336 | Outgoing webhooks and Slash commands allow you to send data out of Slack. 337 | 338 | Outgoing webhooks are used to match keywords or phrases in Slack. [Read Slack's official documentation here.](https://api.slack.com/outgoing-webhooks) 339 | 340 | Slash commands are special commands triggered by typing a "/" then a command. 341 | [Read Slack's official documentation here.](https://api.slack.com/slash-commands) 342 | 343 | Though these integrations are subtly different, Botkit normalizes the details 344 | so developers may focus on providing useful functionality rather than peculiarities 345 | of the Slack API parameter names. 346 | 347 | Note that since these integrations use send webhooks from Slack to your application, 348 | your application will have to be hosted at a public IP address or domain name, 349 | and properly configured within Slack. 350 | 351 | [Set up an outgoing webhook](https://my.slack.com/services/new/outgoing-webhook) 352 | 353 | [Set up a Slash command](https://my.slack.com/services/new/slash-commands) 354 | 355 | ```javascript 356 | controller.setupWebserver(port,function(err,express_webserver) { 357 | controller.createWebhookEndpoints(express_webserver) 358 | }); 359 | ``` 360 | 361 | #### Securing Outgoing Webhooks and Slash commands 362 | 363 | You can optionally protect your application with authentication of the requests 364 | from Slack. Slack will generate a unique request token for each Slash command and 365 | outgoing webhook (see [Slack documentation](https://api.slack.com/slash-commands#validating_the_command)). 366 | You can configure the web server to validate that incoming requests contain a valid api token 367 | by adding an express middleware authentication module. 368 | 369 | ```javascript 370 | controller.setupWebserver(port,function(err,express_webserver) { 371 | controller.createWebhookEndpoints(express_webserver, ['AUTH_TOKEN', 'ANOTHER_AUTH_TOKEN']); 372 | // you can pass the tokens as an array, or variable argument list 373 | //controller.createWebhookEndpoints(express_webserver, 'AUTH_TOKEN_1', 'AUTH_TOKEN_2'); 374 | // or 375 | //controller.createWebhookEndpoints(express_webserver, 'AUTH_TOKEN'); 376 | }); 377 | ``` 378 | 379 | #### Handling `slash_command` and `outgoing_webhook` events 380 | 381 | ``` 382 | controller.on('slash_command',function(bot,message) { 383 | 384 | // reply to slash command 385 | bot.replyPublic(message,'Everyone can see this part of the slash command'); 386 | bot.replyPrivate(message,'Only the person who used the slash command can see this.'); 387 | 388 | }) 389 | 390 | controller.on('outgoing_webhook',function(bot,message) { 391 | 392 | // reply to outgoing webhook command 393 | bot.replyPublic(message,'Everyone can see the results of this webhook command'); 394 | 395 | }) 396 | ``` 397 | 398 | #### controller.setupWebserver() 399 | | Argument | Description 400 | |--- |--- 401 | | port | port for webserver 402 | | callback | callback function 403 | 404 | Setup an [Express webserver](http://expressjs.com/en/index.html) for 405 | use with `createWebhookEndpoints()` 406 | 407 | If you need more than a simple webserver to receive webhooks, 408 | you should by all means create your own Express webserver! 409 | 410 | The callback function receives the Express object as a parameter, 411 | which may be used to add further web server routes. 412 | 413 | #### controller.createWebhookEndpoints() 414 | 415 | This function configures the route `http://_your_server_/slack/receive` 416 | to receive webhooks from Slack. 417 | 418 | This url should be used when configuring Slack. 419 | 420 | When a slash command is received from Slack, Botkit fires the `slash_command` event. 421 | 422 | When an outgoing webhook is recieved from Slack, Botkit fires the `outgoing_webhook` event. 423 | 424 | 425 | #### bot.replyPublic() 426 | 427 | | Argument | Description 428 | |--- |--- 429 | | src | source message as received from slash or webhook 430 | | reply | reply message (string or object) 431 | | callback | optional callback 432 | 433 | When used with outgoing webhooks, this function sends an immediate response that is visible to everyone in the channel. 434 | 435 | When used with slash commands, this function has the same functionality. However, 436 | slash commands also support private, and delayed messages. See below. 437 | [View Slack's docs here](https://api.slack.com/slash-commands) 438 | 439 | #### bot.replyPrivate() 440 | 441 | | Argument | Description 442 | |--- |--- 443 | | src | source message as received from slash 444 | | reply | reply message (string or object) 445 | | callback | optional callback 446 | 447 | 448 | #### bot.replyPublicDelayed() 449 | 450 | | Argument | Description 451 | |--- |--- 452 | | src | source message as received from slash 453 | | reply | reply message (string or object) 454 | | callback | optional callback 455 | 456 | #### bot.replyPrivateDelayed() 457 | 458 | | Argument | Description 459 | |--- |--- 460 | | src | source message as received from slash 461 | | reply | reply message (string or object) 462 | | callback | optional callback 463 | 464 | 465 | 466 | ### Using the Slack Web API 467 | 468 | All (or nearly all - they change constantly!) of Slack's current web api methods are supported 469 | using a syntax designed to match the endpoints themselves. 470 | 471 | If your bot has the appropriate scope, it may call [any of these method](https://api.slack.com/methods) using this syntax: 472 | 473 | ```javascript 474 | bot.api.channels.list({},function(err,response) { 475 | //Do something... 476 | }) 477 | ``` 478 | 479 | 480 | 481 | ## Use the Slack Button 482 | 483 | The [Slack Button](https://api.slack.com/docs/slack-button) is a way to offer a Slack 484 | integration as a service available to multiple teams. Botkit includes a framework 485 | on top of which Slack Button applications can be built. 486 | 487 | Slack button applications can use one or more of the [real time API](http://api.slack.com/rtm), 488 | [incoming webhook](http://api.slack.com/incoming-webhooks) and [slash command](http://api.slack.com/slash-commands) integrations, which can be 489 | added *automatically* to a team using a special oauth scope. 490 | 491 | If special oauth scopes sounds scary, this is probably not for you! 492 | The Slack Button is useful for developers who want to offer a service 493 | to multiple teams. 494 | 495 | How many teams can a Slack button app built using Botkit handle? 496 | This will largely be dependent on the environment it is hosted in and the 497 | type of integrations used. A reasonably well equipped host server should 498 | be able to easily handle _at least one hundred_ real time connections at once. 499 | 500 | To handle more than one hundred bots at once, [consider speaking to the 501 | creators of Botkit at Howdy.ai](http://howdy.ai) 502 | 503 | For Slack button applications, Botkit provides: 504 | 505 | * A simple webserver 506 | * OAuth Endpoints for login via Slack 507 | * Storage of API tokens and team data via built-in Storage 508 | * Events for when a team joins, a new integration is added, and others... 509 | 510 | See the [included examples](readme.md#included-examples) for several ready to use example apps. 511 | 512 | #### controller.configureSlackApp() 513 | 514 | | Argument | Description 515 | |--- |--- 516 | | config | configuration object containing clientId, clientSecret, redirectUri and scopes 517 | 518 | Configure Botkit to work with a Slack application. 519 | 520 | Get a clientId and clientSecret from [Slack's API site](https://api.slack.com/applications). 521 | Configure Slash command, incoming webhook, or bot user integrations on this site as well. 522 | 523 | Configuration must include: 524 | 525 | * clientId - Application clientId from Slack 526 | * clientSecret - Application clientSecret from Slack 527 | * redirectUri - the base url of your application 528 | * scopes - an array of oauth permission scopes 529 | 530 | Slack has [_many, many_ oauth scopes](https://api.slack.com/docs/oauth-scopes) 531 | that can be combined in different ways. There are also [_special oauth scopes_ 532 | used when requesting Slack Button integrations](https://api.slack.com/docs/slack-button). 533 | It is important to understand which scopes your application will need to function, 534 | as without the proper permission, your API calls will fail. 535 | 536 | #### controller.createOauthEndpoints() 537 | | Argument | Description 538 | |--- |--- 539 | | webserver | an Express webserver Object 540 | | error_callback | function to handle errors that may occur during oauth 541 | 542 | Call this function to create two web urls that handle login via Slack. 543 | 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 544 | the "Redirect URI" field of your application on Slack's API site. 545 | 546 | 547 | ```javascript 548 | var Botkit = require('botkit'); 549 | var controller = Botkit.slackbot(); 550 | 551 | controller.configureSlackApp({ 552 | clientId: process.env.clientId, 553 | clientSecret: process.env.clientSecret, 554 | redirectUri: 'http://localhost:3002', 555 | scopes: ['incoming-webhook','team:read','users:read','channels:read','im:read','im:write','groups:read','emoji:read','chat:write:bot'] 556 | }); 557 | 558 | controller.setupWebserver(process.env.port,function(err,webserver) { 559 | 560 | // set up web endpoints for oauth, receiving webhooks, etc. 561 | controller 562 | .createHomepageEndpoint(controller.webserver) 563 | .createOauthEndpoints(controller.webserver,function(err,req,res) { ... }) 564 | .createWebhookEndpoints(controller.webserver); 565 | 566 | }); 567 | 568 | ``` 569 | 570 | ### How to identify what team your message came from 571 | ```javascript 572 | bot.identifyTeam(function(err,team_id) { 573 | 574 | }) 575 | ``` 576 | 577 | 578 | ### How to identify the bot itself (for RTM only) 579 | ```javascript 580 | bot.identifyBot(function(err,identity) { 581 | // identity contains... 582 | // {name, id, team_id} 583 | }) 584 | ``` 585 | 586 | 587 | ### Slack Button specific events: 588 | 589 | | Event | Description 590 | |--- |--- 591 | | create_incoming_webhook | 592 | | create_bot | 593 | | update_team | 594 | | create_team | 595 | | create_user | 596 | | update_user | 597 | | oauth_error | 598 | -------------------------------------------------------------------------------- /lib/SlackBot.js: -------------------------------------------------------------------------------- 1 | var Botkit = require(__dirname + '/CoreBot.js'); 2 | var request = require('request'); 3 | var express = require('express'); 4 | var bodyParser = require('body-parser'); 5 | 6 | function Slackbot(configuration) { 7 | 8 | // Create a core botkit bot 9 | var slack_botkit = Botkit(configuration || {}); 10 | 11 | // customize the bot definition, which will be used when new connections 12 | // spawn! 13 | slack_botkit.defineBot(require(__dirname + '/Slackbot_worker.js')); 14 | 15 | // set up configuration for oauth 16 | // slack_app_config should contain 17 | // { clientId, clientSecret, scopes} 18 | // https://api.slack.com/docs/oauth-scopes 19 | slack_botkit.configureSlackApp = function(slack_app_config, cb) { 20 | 21 | slack_botkit.log('** Configuring app as a Slack App!'); 22 | if (!slack_app_config || !slack_app_config.clientId || 23 | !slack_app_config.clientSecret || !slack_app_config.scopes) { 24 | throw new Error('Missing oauth config details', bot); 25 | } else { 26 | slack_botkit.config.clientId = slack_app_config.clientId; 27 | slack_botkit.config.clientSecret = slack_app_config.clientSecret; 28 | if (slack_app_config.redirectUri) slack_botkit.config.redirectUri = slack_app_config.redirectUri; 29 | if (typeof(slack_app_config.scopes) == 'string') { 30 | slack_botkit.config.scopes = slack_app_config.scopes.split(/\,/); 31 | } else { 32 | slack_botkit.config.scopes = slack_app_config.scopes; 33 | } 34 | if (cb) cb(null, bot); 35 | } 36 | 37 | return slack_botkit; 38 | 39 | }; 40 | 41 | // set up a web route that is a landing page 42 | slack_botkit.createHomepageEndpoint = function(webserver) { 43 | 44 | slack_botkit.log('** Serving app landing page at : http://MY_HOST:' + slack_botkit.config.port + '/'); 45 | 46 | // FIX THIS!!! 47 | // this is obvs not right. 48 | webserver.get('/', function(req, res) { 49 | 50 | res.send('Howdy!'); 51 | 52 | }); 53 | 54 | return slack_botkit; 55 | 56 | }; 57 | 58 | 59 | // adds the webhook authentication middleware module to the webserver 60 | function secureWebhookEndpoints() { 61 | var authenticationMiddleware = require(__dirname + '/middleware/slack_authentication.js'); 62 | // convert a variable argument list to an array, drop the webserver argument 63 | var tokens = Array.prototype.slice.call(arguments); 64 | var webserver = tokens.shift(); 65 | 66 | slack_botkit.log( 67 | '** Requiring token authentication for webhook endpoints for Slash commands ' + 68 | 'and outgoing webhooks; configured ' + tokens.length + ' token(s)' 69 | ); 70 | 71 | webserver.use(authenticationMiddleware(tokens)); 72 | } 73 | 74 | // set up a web route for receiving outgoing webhooks and/or slash commands 75 | slack_botkit.createWebhookEndpoints = function(webserver, authenticationTokens) { 76 | slack_botkit.log( 77 | '** Serving webhook endpoints for Slash commands and outgoing ' + 78 | 'webhooks at: http://MY_HOST:' + slack_botkit.config.port + '/slack/receive'); 79 | webserver.post('/slack/receive', function(req, res) { 80 | 81 | if (authenticationTokens !== undefined && arguments.length > 1 && arguments[1].length) { 82 | secureWebhookEndpoints.apply(null, arguments); 83 | } 84 | 85 | // this is a slash command 86 | if (req.body.command) { 87 | var message = {}; 88 | 89 | for (var key in req.body) { 90 | message[key] = req.body[key]; 91 | } 92 | 93 | // let's normalize some of these fields to match the rtm message format 94 | message.user = message.user_id; 95 | message.channel = message.channel_id; 96 | 97 | // Is this configured to use Slackbutton? 98 | // If so, validate this team before triggering the event! 99 | // Otherwise, it's ok to just pass a generic bot in 100 | if (slack_botkit.config.clientId && slack_botkit.config.clientSecret) { 101 | 102 | slack_botkit.findTeamById(message.team_id, function(err, team) { 103 | if (err || !team) { 104 | slack_botkit.log.error('Received slash command, but could not load team'); 105 | } else { 106 | message.type = 'slash_command'; 107 | // HEY THERE 108 | // Slash commands can actually just send back a response 109 | // and have it displayed privately. That means 110 | // the callback needs access to the res object 111 | // to send an optional response. 112 | 113 | res.status(200); 114 | 115 | var bot = slack_botkit.spawn(team); 116 | 117 | bot.team_info = team; 118 | bot.res = res; 119 | 120 | slack_botkit.receiveMessage(bot, message); 121 | 122 | } 123 | }); 124 | } else { 125 | 126 | message.type = 'slash_command'; 127 | // HEY THERE 128 | // Slash commands can actually just send back a response 129 | // and have it displayed privately. That means 130 | // the callback needs access to the res object 131 | // to send an optional response. 132 | 133 | var team = { 134 | id: message.team_id, 135 | }; 136 | 137 | res.status(200); 138 | 139 | var bot = slack_botkit.spawn({}); 140 | 141 | bot.team_info = team; 142 | bot.res = res; 143 | 144 | slack_botkit.receiveMessage(bot, message); 145 | 146 | } 147 | 148 | } else if (req.body.trigger_word) { 149 | 150 | var message = {}; 151 | 152 | for (var key in req.body) { 153 | message[key] = req.body[key]; 154 | } 155 | 156 | 157 | var team = { 158 | id: message.team_id, 159 | }; 160 | 161 | // let's normalize some of these fields to match the rtm message format 162 | message.user = message.user_id; 163 | message.channel = message.channel_id; 164 | 165 | message.type = 'outgoing_webhook'; 166 | 167 | res.status(200); 168 | 169 | var bot = slack_botkit.spawn(team); 170 | bot.res = res; 171 | bot.team_info = team; 172 | 173 | 174 | slack_botkit.receiveMessage(bot, message); 175 | 176 | // outgoing webhooks are also different. They can simply return 177 | // a response instead of using the API to reply. Maybe this is 178 | // a different type of event!! 179 | 180 | } 181 | 182 | }); 183 | 184 | return slack_botkit; 185 | }; 186 | 187 | slack_botkit.saveTeam = function(team, cb) { 188 | slack_botkit.storage.teams.save(team, cb); 189 | }; 190 | 191 | // look up a team's memory and configuration and return it, or 192 | // return an error! 193 | slack_botkit.findTeamById = function(id, cb) { 194 | slack_botkit.storage.teams.get(id, cb); 195 | }; 196 | 197 | slack_botkit.setupWebserver = function(port, cb) { 198 | 199 | if (!port) { 200 | throw new Error('Cannot start webserver without a port'); 201 | } 202 | if (isNaN(port)) { 203 | throw new Error('Specified port is not a valid number'); 204 | } 205 | 206 | slack_botkit.config.port = port; 207 | 208 | slack_botkit.webserver = express(); 209 | slack_botkit.webserver.use(bodyParser.json()); 210 | slack_botkit.webserver.use(bodyParser.urlencoded({ extended: true })); 211 | slack_botkit.webserver.use(express.static(__dirname + '/public')); 212 | 213 | var server = slack_botkit.webserver.listen( 214 | slack_botkit.config.port, 215 | function() { 216 | slack_botkit.log('** Starting webserver on port ' + 217 | slack_botkit.config.port); 218 | if (cb) { cb(null, slack_botkit.webserver); } 219 | }); 220 | 221 | return slack_botkit; 222 | 223 | }; 224 | 225 | // get a team url to redirect the user through oauth process 226 | slack_botkit.getAuthorizeURL = function(team_id) { 227 | 228 | var url = 'https://slack.com/oauth/authorize'; 229 | var scopes = slack_botkit.config.scopes; 230 | url = url + '?client_id=' + slack_botkit.config.clientId + '&scope=' + 231 | scopes.join(',') + '&state=botkit'; 232 | 233 | if (team_id) { 234 | url = url + '&team=' + team_id; 235 | } 236 | if (slack_botkit.config.redirectUri) { 237 | url = url + '&redirect_uri=' + slack_botkit.config.redirectUri; 238 | } 239 | 240 | return url; 241 | 242 | }; 243 | 244 | // set up a web route for redirecting users 245 | // and collecting authentication details 246 | // https://api.slack.com/docs/oauth 247 | // https://api.slack.com/docs/oauth-scopes 248 | slack_botkit.createOauthEndpoints = function(webserver, callback) { 249 | 250 | slack_botkit.log('** Serving login URL: http://MY_HOST:' + slack_botkit.config.port + '/login'); 251 | 252 | if (!slack_botkit.config.clientId) { 253 | throw new Error( 254 | 'Cannot create oauth endpoints without calling configureSlackApp() with a clientId first'); 255 | } 256 | if (!slack_botkit.config.clientSecret) { 257 | throw new Error( 258 | 'Cannot create oauth endpoints without calling configureSlackApp() with a clientSecret first'); 259 | } 260 | if (!slack_botkit.config.scopes) { 261 | throw new Error( 262 | 'Cannot create oauth endpoints without calling configureSlackApp() with a list of scopes first'); 263 | } 264 | 265 | var call_api = function(command, options, cb) { 266 | slack_botkit.log('** API CALL: ' + 'https://slack.com/api/' + command); 267 | request.post('https://slack.com/api/' + command, function(error, response, body) { 268 | slack_botkit.debug('Got response', error, body); 269 | if (!error && response.statusCode == 200) { 270 | var json = JSON.parse(body); 271 | if (json.ok) { 272 | if (cb) cb(null, json); 273 | } else { 274 | if (cb) cb(json.error, json); 275 | } 276 | } else { 277 | if (cb) cb(error); 278 | } 279 | }).form(options); 280 | }; 281 | 282 | var oauth_access = function(options, cb) { 283 | call_api('oauth.access', options, cb); 284 | }; 285 | 286 | var auth_test = function(options, cb) { 287 | call_api('auth.test', options, cb); 288 | }; 289 | 290 | webserver.get('/login', function(req, res) { 291 | res.redirect(slack_botkit.getAuthorizeURL()); 292 | }); 293 | 294 | slack_botkit.log('** Serving oauth return endpoint: http://MY_HOST:' + slack_botkit.config.port + '/oauth'); 295 | 296 | webserver.get('/oauth', function(req, res) { 297 | 298 | var code = req.query.code; 299 | var state = req.query.state; 300 | 301 | var opts = { 302 | client_id: slack_botkit.config.clientId, 303 | client_secret: slack_botkit.config.clientSecret, 304 | code: code 305 | }; 306 | 307 | if (slack_botkit.config.redirectUri) opts.redirect_uri = slack_botkit.config.redirectUri; 308 | 309 | oauth_access(opts, function(err, auth) { 310 | 311 | if (err) { 312 | if (callback) { 313 | callback(err, req, res); 314 | } else { 315 | res.status(500).send(err); 316 | } 317 | slack_botkit.trigger('oauth_error', [err]); 318 | } else { 319 | 320 | // auth contains at least: 321 | // { access_token, scope, team_name} 322 | // May also contain: 323 | // { team_id } (not in incoming_webhook scope) 324 | // info about incoming webhooks: 325 | // { incoming_webhook: { url, channel, configuration_url} } 326 | // might also include slash commands: 327 | // { commands: ??} 328 | 329 | // what scopes did we get approved for? 330 | var scopes = auth.scope.split(/\,/); 331 | 332 | // temporarily use the token we got from the oauth 333 | // we need to call auth.test to make sure the token is valid 334 | // but also so that we reliably have the team_id field! 335 | //slack_botkit.config.token = auth.access_token; 336 | auth_test({token: auth.access_token}, function(err, identity) { 337 | 338 | if (err) { 339 | if (callback) { 340 | callback(err, req, res); 341 | } else { 342 | res.status(500).send(err); 343 | } 344 | 345 | slack_botkit.trigger('oauth_error', [err]); 346 | 347 | } else { 348 | req.identity = identity; 349 | 350 | // we need to deal with any team-level provisioning info 351 | // like incoming webhooks and bot users 352 | // and also with the personal access token from the user 353 | 354 | slack_botkit.findTeamById(identity.team_id, function(err, team) { 355 | 356 | var isnew = false; 357 | if (!team) { 358 | isnew = true; 359 | team = { 360 | id: identity.team_id, 361 | createdBy: identity.user_id, 362 | url: identity.url, 363 | name: identity.team, 364 | }; 365 | } 366 | 367 | var bot = slack_botkit.spawn(team); 368 | 369 | if (auth.incoming_webhook) { 370 | auth.incoming_webhook.token = auth.access_token; 371 | auth.incoming_webhook.createdBy = identity.user_id; 372 | team.incoming_webhook = auth.incoming_webhook; 373 | bot.configureIncomingWebhook(team.incoming_webhook); 374 | slack_botkit.trigger('create_incoming_webhook', [bot, team.incoming_webhook]); 375 | } 376 | 377 | if (auth.bot) { 378 | team.bot = { 379 | token: auth.bot.bot_access_token, 380 | user_id: auth.bot.bot_user_id, 381 | createdBy: identity.user_id, 382 | }; 383 | bot.configureRTM(team.bot); 384 | slack_botkit.trigger('create_bot', [bot, team.bot]); 385 | } 386 | 387 | slack_botkit.saveTeam(team, function(err, id) { 388 | if (err) { 389 | slack_botkit.log.error('An error occurred while saving a team: ', err); 390 | if (callback) { 391 | callback(err, req, res); 392 | } else { 393 | res.status(500).send(err); 394 | } 395 | slack_botkit.trigger('error', [err]); 396 | } else { 397 | if (isnew) { 398 | slack_botkit.trigger('create_team', [bot, team]); 399 | } else { 400 | slack_botkit.trigger('update_team', [bot, team]); 401 | } 402 | 403 | slack_botkit.storage.users.get(identity.user_id, function(err, user) { 404 | isnew = false; 405 | if (!user) { 406 | isnew = true; 407 | user = { 408 | id: identity.user_id, 409 | access_token: auth.access_token, 410 | scopes: scopes, 411 | team_id: identity.team_id, 412 | user: identity.user, 413 | }; 414 | } 415 | slack_botkit.storage.users.save(user, function(err, id) { 416 | 417 | if (err) { 418 | slack_botkit.log.error( 419 | 'An error occurred while saving a user: ', err); 420 | if (callback) { 421 | callback(err, req, res); 422 | } else { 423 | res.status(500).send(err); 424 | } 425 | slack_botkit.trigger('error', [err]); 426 | } else { 427 | if (isnew) { 428 | slack_botkit.trigger('create_user', [bot, user]); 429 | } else { 430 | slack_botkit.trigger('update_user', [bot, user]); 431 | } 432 | if (callback) { 433 | callback(null, req, res); 434 | } else { 435 | res.redirect('/'); 436 | } 437 | } 438 | }); 439 | }); 440 | } 441 | }); 442 | }); 443 | } 444 | }); 445 | } 446 | }); 447 | }); 448 | 449 | return slack_botkit; 450 | 451 | }; 452 | 453 | slack_botkit.handleSlackEvents = function() { 454 | 455 | slack_botkit.log('** Setting up custom handlers for processing Slack messages'); 456 | slack_botkit.on('message_received', function(bot, message) { 457 | var mentionSyntax = '<@' + bot.identity.id + '(\\|' + bot.identity.name.replace('.', '\\.') + ')?>'; 458 | var mention = new RegExp(mentionSyntax, 'i'); 459 | var direct_mention = new RegExp('^' + mentionSyntax, 'i'); 460 | 461 | if (message.ok != undefined) { 462 | // this is a confirmation of something we sent. 463 | return false; 464 | } 465 | 466 | slack_botkit.debug('DEFAULT SLACK MSG RECEIVED RESPONDER'); 467 | if ('message' == message.type) { 468 | 469 | if (message.text) { 470 | message.text = message.text.trim(); 471 | } 472 | 473 | // set up a couple of special cases based on subtype 474 | if (message.subtype && message.subtype == 'channel_join') { 475 | // someone joined. maybe do something? 476 | if (message.user == bot.identity.id) { 477 | slack_botkit.trigger('bot_channel_join', [bot, message]); 478 | return false; 479 | } else { 480 | slack_botkit.trigger('user_channel_join', [bot, message]); 481 | return false; 482 | } 483 | } else if (message.subtype && message.subtype == 'group_join') { 484 | // someone joined. maybe do something? 485 | if (message.user == bot.identity.id) { 486 | slack_botkit.trigger('bot_group_join', [bot, message]); 487 | return false; 488 | } else { 489 | slack_botkit.trigger('user_group_join', [bot, message]); 490 | return false; 491 | } 492 | } else if (message.subtype) { 493 | slack_botkit.trigger(message.subtype, [bot, message]); 494 | return false; 495 | } else if (message.channel.match(/^D/)) { 496 | // this is a direct message 497 | if (message.user == bot.identity.id) { 498 | return false; 499 | } 500 | if (!message.text) { 501 | // message without text is probably an edit 502 | return false; 503 | } 504 | 505 | // remove direct mention so the handler doesn't have to deal with it 506 | message.text = message.text.replace(direct_mention, '') 507 | .replace(/^\s+/, '').replace(/^\:\s+/, '').replace(/^\s+/, ''); 508 | 509 | message.event = 'direct_message'; 510 | 511 | slack_botkit.trigger('direct_message', [bot, message]); 512 | return false; 513 | 514 | } else { 515 | if (message.user == bot.identity.id) { 516 | return false; 517 | } 518 | if (!message.text) { 519 | // message without text is probably an edit 520 | return false; 521 | } 522 | 523 | 524 | if (message.text.match(direct_mention)) { 525 | // this is a direct mention 526 | message.text = message.text.replace(direct_mention, '') 527 | .replace(/^\s+/, '').replace(/^\:\s+/, '').replace(/^\s+/, ''); 528 | message.event = 'direct_mention'; 529 | 530 | slack_botkit.trigger('direct_mention', [bot, message]); 531 | return false; 532 | } else if (message.text.match(mention)) { 533 | message.event = 'mention'; 534 | slack_botkit.trigger('mention', [bot, message]); 535 | return false; 536 | } else { 537 | message.event = 'ambient'; 538 | slack_botkit.trigger('ambient', [bot, message]); 539 | return false; 540 | 541 | } 542 | } 543 | } else { 544 | // this is a non-message object, so trigger a custom event based on the type 545 | slack_botkit.trigger(message.type, [bot, message]); 546 | } 547 | }); 548 | }; 549 | 550 | // set up the RTM message handlers once 551 | slack_botkit.handleSlackEvents(); 552 | 553 | return slack_botkit; 554 | }; 555 | 556 | module.exports = Slackbot; 557 | --------------------------------------------------------------------------------