UNPKG

prisme-flow

Version:

prisme platform flow engine

576 lines (541 loc) 19.7 kB
/** * Created by prisme.io on 09/06/2017. */ var _ = require('underscore') var TelegramBot = require('node-telegram-bot-api') var moment = require('moment') var ChatLog = require('./lib/chat-log') var ChatContextStore = require('./lib/chat-context-store') var helpers = require('./lib/telegram/telegram') var utils = require('./lib/helpers/utils') var DEBUG = false module.exports = function (RED) { function TelegramBotNode (n) { RED.nodes.createNode(this, n) var self = this this.botname = n.botname this.polling = n.polling 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.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 getMessageDetails (botMsg) { var telegramBot = self.telegramBot return new Promise(function (resolve, reject) { if (botMsg.text) { resolve({ chatId: botMsg.chat.id, messageId: botMsg.message_id, type: 'message', content: botMsg.text, date: moment.unix(botMsg.date), inbound: true }) } else if (botMsg.photo) { telegramBot.getFileLink(botMsg.photo[botMsg.photo.length - 1].file_id) .then(function (path) { return helpers.downloadFile(path) }) .then(function (buffer) { resolve({ chatId: botMsg.chat.id, messageId: botMsg.message_id, type: 'photo', content: buffer, caption: botMsg.caption, date: moment.unix(botMsg.date), inbound: true }) }) .catch(function (error) { reject(error) }) } else if (botMsg.audio) { resolve({ chatId: botMsg.chat.id, messageId: botMsg.message_id, type: 'audio', content: botMsg.audio.file_id, caption: botMsg.caption, date: moment.unix(botMsg.date), inbound: true }) } else if (botMsg.document) { resolve({ chatId: botMsg.chat.id, messageId: botMsg.message_id, type: 'document', content: botMsg.document.file_id, caption: botMsg.caption, date: moment.unix(botMsg.date), inbound: true }) } else if (botMsg.sticker) { resolve({ chatId: botMsg.chat.id, messageId: botMsg.message_id, type: 'sticker', content: botMsg.sticker.file_id, date: moment.unix(botMsg.date), inbound: true }) } else if (botMsg.video) { resolve({ chatId: botMsg.chat.id, messageId: botMsg.message_id, type: 'video', content: botMsg.video.file_id, caption: botMsg.caption, date: moment.unix(botMsg.date), inbound: true }) } else if (botMsg.voice) { resolve({ chatId: botMsg.chat.id, messageId: botMsg.message_id, type: 'voice', content: botMsg.voice.file_id, caption: botMsg.caption, date: moment.unix(botMsg.date), inbound: true }) } else if (botMsg.location) { resolve({ chatId: botMsg.chat.id, messageId: botMsg.message_id, type: 'location', content: botMsg.location, date: moment.unix(botMsg.date), inbound: true }) } else if (botMsg.contact) { resolve({ chatId: botMsg.chat.id, messageId: botMsg.message_id, type: 'contact', content: botMsg.contact, date: moment.unix(botMsg.date), inbound: true }) } else { reject('Unable to detect incoming Telegram message') } }) } this.handleCallback = function (botMsg) { var chatId = botMsg.message.chat.id var alert = false var answer = null if (self.telegramBot.lastInlineButtons[chatId] != null) { // find the button with the right value, takes the answer and alert if any var button = _(self.telegramBot.lastInlineButtons[chatId]).findWhere({value: botMsg.data}) if (button != null) { answer = button.answer alert = button.alert } // do not remove from hash, the user could click again } // copy the "from" of the message containing user information, not chatbot detail botMsg.message.from = botMsg.from // send answer back to client self.telegramBot.answerCallbackQuery(botMsg.id, answer, alert) .then(function () { // send through the message as usual botMsg.message.text = botMsg.data self.handleMessage(botMsg.message) }) } this.handleMessage = function (botMsg) { var telegramBot = self.telegramBot // mark the original message with the platform botMsg.transport = 'telegram' 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:-------') } var username = !_.isEmpty(botMsg.from.username) ? botMsg.from.username : null var chatId = botMsg.chat.id var userId = botMsg.from.id var isAuthorized = self.isAuthorized(username, userId) var chatContext = ChatContextStore.getOrCreateChatContext(self, chatId) // store some information chatContext.set('chatId', chatId) chatContext.set('messageId', botMsg.message_id) chatContext.set('userId', userId) chatContext.set('firstName', botMsg.from.first_name) chatContext.set('lastName', botMsg.from.last_name) chatContext.set('authorized', isAuthorized) chatContext.set('transport', 'telegram') chatContext.set('message', botMsg.text) // decode the message self.getMessageDetails(botMsg) .then(function (payload) { var chatLog = new ChatLog(chatContext) return chatLog.log({ payload: payload, originalMessage: botMsg, chat: function () { return ChatContextStore.getChatContext(self, chatId) } }, self.log) }) /* .then(function(msg) { return analytics.dashbot.inbound(msg); }) */ .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 node id chatContext.set('currentConversationNode', null) // emit message directly the node where the conversation stopped RED.events.emit('node:' + currentConversationNode, msg) } else { telegramBot.emit('relay', msg) } }) .catch(function (error) { telegramBot.emit('relay', null, error) }) } // end handleMessage var telegramBot = null if (this.credentials) { this.token = this.credentials.token if (this.token) { this.token = this.token.trim() if (!this.telegramBot) { telegramBot = new TelegramBot(this.token, { polling: { timeout: 10, interval: !isNaN(parseInt(self.polling, 10)) ? parseInt(self.polling, 10) : 1000 } }) this.telegramBot = telegramBot this.telegramBot.setMaxListeners(0) this.telegramBot.on('message', this.handleMessage) this.telegramBot.on('callback_query', this.handleCallback) } } } this.on('close', function (done) { // stop polling only once if (this.telegramBot != null && this.telegramBot._polling) { self.telegramBot.off('message', self.handleMessage) self.telegramBot.off('callback_query', self.handleCallback) self.telegramBot.stopPolling() .then(function () { self.telegramBot = null done() }) } else { done() } }) } // end TelegramBotNode RED.nodes.registerType('bot-telegram-node', TelegramBotNode, { credentials: { token: { type: 'text' } } }) function TelegramInNode (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.telegramBot = this.config.telegramBot if (node.telegramBot) { this.status({fill: 'green', shape: 'ring', text: 'connected'}) /* todo implement inline node.telegramBot.on('inline_query', function(botMsg) { console.log('inline request', botMsg); }); */ node.telegramBot.on('relay', function (message, error) { if (error != null) { node.error(error) } else { node.send(message) } }) } else { node.warn('no bot in config.') } } else { node.warn('no config.') } } RED.nodes.registerType('bot-telegram-receive', TelegramInNode) function TelegramOutNode (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.telegramBot = this.config.telegramBot if (node.telegramBot) { this.status({ fill: 'green', shape: 'ring', text: 'connected' }) } else { node.warn('no bot in config.') } } else { node.warn('no config.') } // 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.handleError = function (error, msg) { // convert to string var plainError = error != null ? String(error) : '' // remove anything before any eventual '{', telegram lib returns a plain text error // in order to parse it var jsonError = plainError.replace(/^.*\{/, '{') var errorCode = null var errorDescription = plainError // now try to parse try { var parsedError = JSON.parse(jsonError) errorCode = parsedError.error_code errorDescription = parsedError.description } catch (e) { // do nothing } // send out, second parameter goes through the catch all node node.error(plainError, { chatId: msg.payload.chatId, code: errorCode, description: errorDescription }) } this.on('input', function (msg) { // check if the message is from telegram if (msg.originalMessage != null && msg.originalMessage.transport !== 'telegram') { // exit, it's not from telegram return } if (msg.payload == null) { node.warn('msg.payload is empty') return } if (msg.payload.chatId == null) { node.warn('msg.payload.chatId is empty') return } if (msg.payload.type == null) { node.warn('msg.payload.type is empty') return } // var context = node.context(); var buttons = null var track = node.track var chatId = utils.getChatId(msg) var chatContext = utils.getChatContext(msg) var type = msg.payload.type // 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 messageOk = function (response) { chatContext.set('messageId', response.message_id) } var messageError = function (error) { node.handleError(error, msg) } var chatLog = new ChatLog(chatContext) chatLog.log(msg, this.config.log) .then(function () { switch (type) { case 'message': if (msg.originalMessage.modify_message_id != null) { node.telegramBot.editMessageText(msg.payload.content, { chat_id: chatId, message_id: msg.originalMessage.modify_message_id }).then(messageOk, messageError) } else { node.telegramBot.sendMessage(chatId, msg.payload.content, msg.payload.options) .then(messageOk, messageError) } break case 'photo': node.telegramBot.sendPhoto(chatId, msg.payload.content, { caption: msg.payload.caption }).then(messageOk, messageError) break case 'document': node.telegramBot.sendDocument(chatId, msg.payload.content, { caption: msg.payload.caption }, { filename: msg.payload.filename }).then(messageOk, messageError) break case 'sticker': node.telegramBot.sendSticker(chatId, msg.payload.content, msg.payload.options) .then(messageOk, messageError) break case 'video': node.telegramBot.sendVideo(chatId, msg.payload.content, msg.payload.options) .then(messageOk, messageError) break case 'audio': node.telegramBot.sendVoice(chatId, msg.payload.content, msg.payload.options) .then(messageOk, messageError) break case 'location': node.telegramBot.sendLocation(chatId, msg.payload.content.latitude, msg.payload.content.longitude, msg.payload.options) .then(messageOk, messageError) break case 'action': node.telegramBot.sendChatAction(chatId, msg.payload.waitingType != null ? msg.payload.waitingType : 'typing') .then(messageOk, messageError) break case 'request': var keyboard = null if (msg.payload.requestType === 'location') { keyboard = [ [{ text: !_.isEmpty(msg.payload.buttonLabel) ? msg.payload.buttonLabel : 'Send your position', request_location: true }] ] } else if (msg.payload.requestType === 'phone-number') { keyboard = [ [{ text: !_.isEmpty(msg.payload.buttonLabel) ? msg.payload.buttonLabel : 'Send your phone number', request_contact: true }] ] } if (keyboard != null) { node.telegramBot .sendMessage(chatId, msg.payload.content, { reply_markup: JSON.stringify({ keyboard: keyboard, 'resize_keyboard': true, 'one_time_keyboard': true }) }) .then(messageOk, messageError) } else { node.error('Request type not supported') } break case 'inline-buttons': // create inline buttons, docs for this is https://core.telegram.org/bots/api#inlinekeyboardmarkup // create the first array of array var inlineKeyboard = [[]] // cycle through buttons, add new line at the end if flag _(msg.payload.buttons).each(function (button) { var json = null if (!_.isEmpty(button.url)) { json = { text: button.label, url: button.url } } else if (!_.isEmpty(button.value)) { json = { text: button.label, callback_data: button.value } } else { json = { text: button.label, callback_data: button.label } } // add the button to the last row inlineKeyboard[inlineKeyboard.length - 1].push(json) // if new line, then add a blank array if (button.newLine) { inlineKeyboard.push([]) } }) // store the last buttons, this will be handled by the receiver if (node.telegramBot.lastInlineButtons == null) { node.telegramBot.lastInlineButtons = {} } node.telegramBot.lastInlineButtons[chatId] = msg.payload.buttons // send buttons or edit if (msg.originalMessage.modify_message_id != null) { node.telegramBot.editMessageReplyMarkup(JSON.stringify({ inline_keyboard: inlineKeyboard }), { chat_id: chatId, message_id: msg.originalMessage.modify_message_id }).then(messageOk, messageError) } else { // finally send node.telegramBot.sendMessage(chatId, msg.payload.content, { reply_markup: JSON.stringify({ inline_keyboard: inlineKeyboard }) }).then(messageOk, messageError) } break case 'buttons': if (_.isEmpty(msg.payload.content)) { node.error('Buttons node needs a non-empty message') return } buttons = { reply_markup: JSON.stringify({ keyboard: _(msg.payload.buttons).map(function (button) { return [button.value] }), resize_keyboard: true, one_time_keyboard: true }) } // finally send node.telegramBot.sendMessage( chatId, msg.payload.content, buttons ).then(messageOk, messageError) break default: // unknown type, do nothing } }) }) } RED.nodes.registerType('bot-telegram-send', TelegramOutNode) }