UNPKG

mangrove-botkit

Version:

Building blocks for Building Bots

717 lines (605 loc) 25.8 kB
var Botkit = require(__dirname + '/CoreBot.js'); var request = require('request'); var crypto = require('crypto'); var bodyParser = require('body-parser'); function Facebookbot(configuration) { var api_host = configuration.api_host || 'graph.facebook.com'; // Create a core botkit bot var facebook_botkit = Botkit(configuration || {}); if (facebook_botkit.config.require_delivery) { facebook_botkit.on('message_delivered', function(bot, message) { // get list of mids in this message for (var m = 0; m < message.delivery.mids.length; m++) { var mid = message.delivery.mids[m]; // loop through all active conversations this bot is having // and mark messages in conversations as delivered = true bot.findConversation(message, function(convo) { if (convo) { for (var s = 0; s < convo.sent.length; s++) { if (convo.sent[s].sent_timestamp <= message.delivery.watermark || (convo.sent[s].api_response && convo.sent[s].api_response.mid == mid)) { convo.sent[s].delivered = true; } } } }); } }); } // For backwards-compatability, support the receive_via_postback config option // this causes facebook_postback events to be replicated as message_received events // allowing them to be heard without subscribing to additional events if (facebook_botkit.config.receive_via_postback) { facebook_botkit.on('facebook_postback', function(bot, message) { facebook_botkit.trigger('message_received', [bot, message]); }); } facebook_botkit.middleware.format.use(function(bot, message, platform_message, next) { platform_message.recipient = {}; platform_message.message = message.sender_action ? undefined : {}; if (typeof(message.channel) == 'string' && message.channel.match(/\+\d+\(\d\d\d\)\d\d\d\-\d\d\d\d/)) { platform_message.recipient.phone_number = message.channel; } else { platform_message.recipient.id = message.channel; } if (!message.sender_action) { if (message.text) { platform_message.message.text = message.text; } if (message.attachment) { platform_message.message.attachment = message.attachment; } if (message.tag) { platform_message.message.tag = message.tag; } if (message.sticker_id) { platform_message.message.sticker_id = message.sticker_id; } if (message.quick_replies) { // sanitize the length of the title to maximum of 20 chars var titleLimit = function(title) { if (title.length > 20) { var newTitle = title.substring(0, 16) + '...'; return newTitle; } else { return title; } }; platform_message.message.quick_replies = message.quick_replies.map(function(item) { var quick_reply = {}; if (item.content_type === 'text' || !item.content_type) { quick_reply = { content_type: 'text', title: titleLimit(item.title), payload: item.payload, image_url: item.image_url, }; } else if (item.content_type === 'location') { quick_reply = { content_type: 'location' }; } else { // Future quick replies types } return quick_reply; }); } } else { platform_message.sender_action = message.sender_action; } if (message.sender_action) { platform_message.sender_action = message.sender_action; } if (message.notification_type) { platform_message.notification_type = message.notification_type; } next(); }); // customize the bot definition, which will be used when new connections // spawn! facebook_botkit.defineBot(function(botkit, config) { var bot = { type: 'fb', botkit: botkit, config: config || {}, utterances: botkit.utterances, }; bot.send = function(message, cb) { //Add Access Token to outgoing request message.access_token = configuration.access_token; request({ method: 'POST', json: true, headers: { 'content-type': 'application/json', }, body: message, uri: 'https://' + api_host + '/v2.6/me/messages' }, function(err, res, body) { if (err) { botkit.debug('WEBHOOK ERROR', err); return cb && cb(err); } if (body.error) { botkit.debug('API ERROR', body.error); return cb && cb(body.error.message); } botkit.debug('WEBHOOK SUCCESS', body); cb && cb(null, body); }); }; bot.startTyping = function(src, cb) { var msg = {}; msg.channel = src.channel; msg.sender_action = 'typing_on'; bot.say(msg, cb); }; bot.stopTyping = function(src, cb) { var msg = {}; msg.channel = src.channel; msg.sender_action = 'typing_off'; bot.say(msg, cb); }; bot.replyWithTyping = function(src, resp, cb) { var textLength; if (typeof(resp) == 'string') { textLength = resp.length; } else if (resp.text) { textLength = resp.text.length; } else { textLength = 80; //default attachement text length } var avgWPM = 85; var avgCPM = avgWPM * 7; var typingLength = Math.min(Math.floor(textLength / (avgCPM / 60)) * 1000, 5000); bot.startTyping(src, function(err) { if (err) console.log(err); setTimeout(function() { bot.reply(src, resp, cb); }, typingLength); }); }; bot.reply = function(src, resp, cb) { var msg = {}; if (typeof(resp) == 'string') { msg.text = resp; } else { msg = resp; } msg.channel = src.channel; msg.to = src.user; bot.say(msg, cb); }; bot.findConversation = function(message, cb) { botkit.debug('CUSTOM FIND CONVO', message.user, message.channel); for (var t = 0; t < botkit.tasks.length; t++) { for (var c = 0; c < botkit.tasks[t].convos.length; c++) { if ( botkit.tasks[t].convos[c].isActive() && botkit.tasks[t].convos[c].source_message.user == message.user ) { botkit.debug('FOUND EXISTING CONVO!'); cb(botkit.tasks[t].convos[c]); return; } } } cb(); }; return bot; }); // set up a web route for receiving outgoing webhooks and/or slash commands facebook_botkit.createWebhookEndpoints = function(webserver, bot, cb) { facebook_botkit.log( '** Serving webhook endpoints for Messenger Platform at: ' + 'http://' + facebook_botkit.config.hostname + ':' + facebook_botkit.config.port + '/facebook/receive'); webserver.post('/facebook/receive', function(req, res) { res.send('ok'); facebook_botkit.handleWebhookPayload(req, res, bot); }); webserver.get('/facebook/receive', function(req, res) { if (req.query['hub.mode'] == 'subscribe') { if (req.query['hub.verify_token'] == configuration.verify_token) { res.send(req.query['hub.challenge']); } else { res.send('OK'); } } }); if (cb) { cb(); } return facebook_botkit; }; facebook_botkit.handleWebhookPayload = function(req, res, bot) { var payload = req.body; // facebook may send more than 1 message payload at a time // we split these up into multiple message objects for ingestion if (payload.entry) { for (var e = 0; e < payload.entry.length; e++) { for (var m = 0; m < payload.entry[e].messaging.length; m++) { facebook_botkit.ingest(bot, payload.entry[e].messaging[m], res); } } } }; // universal normalizing steps // handle normal messages from users (text, stickers, files, etc count!) facebook_botkit.middleware.normalize.use(function normalizeMessage(bot, message, next) { // capture the user ID message.user = message.sender.id; // since there are only 1:1 channels on Facebook, the channel id is set to the user id message.channel = message.sender.id; // copy over some facebook specific features message.page = message.recipient.id; next(); }); // handle normal messages from users (text, stickers, files, etc count!) facebook_botkit.middleware.normalize.use(function handleMessage(bot, message, next) { if (message.message) { // capture the message text message.text = message.message.text; // copy over some facebook specific features message.seq = message.message.seq; message.is_echo = message.message.is_echo; message.mid = message.message.mid; message.sticker_id = message.message.sticker_id; message.attachments = message.message.attachments; message.quick_reply = message.message.quick_reply; message.nlp = message.message.nlp; } next(); }); // handle postback messages (when a user clicks a button) facebook_botkit.middleware.normalize.use(function handlePostback(bot, message, next) { if (message.postback) { message.text = message.postback.payload; message.payload = message.postback.payload; message.referral = message.postback.referral; message.type = 'facebook_postback'; } next(); }); // handle message sub-types facebook_botkit.middleware.categorize.use(function handleOptIn(bot, message, next) { if (message.optin) { message.type = 'facebook_optin'; } if (message.delivery) { message.type = 'message_delivered'; } if (message.read) { message.type = 'message_read'; } if (message.referral) { message.type = 'facebook_referral'; } if (message.account_linking) { message.type = 'facebook_account_linking'; } if (message.is_echo) { message.type = 'message_echo'; } next(); }); facebook_botkit.on('webserver_up', function(webserver) { // Validate that requests come from facebook, and abort on validation errors if (facebook_botkit.config.validate_requests === true) { // Load verify middleware just for post route on our receive webhook, and catch any errors it might throw to prevent the request from being parsed further. webserver.post('/facebook/receive', bodyParser.json({verify: verifyRequest})); webserver.use(abortOnValidationError); } }); var messenger_profile_api = { greeting: function(payload) { var message = { 'greeting': [] }; if (Array.isArray(payload)) { message.greeting = payload; } else { message.greeting.push({ 'locale': 'default', 'text': payload }); } facebook_botkit.api.messenger_profile.postAPI(message); }, delete_greeting: function() { facebook_botkit.api.messenger_profile.deleteAPI('greeting'); }, get_greeting: function(cb) { facebook_botkit.api.messenger_profile.getAPI('greeting', cb); }, get_started: function(payload) { var message = { 'get_started': { 'payload': payload } }; facebook_botkit.api.messenger_profile.postAPI(message); }, delete_get_started: function() { facebook_botkit.api.messenger_profile.deleteAPI('get_started'); }, get_get_started: function(cb) { facebook_botkit.api.messenger_profile.getAPI('get_started', cb); }, menu: function(payload) { var messege = { persistent_menu: payload }; facebook_botkit.api.messenger_profile.postAPI(messege); }, delete_menu: function() { facebook_botkit.api.messenger_profile.deleteAPI('persistent_menu'); }, get_menu: function(cb) { facebook_botkit.api.messenger_profile.getAPI('persistent_menu', cb); }, account_linking: function(payload) { var message = { 'account_linking_url': payload }; facebook_botkit.api.messenger_profile.postAPI(message); }, delete_account_linking: function() { facebook_botkit.api.messenger_profile.deleteAPI('account_linking_url'); }, get_account_linking: function(cb) { facebook_botkit.api.messenger_profile.getAPI('account_linking_url', cb); }, domain_whitelist: function(payload) { var message = { 'whitelisted_domains': Array.isArray(payload) ? payload : [payload] }; facebook_botkit.api.messenger_profile.postAPI(message); }, delete_domain_whitelist: function() { facebook_botkit.api.messenger_profile.deleteAPI('whitelisted_domains'); }, get_domain_whitelist: function(cb) { facebook_botkit.api.messenger_profile.getAPI('whitelisted_domains', cb); }, target_audience: function(payload) { var message = { 'target_audience': payload }; facebook_botkit.api.messenger_profile.postAPI(message); }, delete_target_audience: function() { facebook_botkit.api.messenger_profile.deleteAPI('target_audience'); }, get_target_audience: function(cb) { facebook_botkit.api.messenger_profile.getAPI('target_audience', cb); }, home_url: function(payload) { var message = { home_url: payload }; facebook_botkit.api.messenger_profile.postAPI(message); }, delete_home_url: function() { facebook_botkit.api.messenger_profile.deleteAPI('home_url'); }, get_home_url: function(cb) { facebook_botkit.api.messenger_profile.getAPI('home_url', cb); }, postAPI: function(message) { request.post('https://' + api_host + '/v2.6/me/messenger_profile?access_token=' + configuration.access_token, {form: message}, function(err, res, body) { if (err) { facebook_botkit.log('Could not configure messenger profile'); } else { var results = null; try { results = JSON.parse(body); } catch (err) { facebook_botkit.log('ERROR in messenger profile API call: Could not parse JSON', err, body); } if (results) { if (results.error) { facebook_botkit.log('ERROR in messenger profile API call: ', results.error.message); } else { facebook_botkit.debug('Successfully configured messenger profile', body); } } } }); }, deleteAPI: function(type) { var message = { 'fields': [type] }; request.delete('https://' + api_host + '/v2.6/me/messenger_profile?access_token=' + configuration.access_token, {form: message}, function(err, res, body) { if (err) { facebook_botkit.log('Could not configure messenger profile'); } else { facebook_botkit.debug('Successfully configured messenger profile', message); } }); }, getAPI: function(fields, cb) { request.get('https://' + api_host + '/v2.6/me/messenger_profile?fields=' + fields + '&access_token=' + configuration.access_token, function(err, res, body) { if (err) { facebook_botkit.log('Could not get messenger profile'); cb(err); } else { facebook_botkit.debug('Successfully got messenger profile ', body); cb(null, body); } }); }, get_messenger_code: function(image_size, cb, ref) { var message = { 'type': 'standard', 'image_size': image_size || 1000 }; if (ref) { message.data = {'ref': ref}; } request.post('https://' + api_host + '/v2.6/me/messenger_codes?access_token=' + configuration.access_token, {form: message}, function(err, res, body) { if (err) { facebook_botkit.log('Could not configure get messenger code'); cb(err); } else { var results = null; try { results = JSON.parse(body); } catch (err) { facebook_botkit.log('ERROR in messenger code API call: Could not parse JSON', err, body); cb(err); } if (results) { if (results.error) { facebook_botkit.log('ERROR in messenger code API call: ', results.error.message); cb(results.error); } else { var uri = results.uri; facebook_botkit.log('Successfully got messenger code', uri); cb(null, uri); } } } }); } }; var attachment_upload_api = { upload: function(attachment, cb) { var message = { message: { attachment: attachment } }; request.post('https://' + api_host + '/v2.6/me/message_attachments?access_token=' + configuration.access_token, { form: message }, function(err, res, body) { if (err) { facebook_botkit.log('Could not upload attachment'); cb(err); } else { var results = null; try { results = JSON.parse(body); } catch (err) { facebook_botkit.log('ERROR in attachment upload API call: Could not parse JSON', err, body); cb(err); } if (results) { if (results.error) { facebook_botkit.log('ERROR in attachment upload API call: ', results.error.message); cb(results.error); } else { var attachment_id = results.attachment_id; facebook_botkit.log('Successfully got attachment id ', attachment_id); cb(null, attachment_id); } } } }); } }; var tags = { get_all: function(cb) { request.get('https://' + api_host + '/v2.6/page_message_tags?access_token=' + configuration.access_token, function(err, res, body) { if (err) { facebook_botkit.log('Could not get tags list'); } else { var results = null; try { results = JSON.parse(body); } catch (err) { facebook_botkit.log('ERROR in page message tags call: Could not parse JSON', err, body); } if (results) { if (results.error) { facebook_botkit.log('ERROR in page message tags: ', results.error.message); } else { facebook_botkit.debug('Successfully call page message tags', body); cb(results); } } } }); } }; var nlp = { enable: function(custom_token) { facebook_botkit.api.nlp.postAPI(true, custom_token); }, disable: function() { facebook_botkit.api.nlp.postAPI(false); }, postAPI: function(value, custom_token) { var uri = 'https://' + api_host + '/v2.8/me/nlp_configs?nlp_enabled=' + value + '&access_token=' + configuration.access_token; if (custom_token) { uri += '&custom_token=' + custom_token; } request.post(uri, {}, function(err, res, body) { if (err) { facebook_botkit.log('Could not enable/disable build-in NLP'); } else { var results = null; try { results = JSON.parse(body); } catch (err) { facebook_botkit.log('ERROR in build-in NLP API call: Could not parse JSON', err, body); } if (results) { if (results.error) { facebook_botkit.log('ERROR in build-in API call: ', results.error.message); } else { facebook_botkit.debug('Successfully enable/disable build-in NLP', body); } } } }); } }; facebook_botkit.api = { 'messenger_profile': messenger_profile_api, 'thread_settings': messenger_profile_api, 'attachment_upload': attachment_upload_api, 'nlp': nlp, 'tags': tags, }; // Verifies the SHA1 signature of the raw request payload before bodyParser parses it // Will abort parsing if signature is invalid, and pass a generic error to response function verifyRequest(req, res, buf, encoding) { var expected = req.headers['x-hub-signature']; var calculated = getSignature(buf); if (expected !== calculated) { throw new Error('Invalid signature on incoming request'); } else { // facebook_botkit.debug('** X-Hub Verification successful!') } } function getSignature(buf) { var hmac = crypto.createHmac('sha1', facebook_botkit.config.app_secret); hmac.update(buf, 'utf-8'); return 'sha1=' + hmac.digest('hex'); } function abortOnValidationError(err, req, res, next) { if (err) { facebook_botkit.log('** Invalid X-HUB signature on incoming request!'); facebook_botkit.debug('** X-HUB Validation Error:', err); res.status(400).send({ error: 'Invalid signature.' }); } else { next(); } } facebook_botkit.startTicking(); return facebook_botkit; }; module.exports = Facebookbot;