UNPKG

prisme-flow

Version:

prisme platform flow engine

613 lines (549 loc) 18.7 kB
/** * Created by prisme.io on 09/06/2017. */ var _ = require('underscore') var moment = require('moment') var ChatLog = require('./lib/chat-log') var ChatContextStore = require('./lib/chat-context-store') var helpers = require('./lib/facebook/facebook') var utils = require('./lib/helpers/utils') var request = require('request').defaults({ encoding: null }) var Bot = require('./lib/facebook/messenger-bot') var clc = require('cli-color') var DEBUG = false var green = clc.greenBright var white = clc.white var grey = clc.blackBright module.exports = function (RED) { function FacebookBotNode (n) { RED.nodes.createNode(this, n) var self = this this.botname = n.botname this.log = n.log this.usernames = [] if (n.usernames) { this.usernames = _(n.usernames.split(',')).chain() .map(function (userId) { return userId.match(/^[a-zA-Z0-9_]+?$/) ? userId : null }) .compact() .value() } this.handleMessage = function (botMsg) { /* { sender: { id: '10153461620831415' }, recipient: { id: '141972351547' }, timestamp: 1468868282071, message: { mid: 'mid.1468868282014:a9429329545544f523', seq: 334, text: 'test' }, transport: 'facebook' } */ var facebookBot = self.bot if (DEBUG) { // eslint-disable-next-line no-console console.log('START:-------') // eslint-disable-next-line no-console console.log(botMsg) // eslint-disable-next-line no-console console.log('END:-------') } // mark the original message with the platform botMsg = _.extend({}, botMsg, {transport: 'facebook'}) var userId = botMsg.sender.id var chatId = botMsg.sender.id var messageId = botMsg.message != null ? botMsg.message.mid : null // todo fix this // var isAuthorized = node.config.isAuthorized(username, userId); var isAuthorized = true var chatContext = ChatContextStore.getOrCreateChatContext(self, chatId) var payload = null // decode the message, eventually download stuff self.getMessageDetails(botMsg, self.bot) .then(function (obj) { payload = obj return helpers.getOrFetchProfile(userId, self.bot) }) .then(function (profile) { // store some information chatContext.set('chatId', chatId) chatContext.set('messageId', messageId) chatContext.set('userId', userId) chatContext.set('firstName', profile.first_name) chatContext.set('lastName', profile.last_name) chatContext.set('authorized', isAuthorized) chatContext.set('transport', 'facebook') chatContext.set('message', payload.content) var chatLog = new ChatLog(chatContext) return chatLog.log({ payload: payload, originalMessage: { transport: 'facebook', chat: { id: chatId } }, chat: function () { return ChatContextStore.getChatContext(self, chatId) } }, self.log) }) .then(function (msg) { var currentConversationNode = chatContext.get('currentConversationNode') // if a conversation is going on, go straight to the conversation node, otherwise if authorized // then first pin, if not second pin if (currentConversationNode != null) { // void the current conversation chatContext.set('currentConversationNode', null) // emit message directly the node where the conversation stopped RED.events.emit('node:' + currentConversationNode, msg) } else { facebookBot.emit('relay', msg) } }) .catch(function (error) { facebookBot.emit('relay', null, error) }) } if (this.credentials) { this.token = this.credentials.token this.app_secret = this.credentials.app_secret this.verify_token = this.credentials.verify_token if (this.token) { this.token = this.token.trim() if (!this.bot) { this.bot = new Bot({ token: this.token, verify: this.verify_token, app_secret: this.app_secret }) var uiPort = RED.settings.get('uiPort') // eslint-disable-next-line no-console console.log('') // eslint-disable-next-line no-console console.log(grey('------ Facebook Webhook ----------------')) // eslint-disable-next-line no-console console.log(green('Webhook URL: ') + white('http://localhost' + (uiPort != '80' ? ':' + uiPort : '') + '/redbot/facebook')) // eslint-disable-next-line no-console console.log(green('Verify token is: ') + white(this.verify_token)) // eslint-disable-next-line no-console console.log('') // mount endpoints on local express this.bot.expressMiddleware(RED.httpNode) this.bot.on('message', this.handleMessage) this.bot.on('postback', this.handleMessage) this.bot.on('account_linking', this.handleMessage) } } } this.on('close', function (done) { var endpoints = ['/facebook', '/facebook/_status'] // remove middleware for facebook callback RED.httpNode._router.stack.forEach(function (route, i, routes) { if (route.route && _.contains(endpoints, route.route.path)) { routes.splice(i, 1) } }) done() }) this.isAuthorized = function (username, userId) { if (self.usernames.length > 0) { return self.usernames.indexOf(username) != -1 || self.usernames.indexOf(String(userId)) != -1 } return true } // creates the message details object from the original message this.getMessageDetails = function (botMsg) { return new Promise(function (resolve, reject) { // var userId = botMsg.sender.id; var chatId = botMsg.sender.id var messageId = botMsg.message != null ? botMsg.message.mid : null if (!_.isEmpty(botMsg.account_linking)) { resolve({ chatId: chatId, messageId: messageId, type: 'account-linking', content: botMsg.account_linking.authorization_code, linkStatus: botMsg.account_linking.status, date: moment(botMsg.timestamp), inbound: true }) return } if (botMsg.message == null) { reject('Unable to detect inbound message for Facebook') } var message = botMsg.message if (!_.isEmpty(message.quick_reply)) { resolve({ chatId: chatId, messageId: messageId, type: 'message', content: message.quick_reply.payload, date: moment(botMsg.timestamp), inbound: true }) return } else if (!_.isEmpty(message.text)) { resolve({ chatId: chatId, messageId: messageId, type: 'message', content: message.text, date: moment(botMsg.timestamp), inbound: true }) return } if (_.isArray(message.attachments) && !_.isEmpty(message.attachments)) { var attachment = message.attachments[0] switch (attachment.type) { case 'image': // download the image into a buffer helpers.downloadFile(attachment.payload.url) .then(function (buffer) { resolve({ chatId: chatId, messageId: messageId, type: 'photo', content: buffer, date: moment(botMsg.timestamp), inbound: true }) }) .catch(function () { reject('Unable to download ' + attachment.payload.url) }) break case 'file': // download the image into a buffer helpers.downloadFile(attachment.payload.url) .then(function (buffer) { resolve({ chatId: chatId, messageId: messageId, type: 'document', content: buffer, date: moment(botMsg.timestamp), inbound: true }) }) .catch(function () { reject('Unable to download ' + attachment.payload.url) }) break case 'location': resolve({ chatId: chatId, messageId: messageId, type: 'location', content: { latitude: attachment.payload.coordinates.lat, longitude: attachment.payload.coordinates.long }, date: moment(botMsg.timestamp), inbound: true }) break } } else { reject('Unable to detect inbound message for Facebook Messenger') } }) } } RED.nodes.registerType('bot-facebook-node', FacebookBotNode, { credentials: { token: { type: 'text' }, app_secret: { type: 'text' }, verify_token: { type: 'text' }, key_pem: { type: 'text' }, cert_pem: { type: 'text' } } }) function FacebookInNode (config) { RED.nodes.createNode(this, config) var node = this this.bot = config.bot this.config = RED.nodes.getNode(this.bot) if (this.config) { this.status({fill: 'red', shape: 'ring', text: 'disconnected'}) node.bot = this.config.bot if (node.bot) { this.status({fill: 'green', shape: 'ring', text: 'connected'}) node.bot.on('relay', function (message, error) { if (error != null) { node.error(error) } else { node.send(message) } }) } else { node.warn('Please select a bot configuration in Facebook Messenger Receiver') } } else { node.warn('Missing configuration in Facebook Messenger Receiver') } } RED.nodes.registerType('bot-facebook-receive', FacebookInNode) function FacebookOutNode (config) { RED.nodes.createNode(this, config) var node = this this.bot = config.bot this.track = config.track this.config = RED.nodes.getNode(this.bot) if (this.config) { this.status({fill: 'red', shape: 'ring', text: 'disconnected'}) node.bot = this.config.bot if (node.bot) { this.status({fill: 'green', shape: 'ring', text: 'connected'}) } else { node.warn('no bot in config.') } } else { node.warn('no config.') } function sendMeta (msg) { return new Promise(function (resolve, reject) { var type = msg.payload.type var bot = node.bot // var credentials = node.config.credentials; var reportError = function (err) { if (err) { reject(err) } else { resolve() } } switch (type) { case 'persistent-menu': bot.setPersistentMenu(msg.payload.items, reportError) break default: reject() } }) } function sendMessage (msg) { return new Promise(function (resolve, reject) { var type = msg.payload.type var bot = node.bot var credentials = node.config.credentials var reportError = function (err) { if (err) { reject(err) } else { resolve() } } switch (type) { case 'action': request({ method: 'POST', json: { recipient: { id: msg.payload.chatId }, 'sender_action': 'typing_on' }, url: 'https://graph.facebook.com/v2.6/me/messages?access_token=' + credentials.token }, reportError) break case 'request': // todo error if not location // send bot.sendMessage( msg.payload.chatId, { text: msg.payload.content, quick_replies: [ { 'content_type': 'location' } ] }, reportError ) break case 'account-link': var attachment = { 'type': 'template', 'payload': { 'template_type': 'button', 'text': msg.payload.content, 'buttons': [ { 'type': 'account_link', 'url': msg.payload.authUrl } ] } } bot.sendMessage( msg.payload.chatId, { attachment: attachment }, reportError ) break case 'inline-buttons': var quickReplies = _(msg.payload.buttons).map(function (button) { var quickReply = { content_type: 'text', title: button.label, payload: !_.isEmpty(button.value) ? button.value : button.label } if (!_.isEmpty(button.image_url)) { quickReply.image_url = button.image_url } return quickReply }) // send bot.sendMessage( msg.payload.chatId, { text: msg.payload.content, quick_replies: quickReplies }, reportError ) break case 'message': bot.sendMessage( msg.payload.chatId, { text: msg.payload.content }, reportError ) break case 'location': var lat = msg.payload.content.latitude var lon = msg.payload.content.longitude var locationAttachment = { 'type': 'template', 'payload': { 'template_type': 'generic', 'elements': { 'element': { 'title': !_.isEmpty(msg.payload.place) ? msg.payload.place : 'Position', 'image_url': 'https:\/\/maps.googleapis.com\/maps\/api\/staticmap?size=764x400&center=' + lat + ',' + lon + '&zoom=16&markers=' + lat + ',' + lon, 'item_url': 'http:\/\/maps.apple.com\/maps?q=' + lat + ',' + lon + '&z=16' } } } } bot.sendMessage( msg.payload.chatId, { attachment: locationAttachment }, reportError ) break case 'audio': var audio = msg.payload.content helpers.uploadBuffer({ recipient: msg.payload.chatId, type: 'audio', buffer: audio, token: credentials.token, filename: msg.payload.filename }).catch(function (err) { reject(err) }) break case 'document': helpers.uploadBuffer({ recipient: msg.payload.chatId, type: 'file', buffer: msg.payload.content, token: credentials.token, filename: msg.payload.filename, mimeType: msg.payload.mimeType }).catch(function (err) { reject(err) }) break case 'photo': var image = msg.payload.content helpers.uploadBuffer({ recipient: msg.payload.chatId, type: 'image', buffer: image, token: credentials.token, filename: msg.payload.filename }).catch(function (err) { reject(err) }) break default: reject('Unable to prepare unknown message type') } }) } // relay message var handler = function (msg) { node.send(msg) } RED.events.on('node:' + config.id, handler) // cleanup on close this.on('close', function () { RED.events.removeListener('node:' + config.id, handler) }) this.on('input', function (msg) { // check if the message is from facebook if (msg.originalMessage != null && msg.originalMessage.transport !== 'facebook') { // exit, it's not from facebook return } // try to send the meta first (those messages that doesn't require a valid payload) sendMeta(msg) .then(function () { // ok, meta sent, stop here }, function (error) { // if here, either there was an error or no met message was sent if (error != null) { node.error(error) } else { // check payload var payloadError = utils.hasValidPayload(msg) if (payloadError != null) { // invalid payload node.error(payloadError) } else { // payload is valid, go on var track = node.track var chatContext = msg.chat() // check if this node has some wirings in the follow up pin, in that case // the next message should be redirected here if (chatContext != null && track && !_.isEmpty(node.wires[0])) { chatContext.set('currentConversationNode', node.id) chatContext.set('currentConversationNode_at', moment()) } var chatLog = new ChatLog(chatContext) chatLog.log(msg, node.config.log) .then(function () { sendMessage(msg) }) } // end valid payload } // end no error }) // end then }) } RED.nodes.registerType('bot-facebook-send', FacebookOutNode) }