UNPKG

node-red-contrib-chatbot

Version:

REDBot a Chat bot for a full featured chat bot for Telegram, Facebook Messenger and Slack. Almost no coding skills required

381 lines (351 loc) 14.7 kB
const _ = require('underscore'); const _s = require('underscore.string'); const moment = require('moment'); const { ContextProviders, ChatExpress } = require('chat-platform'); const clc = require('cli-color'); const prettyjson = require('prettyjson'); const path = require('path'); const utils = require('../../lib/helpers/utils'); const mergeNotEmpty = require('../helpers/merge-not-empty'); const GetEnvironment = require('../../lib/helpers/get-environment'); const lcd = require('../../lib/helpers/lcd'); const validators = require('../../lib/helpers/validators'); const { isValidMessage, getTransport, isSimulator, isSkipValidation } = require('../../lib/helpers/utils'); const GlobalContextHelper = require('../../lib/helpers/global-context-helper'); const StoreMessages = require('./store-messages'); const GetNodeByPath = require('../helpers/get-node-by-path'); const when = utils.when; const warn = clc.yellow; const green = clc.green; const GenericBotNode = ( platform, RED, factory, configurator, validator = botConfiguration => botConfiguration.token != null ) => { return function(n) { const globalContextHelper = GlobalContextHelper(RED); RED.nodes.createNode(this, n); var node = this; globalContextHelper.init(this.context().global); var environment = GetEnvironment(RED)(); var isUsed = utils.isUsed(RED, node.id); var startNode = utils.isUsedInEnvironment(RED, node.id, environment); var platformConfigs = globalContextHelper.get(platform) || {}; var contextProviders = ContextProviders(RED); this.botname = n.botname; this.store = n.store; this.log = n.log; this.usernames = n.usernames != null ? n.usernames.split(',') : []; this.polling = n.polling; this.providerToken = n.providerToken; this.debug = n.debug; this.webHook = n.webHook; this.connectMode = n.connectMode; this.storeMessages = n.storeMessages; this.enableMissionControl = n.enableMissionControl; this.inspectMessages = n.inspectMessages; this.chatbotId = n.chatbotId; if (!isUsed) { // silently exit, this node is not used return; } // exit if the node is not meant to be started in this environment if (!startNode) { // eslint-disable-next-line no-console console.log(lcd.timestamp() + warn(`${_s.capitalize(platform)} Bot ` + this.botname + ' will NOT be launched, environment is ' + environment)); return; } // eslint-disable-next-line no-console console.log(green(lcd.timestamp() + `${_s.capitalize(platform)} Bot ` + this.botname + ' will be launched, environment is ' + environment)); // get the context storage node const contextStorageNode = RED.nodes.getNode(this.store); // build the configuration object let botConfiguration = { ...configurator(n, node), contextProvider: contextStorageNode != null ? contextStorageNode.contextStorage : null, contextParams: contextStorageNode != null ? contextStorageNode.contextParams : null, }; // check if there's a valid configuration in global settings if (platformConfigs[node.botname] != null) { var validation = validators.platform[platform](platformConfigs[node.botname]); if (validation != null) { /* eslint-disable no-console */ console.log(lcd.timestamp() + ''); console.log(lcd.timestamp() + lcd.error(`Found a ${_s.capitalize(platform)} configuration in settings.js "` + node.botname + '", but it\'s invalid.')); console.log(lcd.timestamp() + lcd.grey('Errors:')); console.log(lcd.timestamp() + prettyjson.render(validation)); console.log(lcd.timestamp() + ''); return; } else { console.log(lcd.timestamp() + ''); console.log(lcd.timestamp() + lcd.grey(`Found a valid ${_s.capitalize(platform)} configuration in settings.js: "` + node.botname + '":')); console.log(lcd.timestamp() + prettyjson.render(platformConfigs[node.botname])); console.log(lcd.timestamp() + ''); /* eslint-enable no-console */ botConfiguration = platformConfigs[node.botname]; } } // check if context node if (botConfiguration.enableMissionControl) { // eslint-disable-next-line no-console console.log(lcd.timestamp() + lcd.warn('Using Mission Control context provider (SQLite) for ' + node.botname + '.')); botConfiguration.contextProvider = 'sqlite'; botConfiguration.contextParams = { dbPath: RED.settings.RedBot != null ? RED.settings.RedBot.dbPath : path.join(RED.settings.userDir, 'mission-control.sqlite') }; } else if (botConfiguration.contextProvider == null) { // eslint-disable-next-line no-console console.log(lcd.timestamp() + lcd.warn('No context provider specified for chatbot ' + node.botname + '. Defaulting to "memory"')); botConfiguration.contextProvider = 'memory'; botConfiguration.contextParams = {}; } // if chat is not already there and there's a token if (node.chat == null && validator(botConfiguration)) { // check if provider exisst if (!contextProviders.hasProvider(botConfiguration.contextProvider)) { node.error(lcd.timestamp() + 'Error creating chatbot ' + this.botname + '. The context provider ' + botConfiguration.contextProvider + ' doesn\'t exist.'); return; } // create a factory for the context provider node.contextProvider = contextProviders.getProvider( botConfiguration.contextProvider, // prevents empty chatbotId = '' overwriting a non-empty value mergeNotEmpty(botConfiguration.contextParams, { id: this.store, chatbotId: node.chatbotId }) ); // try to start the servers try { node.contextProvider.start(); node.chat = factory(node, botConfiguration); // add extensions RED.nodes.eachNode(function(currentNode) { if (currentNode.type === 'chatbot-extend' && !_.isEmpty(currentNode.codeJs) && currentNode.platform === platform) { try { eval(currentNode.codeJs); } catch (e) { lcd.node(currentNode.codeJs, { color: lcd.red, node: currentNode, title: 'Syntax error in Extend Chat Server node' }); } } }); // finally launch it node.chat.start(); // handle error on sl6teack chat server node.chat.on('error', function(error) { node.error(error); }); node.chat.on('warning', function(warning) { node.warn(warning); }); } catch(e) { node.error(e); } } this.on('close', function (done) { node.chat.stop() .then(function() { return node.contextProvider.stop(); }) .then(function() { node.chat = null; node.contextProvider = null; ChatExpress.reset(); ContextProviders.reset(); done(); }); }); } } const GenericInNode = (platform, RED) => { const { storeInboundMessage } = StoreMessages(RED); const isMissionControlEnabled = require('../../lib/helpers/is-mc-enabled')(RED); return function(config) { RED.nodes.createNode(this, config); const globalContextHelper = GlobalContextHelper(RED); const node = this; const environment = GetEnvironment(RED)(); const getNodeByPath = GetNodeByPath(RED); let nodeGlobalKey = null; globalContextHelper.init(this.context().global); this.bot = config.bot; this.botProduction = config.botProduction; this.config = RED.nodes.getNode(environment === 'production' ? this.botProduction : this.bot); if (this.config) { this.status({fill: 'red', shape: 'ring', text: 'disconnected'}); node.chat = this.config.chat; if (node.chat) { this.status({fill: 'green', shape: 'ring', text: 'connected'}); // set a node that will be the master node, the only node allowed to forward // messages to the node specified in _currentConversatioNode nodeGlobalKey = platform + '_master_' + this.config.id.replace('.','_'); let isMaster = false; if (globalContextHelper.get(nodeGlobalKey) == null) { isMaster = true; globalContextHelper.set(nodeGlobalKey, node.id); // store a list of chatbots and its references globalContextHelper.set('chatbot_info_' + this.config.id.replace('.','_'), { nodeId: node.id, botNode: this.config.id, transport: platform, name: this.config.botname, chatbotId: this.config.chatbotId }); } node.chat.on('message', async function(message) { const context = message.chat(); message.redBot = { environment }; if (isMaster) { // store message if (isMissionControlEnabled() && node.config.storeMessages) { await storeInboundMessage(message, node); } // send payload to ws if (node.config.inspectMessages) { RED.comms.publish('redbot', { topic: 'message.in', payload: message.payload }); } } // check if there is a conversation is going on const trackVars = await when(context.get( `${platform}_currentConversationNode`, `${platform}_trackNode` )); // if there's a current converation, then the message must be forwarded if (trackVars[`${platform}_currentConversationNode`] != null) { // if the current node is master, then relay, if not master do nothing if (isMaster) { await when(context.remove(`${platform}_currentConversationNode`, `${platform}_currentConversationNode_at`)); // relay message to the node if found const trackNode = getNodeByPath(trackVars[`${platform}_currentConversationNode`]); if (trackNode && _.isFunction(trackNode.relay)) { trackNode.relay(message); } } } else if (trackVars[`${platform}_trackNode`] != null) { // if the current node is master, then relay, if not master do nothing if (isMaster) { await when(context.remove(`${platform}_trackNode`, `${platform}_trackNode_at`)); // relay message to the node if found const trackNode = getNodeByPath(trackVars[`${platform}_trackNode`]); if (trackNode && _.isFunction(trackNode.relay)) { trackNode.relay(message); } } } else { node.send(message); } }); } else { node.warn(`Missing or incomplete configuration in ${_s.capitalize(platform)} Receiver`); } } else { node.warn(`Missing configuration in ${_s.capitalize(platform)} Receiver`); } this.on('close', function (done) { globalContextHelper.set(nodeGlobalKey, null); if (node.chat != null) { node.chat.off('message'); } done(); }); } }; const GenericOutNode = (platform, RED) => { const { storeOutboundMessage } = StoreMessages(RED); const isMissionControlEnabled = require('../../lib/helpers/is-mc-enabled')(RED); return function(config) { const globalContextHelper = GlobalContextHelper(RED); RED.nodes.createNode(this, config); const node = this; const environment = GetEnvironment(RED)(); globalContextHelper.init(this.context().global); this.bot = config.bot; this.botProduction = config.botProduction; this.track = config.track; this.passThrough = config.passThrough; this.errorOutput = config.errorOutput; this.config = RED.nodes.getNode(environment === 'production' ? this.botProduction : this.bot); if (this.config) { this.status({fill: 'red', shape: 'ring', text: 'disconnected'}); node.chat = this.config.chat; if (node.chat) { this.status({fill: 'green', shape: 'ring', text: 'connected'}); } else { node.warn(`Missing or incomplete configuration in ${_s.capitalize(platform)} Receiver`); } } else { node.warn(`Missing configuration in ${_s.capitalize(platform)} Receiver`); } this.relay = function(msg) { node.send(msg); }; this.on('input', async function(message, send, done) { // send/done compatibility for node-red < 1.0 send = send || function() { node.send.apply(node, arguments) }; done = done || function(error) { node.error.call(node, error, message) }; // check if valid message or right transport, skip also if it's simulator if (!isSkipValidation(message) && (!isValidMessage(message, node) || getTransport(message) !== platform || isSimulator(message)) ) { done(); return; } const context = message.chat(); // check if this node has some wirings in the follow up pin, in that case // the next message should be redirected here if (context != null && node.track && !_.isEmpty(node.wires[0])) { await when(context.set({ [`${platform}_currentConversationNode`]: node._path, [`${platform}_currentConversationNode_at`]: moment().toISOString() })); } if (context != null && !_.isEmpty(message.payload?.trackNodeId)) { await when(context.set({ [`${platform}_trackNode`]: message.payload?.trackNodeId, [`${platform}_trackNode_at`]: moment().toISOString() })); } // finally send out try { const outMessage = await node.chat.send(message); if (isMissionControlEnabled() && node.config.storeMessages) { await storeOutboundMessage(outMessage, node); } // forward if not tracking if (node.passThrough) { send(outMessage); } // send payload to ws if (node.config.inspectMessages) { RED.comms.publish('redbot', { topic: 'message.out', payload: outMessage.sentMessage }); } done(); } catch(error) { // if platform error, then forward the error if (node.errorOutput) { send(node.passThrough || node.track ? [null, { ...message, payload: error }] : { ...message, payload: error }); } // don't send any error to node-red, already done by the configuration node (avoid duplicated errors in sidebar) done(); } }); } }; module.exports = { GenericOutNode, GenericInNode, GenericBotNode };