UNPKG

mangrove-botkit

Version:

Building blocks for Building Bots

794 lines (627 loc) 31.9 kB
var Botkit = require(__dirname + '/CoreBot.js'); var request = require('request'); var querystring = require('querystring'); var async = require('async'); function Slackbot(configuration) { // Create a core botkit bot var slack_botkit = Botkit(configuration || {}); var app_name = configuration.app_name || 'mangrove-bot'; // Set some default configurations unless they've already been set. // Should the RTM connections ingest received messages // Developers using the new Events API will set this to false // This allows an RTM connection to be kept alive (so bot appears online) // but receive messages only via events api if (slack_botkit.config.rtm_receive_messages === undefined) { slack_botkit.config.rtm_receive_messages = true; } var spawned_bots = []; // customize the bot definition, which will be used when new connections // spawn! slack_botkit.defineBot(require(__dirname + '/Slackbot_worker.js')); // Middleware to track spawned bots and connect existing RTM bots to incoming webhooks slack_botkit.middleware.spawn.use(function(worker, next) { // lets first check and make sure we don't already have a bot // for this team! If we already have an RTM connection, copy it // into the new bot so it can be used for replies. var existing_bot = null; if (worker.config.id) { for (var b = 0; b < spawned_bots.length; b++) { if (spawned_bots[b].config.id) { if (spawned_bots[b].config.id == worker.config.id) { // WAIT! We already have a bot spawned here. // so instead of using the new one, use the exist one. existing_bot = spawned_bots[b]; } } } } if (!existing_bot && worker.config.id) { spawned_bots.push(worker); } else if (existing_bot) { if (existing_bot.rtm) { worker.rtm = existing_bot.rtm; } } next(); }); // set up configuration for oauth // slack_app_config should contain // { clientId, clientSecret, scopes} // https://api.slack.com/docs/oauth-scopes slack_botkit.configureSlackApp = function(slack_app_config, cb) { slack_botkit.log('** Configuring app as a Slack App!'); if (!slack_app_config || !slack_app_config.clientId || !slack_app_config.clientSecret || !slack_app_config.scopes) { throw new Error('Missing oauth config details'); } else { slack_botkit.config.clientId = slack_app_config.clientId; slack_botkit.config.clientSecret = slack_app_config.clientSecret; if (slack_app_config.redirectUri) slack_botkit.config.redirectUri = slack_app_config.redirectUri; if (typeof(slack_app_config.scopes) == 'string') { slack_botkit.config.scopes = slack_app_config.scopes.split(/\,/); } else { slack_botkit.config.scopes = slack_app_config.scopes; } if (cb) cb(null); } return slack_botkit; }; // set up a web route that is a landing page slack_botkit.createHomepageEndpoint = function(webserver) { slack_botkit.log('** Serving app landing page at : http://' + slack_botkit.config.hostname + '/' + app_name); // FIX THIS!!! // this is obvs not right. webserver.get('/' + app_name, function(req, res) { res.send('Howdy!'); }); return slack_botkit; }; // adds the webhook authentication middleware module to the webserver function secureWebhookEndpoints() { var authenticationMiddleware = require(__dirname + '/middleware/slack_authentication.js'); // convert a variable argument list to an array, drop the webserver argument var tokens = Array.prototype.slice.call(arguments); var webserver = tokens.shift(); slack_botkit.log( '** Requiring token authentication for webhook endpoints for Slash commands ' + 'and outgoing webhooks; configured ' + tokens.length + ' token(s)' ); webserver.use(authenticationMiddleware(tokens)); } // set up a web route for receiving outgoing webhooks and/or slash commands slack_botkit.createWebhookEndpoints = function(webserver, authenticationTokens) { if (authenticationTokens !== undefined && arguments.length > 1 && arguments[1].length) { secureWebhookEndpoints.apply(null, arguments); } slack_botkit.log( '** Serving webhook endpoints for Slash commands and outgoing ' + 'webhooks at: http://' + slack_botkit.config.hostname + '/' + app_name + '/slack/receive'); webserver.post('/' + app_name + '/slack/receive', function(req, res) { // respond to Slack that the webhook has been received. res.status(200); // Now, pass the webhook into be processed slack_botkit.handleWebhookPayload(req, res); }); return slack_botkit; }; slack_botkit.findAppropriateTeam = function(payload, cb) { var found_team = null; var team_id = payload.team_id || (payload.team && payload.team.id) || null; slack_botkit.findTeamById(team_id, function(err, team) { if (team) { cb(err, team); } else { if (payload.authed_teams) { async.eachSeries(payload.authed_teams, function(team_id, next) { slack_botkit.findTeamById(team_id, function(err, team) { if (team) { found_team = team; next(); } else { next(err); } }); }, function(err) { if (!found_team) { cb(err); } else { cb(null, found_team); } }); } else { cb(new Error(`could not find team ${team_id}`)); } } }); }; slack_botkit.handleWebhookPayload = function(req, res) { // is this an events api url handshake? if (req.body.type === 'url_verification') { slack_botkit.debug('Received url handshake'); res.json({ challenge: req.body.challenge }); return; } var payload = req.body; if (payload.payload) { payload = JSON.parse(payload.payload); } slack_botkit.findAppropriateTeam(payload, function(err, team) { if (err) { slack_botkit.log.error('Could not load team while processing webhook: ', err); return; } else if (!team) { // if this is NOT a slack app, it is ok to spawn a generic bot // this is only likely to happen with custom slash commands if (!slack_botkit.config.clientId) { bot = slack_botkit.spawn({}); } else { return; } } else { // spawn a bot bot = slack_botkit.spawn(team); // Identify the bot from either team storage or identifyBot() bot.team_info = team; // The bot identity is only used in handleEventsAPI during this flow // Recent changes in Slack will break other integrations as they no longer // require a bot and therefore Slack won't send the bot information. if (payload.type === 'event_callback') { if (!team.bot) { slack_botkit.log.error('No bot identity found.'); return; } bot.identity = { id: team.bot.user_id, name: team.bot.name }; } } // include the response channel so that they can be used in // responding to slash commands and outgoing webhooks bot.res = res; // pass the payload into Botkit's message handling pipeline! slack_botkit.ingest(bot, payload, res); }); }; // Send a 200 response back to Slack to acknowledge the message. slack_botkit.middleware.ingest.use(function sendResponse(bot, message, res, next) { if (res && res.statusCode) { // this is an http response // always send a 200 res.status(200); // conditionally send a response back to Slack to acknowledge the message. // we do NOT want to respond to incoming webhooks or slash commands // as the response can be used by developers to actually deliver a reply if (!message.command && !message.trigger_word & !message.submission) { res.send(''); } } next(); }); /* do delivery confirmations for RTM messages */ slack_botkit.middleware.ingest.use(function requireDelivery(bot, message, res, next) { if (message.ok != undefined) { // this is a confirmation of something we sent. if (slack_botkit.config.require_delivery) { // loop through all active conversations this bot is having // and mark messages in conversations as delivered = true for (var t = 0; t < slack_botkit.tasks.length; t++) { var task = slack_botkit.tasks[t]; if (task.isActive()) { for (var c = 0; c < task.convos.length; c++) { var convo = task.convos[c]; for (var s = 0; s < convo.sent.length; s++) { var sent = convo.sent[s]; if (sent.api_response && sent.api_response.id == message.reply_to) { sent.delivered = true; sent.api_response.ts = message.ts; } } } } } } return false; } next(); }); slack_botkit.middleware.categorize.use(function(bot, message, next) { var mentionSyntax = '<@' + bot.identity.id + '(\\|' + bot.identity.name.replace('.', '\\.') + ')?>'; var mention = new RegExp(mentionSyntax, 'i'); var direct_mention = new RegExp('^' + mentionSyntax, 'i'); if ('message' == message.type) { if (message.text) { message.text = message.text.trim(); } // set up a couple of special cases based on subtype if (message.subtype && message.subtype == 'channel_join') { // someone joined. maybe do something? if (message.user == bot.identity.id) { message.type = 'bot_channel_join'; } else { message.type = 'user_channel_join'; } } else if (message.subtype && message.subtype == 'group_join') { // someone joined. maybe do something? if (message.user == bot.identity.id) { message.type = 'bot_group_join'; } else { message.type = 'user_group_join'; } } else if (message.subtype) { message.type = message.subtype; } else if (message.channel.match(/^D/)) { // this is a direct message message.type = 'direct_message'; if (message.user == bot.identity.id && message.bot_id) { message.type = 'self_message'; } if (!message.text) { // message without text is probably an edit return false; } // remove direct mention so the handler doesn't have to deal with it message.text = message.text.replace(direct_mention, '') .replace(/^\s+/, '').replace(/^\:\s+/, '').replace(/^\s+/, ''); } else { if (!message.text) { // message without text is probably an edit return false; } if (message.text.match(direct_mention)) { // this is a direct mention message.text = message.text.replace(direct_mention, '') .replace(/^\s+/, '').replace(/^\:\s+/, '').replace(/^\s+/, ''); message.type = 'direct_mention'; } else if (message.text.match(mention)) { message.type = 'mention'; } else { message.type = 'ambient'; } if (message.user == bot.identity.id && message.bot_id) { message.type = 'self_message'; } } } // move on to the next stage of the pipeline next(); }); /* Handler functions for the various ways Slack might send a message to * Botkit via webhooks. These include interactive messages (button clicks), * events api (messages sent over web hook), slash commands, and outgoing webhooks * (patterns matched in slack that result in a webhook) */ slack_botkit.middleware.normalize.use(function handleInteractiveMessage(bot, message, next) { if (message.callback_id) { // let's normalize some of these fields to match the rtm message format message.user = message.user.id; message.channel = message.channel.id; // put the action value in the text field // this allows button clicks to respond to asks if (message.type == 'interactive_message') { message.text = message.actions[0].value; // handle menus too! // take the first selected item // TODO: When Slack supports multi-select menus, this will need an update! if (message.actions[0].selected_options) { message.text = message.actions[0].selected_options[0].value; } message.type = 'interactive_message_callback'; } else if (message.type == 'dialog_submission') { // message.submissions is where the stuff is } } next(); }); slack_botkit.middleware.normalize.use(function handleEventsAPI(bot, message, next) { if (message.type == 'event_callback') { // var message = {}; for (var key in message.event) { message[key] = message.event[key]; } // let's normalize some of these fields to match the rtm message format message.team = message.team_id; message.events_api = true; message.authed_users = message.authed_users; if (bot.identity == undefined || bot.identity.id == null) { console.error('Could not identify bot'); return; } else if (bot.identity.id === message.user && message.subtype !== 'channel_join' && message.subtype !== 'group_join') { console.error('Got event from this bot user, ignoring it'); return; } } next(); }); slack_botkit.middleware.normalize.use(function handleSlashCommand(bot, message, next) { if (message.command) { message.user = message.user_id; message.channel = message.channel_id; message.type = 'slash_command'; } next(); }); slack_botkit.middleware.normalize.use(function handleOutgoingWebhook(bot, message, next) { if (message.trigger_word) { message.user = message.user_id; message.channel = message.channel_id; message.type = 'outgoing_webhook'; } next(); }); slack_botkit.middleware.format.use(function formatForSlack(bot, message, platform_message, next) { platform_message.type = message.type || 'message'; platform_message.channel = message.channel; platform_message.text = message.text || null; platform_message.username = message.username || null; platform_message.thread_ts = message.thread_ts || null; platform_message.reply_broadcast = message.reply_broadcast || null; platform_message.parse = message.parse || null; platform_message.link_names = message.link_names || null; platform_message.attachments = message.attachments ? JSON.stringify(message.attachments) : null; platform_message.unfurl_links = typeof message.unfurl_links !== 'undefined' ? message.unfurl_links : null; platform_message.unfurl_media = typeof message.unfurl_media !== 'undefined' ? message.unfurl_media : null; platform_message.icon_url = message.icon_url || null; platform_message.icon_emoji = message.icon_emoji || null; // should this message be sent as an ephemeral message if (message.ephemeral) { platform_message.ephemeral = true; platform_message.user = message.user; } if (platform_message.icon_url || platform_message.icon_emoji || platform_message.username) { platform_message.as_user = false; } else { platform_message.as_user = platform_message.as_user || true; } next(); }); /* End of webhook handler functions * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ slack_botkit.saveTeam = function(team, cb) { slack_botkit.storage.teams.save(team, cb); }; // look up a team's memory and configuration and return it, or // return an error! slack_botkit.findTeamById = function(id, cb) { slack_botkit.storage.teams.get(id, cb); }; // get a team url to redirect the user through oauth process slack_botkit.getAuthorizeURL = function(team_id, redirect_params) { var scopes = slack_botkit.config.scopes; var api_root = slack_botkit.config.api_root ? slack_botkit.config.api_root : 'https://slack.com'; var url = api_root + '/oauth/authorize' + '?client_id=' + slack_botkit.config.clientId + '&scope=' + scopes.join(',') + '&state=botkit'; if (team_id) url += '&team=' + team_id; if (slack_botkit.config.redirectUri) { var redirect_query = ''; var redirect_uri = slack_botkit.config.redirectUri; if (redirect_params) { redirect_query += encodeURIComponent(querystring.stringify(redirect_params)); redirect_uri = redirect_uri + '?' + redirect_query; } url += '&redirect_uri=' + redirect_uri; } return url; }; // set up a web route for redirecting users // and collecting authentication details // https://api.slack.com/docs/oauth // https://api.slack.com/docs/oauth-scopes slack_botkit.createOauthEndpoints = function(webserver, callback) { slack_botkit.log('** Serving login URL: http://' + slack_botkit.config.hostname + '/' + app_name + '/login'); if (!slack_botkit.config.clientId) { throw new Error( 'Cannot create oauth endpoints without calling configureSlackApp() with a clientId first'); } if (!slack_botkit.config.clientSecret) { throw new Error( 'Cannot create oauth endpoints without calling configureSlackApp() with a clientSecret first'); } if (!slack_botkit.config.scopes) { throw new Error( 'Cannot create oauth endpoints without calling configureSlackApp() with a list of scopes first'); } var call_api = function(command, options, cb) { var api_root = slack_botkit.config.api_root ? slack_botkit.config.api_root : 'https://slack.com'; slack_botkit.log('** API CALL: ' + api_root + '/api/' + command); request.post(api_root + '/api/' + command, function(error, response, body) { slack_botkit.debug('Got response', error, body); if (!error && response.statusCode == 200) { var json = JSON.parse(body); if (json.ok) { if (cb) cb(null, json); } else { if (cb) cb(json.error, json); } } else { if (cb) cb(error); } }).form(options); }; var oauth_access = function(options, cb) { call_api('oauth.access', options, cb); }; var auth_test = function(options, cb) { call_api('auth.test', options, cb); }; webserver.get('/' + app_name + '/login', function(req, res) { res.redirect(slack_botkit.getAuthorizeURL()); }); slack_botkit.log('** Serving oauth return endpoint: http://' + slack_botkit.config.hostname + '/' + app_name + '/oauth'); webserver.get('/' + app_name + '/oauth', function(req, res) { var code = req.query.code; var state = req.query.state; var opts = { client_id: slack_botkit.config.clientId, client_secret: slack_botkit.config.clientSecret, code: code }; var redirect_params = {}; if (slack_botkit.config.redirectUri) { Object.assign(redirect_params, req.query); delete redirect_params.code; delete redirect_params.state; var redirect_query = querystring.stringify(redirect_params); var redirect_uri = slack_botkit.config.redirectUri; if (redirect_query) { redirect_uri = redirect_uri + '?' + redirect_query; } opts.redirect_uri = redirect_uri; } oauth_access(opts, function(err, auth) { if (err) { if (callback) { callback(err, req, res); } else { res.status(500).send(err); } slack_botkit.trigger('oauth_error', [err]); } else { // auth contains at least: // { access_token, scope, team_name} // May also contain: // { team_id } (not in incoming_webhook scope) // info about incoming webhooks: // { incoming_webhook: { url, channel, configuration_url} } // might also include slash commands: // { commands: ??} // what scopes did we get approved for? var scopes = auth.scope.split(/\,/); // temporarily use the token we got from the oauth // we need to call auth.test to make sure the token is valid // but also so that we reliably have the team_id field! //slack_botkit.config.token = auth.access_token; auth_test({token: auth.access_token}, function(err, identity) { if (err) { if (callback) { callback(err, req, res); } else { res.status(500).send(err); } slack_botkit.trigger('oauth_error', [err]); } else { req.identity = identity; // we need to deal with any team-level provisioning info // like incoming webhooks and bot users // and also with the personal access token from the user slack_botkit.findTeamById(identity.team_id, function(err, team) { var isnew = false; if (!team) { isnew = true; team = { id: identity.team_id, createdBy: identity.user_id, url: identity.url, name: identity.team, }; } team.state = state; var bot = slack_botkit.spawn(team); if (auth.incoming_webhook) { auth.incoming_webhook.token = auth.access_token; auth.incoming_webhook.createdBy = identity.user_id; team.incoming_webhook = auth.incoming_webhook; bot.configureIncomingWebhook(team.incoming_webhook); slack_botkit.trigger('create_incoming_webhook', [bot, team.incoming_webhook]); } if (auth.bot) { team.bot = { token: auth.bot.bot_access_token, user_id: auth.bot.bot_user_id, createdBy: identity.user_id, app_token: auth.access_token, }; bot.configureRTM(team.bot); slack_botkit.trigger('create_bot', [bot, team.bot]); } slack_botkit.saveTeam(team, function(err, id) { if (err) { slack_botkit.log.error('An error occurred while saving a team: ', err); if (callback) { callback(err, req, res); } else { res.status(500).send(err); } slack_botkit.trigger('error', [err]); } else { if (isnew) { slack_botkit.trigger('create_team', [bot, team]); } else { slack_botkit.trigger('update_team', [bot, team]); } if (team.bot) { // call auth test on the bot token // to capture its name auth_test({ token: team.bot.token }, function(err, auth_data) { team.bot.name = auth_data.user; slack_botkit.saveTeam(team, function(err, id) { if (err) { slack_botkit.log.error('An error occurred while saving a team: ', err); } }); }); } slack_botkit.storage.users.get(identity.user_id, function(err, user) { isnew = false; if (!user) { isnew = true; user = { id: identity.user_id, team_id: identity.team_id, user: identity.user, }; } // Always update these because the token could become invalid // and scopes could change. user.access_token = auth.access_token; user.scopes = scopes; slack_botkit.storage.users.save(user, function(err, id) { if (err) { slack_botkit.log.error( 'An error occurred while saving a user: ', err); if (callback) { callback(err, req, res); } else { res.status(500).send(err); } slack_botkit.trigger('error', [err]); } else { if (isnew) { slack_botkit.trigger( 'create_user', [bot, user, redirect_params] ); } else { slack_botkit.trigger( 'update_user', [bot, user, redirect_params] ); } if (callback) { callback(null, req, res); } else { res.redirect('/'); } } }); }); } }); }); } }); } }); }); return slack_botkit; }; return slack_botkit; }; module.exports = Slackbot;