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

315 lines (302 loc) 16.7 kB
<script type="text/javascript"> $.RedBot.registerType('chatbot-universal-node', { category: 'config', defaults: { botname: { value: '', required: true }, usernames: { value: '', required: false }, connectorParams: { value: '' }, store: { value: '', type: 'chatbot-context-store', required: false }, log: { value: null }, debug: { value: false } }, oneditsave: function() { this.connectorParams = $('#node-config-input-connectorParams').typedInput('value'); }, oneditprepare: function() { $("#node-config-input-polling").spinner({min: 0, step: 100}); var node = this; // init free params var widget = $('#node-config-input-connectorParams'); widget.typedInput({ 'default': 'json', types: ['json'] }); widget.typedInput('value', this.connectorParams); // get globals var nodeRedUrl = $.RedBot.getNodeRedUrl(); // fetch available context providers $.get(nodeRedUrl + 'redbot/globals') .done(function(response) { if (response != null && response.telegram != null && response.telegram[node.botname] != null) { $('#node-config-input-botname').prop('readonly', true); $('.form-editable').addClass('hidden'); $('.form-warning').removeClass('hidden'); $('.form-warning .bot-name').html('"' + node.botname + '"'); } }); }, paletteName: 'Universal Bot', credentials: { token: { type: 'text' } }, label: function () { return this.botname; } }); </script> <script type="text/x-red" data-template-name="chatbot-universal-node"> <div class="form-row"> <label for="node-config-input-botname"><i class="icon-bookmark"></i> Bot Name</label> <input type="text" id="node-config-input-botname"> </div> <div class="form-editable"> <div class="form-row"> <label for="node-config-input-usernames">Users</label> <input type="text" id="node-config-input-usernames"> <div style="max-width: 460px;font-size: 12px;color: #999999;line-height: 14px;margin-top:5px;"> Comma separated list of userId authorized to use the chatBot </div> </div> <div class="form-row"> <label for="node-config-input-log">Log file</label> <input type="text" id="node-config-input-log"> <div style="max-width: 460px;font-size: 12px;color: #999999;line-height: 14px;margin-top:5px;"> Store inbound and outbound messages to file </div> </div> <div class="form-row"> <label for="node-input-bot">Context</label> <input type="text" id="node-config-input-store" placeholder="Select storage for chat context"> <div class="redbot-form-hint"> Select the chat context provider to use with this bot, if none is selected then non-persistent "memory" will be used.<br> To extend <strong>RedBot</strong> with a new chat context provider see <a href="https://github.com/guidone/node-red-contrib-chatbot/wiki/Creating-a-Chat-Context-Provider" target="_blank">this tutorial</a>. </div> </div> <div class="form-row"> <label for="node-config-input-debug">Debug</label> <input type="checkbox" value="true" id="node-config-input-debug"> <span class="redbot-form-hint"> Show debug information on send/receive </span> </div> <div class="form-row"> <label for="node-config-input-connectorParams">Params</label> <input type="text" id="node-config-input-connectorParams" placeholder="Params"> <div class="redbot-form-hint"> Parameters passed to the <i>Universal Connector</i>. Use the method <code>this.getOptions()</code> in the middlewares to get the parameters. See <a href="https://github.com/guidone/node-red-contrib-chatbot/wiki/Extend-node" target="_blank">the help page</a> </div> </div> </div> <div class="form-warning hidden"> This bot configuration is stored in <b>Node-RED</b> <em>settings.js</em> and cannot be modified from the UI, check the section <code>functionGlobalContext</code> near the key <em class="bot-name">""</em> </div> </script> <script type="text/x-red" data-help-name="chatbot-universal-node"> test </script> <script type="text/javascript"> $.RedBot.registerType('chatbot-universal-receive', { category: $.RedBot.config.name + ' Platforms', color: '#FFCC66', defaults: { bot: { value: '', type: 'chatbot-universal-node', required: true }, botProduction: { value: '', type: 'chatbot-universal-node', required: false } }, inputs: 1, outputs: 1, icon: 'chatbot-receiver.png', paletteLabel: 'Universal In', label: function () { return 'Universal Receiver'; } }); </script> <script type="text/x-red" data-template-name="chatbot-universal-receive"> <div class="form-row"> <label for="node-input-bot" style="display:block;width:100%;">Bot configuration <span class="redbot-environment">(development)</span></label> <input type="text" id="node-input-bot" placeholder="Bot"> </div> <div class="form-row" style="margin-top:25px;"> <label for="node-input-botProduction" style="display:block;width:100%;">Bot configuration <span class="redbot-environment">(production)</span></label> <input type="text" id="node-input-botProduction" placeholder="Bot"> </div> <div class="redbot-form-hint"> Bot for <strong>production</strong> will be launched only if the global variable <em>"environment"</em> in <em>settings.js</em> is set to <em>"production"</em>, otherwise will be used the configuration for <strong>development</strong>. </div> </script> <script type="text/x-red" data-help-name="chatbot-universal-receive"><p>The <code>Universal Connector node</code> allows to connect the the <strong>RedBot</strong> ecosystem any kind of messaging service (like email, an SMS gateway, a testing stub, etc). It&#39;s an advanced component, a good kwowledge of <strong>JavaScript</strong> and <em>Promises</em> is required in order to use it.</p> <p>Unlike other receiver nodes the <code>Universal Connector node</code> has an input pin, this is where the external service will send the payload in order to be translated and injected in the <strong>RedBot</strong> flow. The <code>Universal Connector node</code> takes care of providing the chat context, the <em>pass thru</em> and <em>track</em> features, the <em>production</em>/<em>development</em> configuration, etc; while the implementation detail about <strong>how</strong> the incoming message is implemented is left to the user and must be implemented with an [[Extend node|Extend-node]].</p> <p>In order to properly <em>translate</em> an incoming message the implementation in the <code>Universal Connector node</code> must:</p> <ol> <li>Extract a <strong>chatId</strong> from the payload: it&#39;s a unique identifier of the conversation in the connected platform (for example the mobile number for a SMS gateway, the email for an email system, etc)</li> <li>Extract or infer the message type. Some messaging platform support different type of messages (for example Telegrams supports <em>message</em>, <em>audio</em>, <em>photo</em>, etc) while a service like a SMS gateway just support one type of message.</li> <li>Extract the message content it also should (but is not mandatory)</li> <li>Extract the <em>timestamp</em> of the message</li> <li>Extract a <em>messageId</em> to properly reference inbound and outbound messages in the external service timeline</li> </ol> <p>Like explained in [[Extend node|Extend-node]] a connector handles inbound messages with a chain of middlewares: chunck of codes executed sequentially in order to accomplish steps 1 to 3. In order to keep the code simple and maintanable is a good practice to let middleware takes care of detecting and translating one kind of message in each middleware, as soon as a message has been <em>resolved</em> (means that a message <em>type</em> is assigned to the incoming message), the rest of middlewares chain is skipped and the message is injected in the <strong>RedBot</strong>&#39;s flow.</p> <p>For example suppose that a SMS gateway calls the <strong>Node-RED</strong> instance webhook with this payload</p> <pre><code class="lang-javascript">{ sms: { from: &#39;+39347123456&#39;, to: &#39;+39338654321&#39;, id: &#39;_xyz&#39;, text: &#39;Hello there!&#39;, sender_name: &#39;Alan Turing&#39; } } </code></pre> <p>The <em>from</em> key is a good candidate for the <em>chatId</em> while the content of the message is in the <em>text</em> key. the code in the <code>Extend node</code> to handle this</p> <pre><code class="lang-javascript">// initial stage of the process, here the message is still not enriched with all the helpers and // variables of a RedBot message like .api(), .chat(), chatId, etc node.chat.onChatId(message =&gt; message.sms.from); node.chat.onMessageId(message =&gt; message.sms.id); node.chat.in(message =&gt; { return new Promise((resolve, reject) =&gt; { // check that the incoming payload is an actual &quot;text&quot; message if (message.originalMessage.sms != null &amp;&amp; message.originalMessage.sms.text != &#39;&#39;) { message.payload.type = &#39;message&#39;; message.payload.content = message.originalMessage.sms.text; } resolve(message); }); }); </code></pre> <p>When a new payload arrives to the input pin the <em>chatId</em> value is extracted from the callback <code>.onChatId()</code> (there are callbacks for other information like <em>messageId</em>, <em>timestamp</em>, <em>language</em>, etc) and a <em>neutral</em> version of the <strong>RedBot</strong> message is passed to the middlewares. A <em>neutral message</em> is a regular <strong>RedBot</strong> message (the messages that flow out of a receiver node) except the type and content key are left blank: is up to the developer to fill in this keys using the information contained in <em>originalMessage</em>, all values needed to build a <em>neutral message</em> (<em>chatId</em>, <em>messageId</em>, <em>timestamp</em>, <em>language</em>) are extracted using callbacks. If the callback in <code>.onChatId()</code> fails to extract the <em>chatId</em> then an exception is raised and the message will never go through the middlewares.</p> <p>In the example above there is additional information in the payload, it&#39;s a good practise to use the chat context for storing this information, for example</p> <pre><code class="lang-javascript">node.chat.onChatId(payload =&gt; payload.sms.from); node.chat.onMessageId(payload =&gt; payload.sms.id); node.chat.in(message =&gt; { const chat = message.chat(); // check that the incoming payload is an actual message, if not return and resolve immediately // this will skip to the next middleware if (message.originalMessage.sms == nulll || message.originalMessage.sms.text == null) { return Promise.resolve(message); } // not all context provider return a promise return Promise.resolve(chat.set(&#39;name&#39;, message.originalMessage.sms.sender_name)) .then(() =&gt; { message.payload.type = &#39;message&#39;; message.payload.content = message.originalMessage.sms.text; return message; }); }); </code></pre> <p>Note that a middleware should always return a <em>Promise</em> that returns the received message (passed as parameter of the middleware) and that will be injected in the flow (a warning on the console will log any middleware not returning a Promise).</p> <p>If none of the middlewares in the inbound chain is able to infer a message type and fill in the <em>type</em> and <em>content</em> of the neutral message, then the message is discarded (enable the <em>debug</em> option in the receiver configuration to log the discarded messages in the system console).</p> <p>The <code>Universal sender node</code> has always an output pin used for following up the conversation or to chain multiple messages to be sent in a specific order. In the example above the code to send a SMS message to a custom API</p> <pre><code class="lang-javascript">// this middleware handles the message of type &#39;message&#39; node.chat.out(&#39;message&#39;, message =&gt; { return request(&#39;https://my.api/send&#39;, data: { text: message.payload.content, to: message.payload.chatId }).then(response =&gt; { message.status = response.statusCode; // this make the chain of promises returns the message object enriched with the status code // returned by API to be used by a downstream node return message; }); }); </code></pre> <p>Like all other sender nodes there are two options:</p> <ul> <li><strong>Track</strong> option: allows to track a conversation from the user: the answer of the user to a message sent with a sender with the track option will appear in the output pin in order to follow up the conversation (basically the message is teleported here). <em>Pay attention to this option while debugging the Universal connector and the Extend node, it will result in a alternate and strange results on the output pin</em></li> <li><strong>Pass through</strong> option: it will forward the <strong>RedBot message</strong> to the output pin. The Red message is the object that passes through the RedBot&#39;s nodes, it contains helpers and data like <em>originalMessage</em>, <em>.api()</em>, <em>.chat()</em>. The main purpose of this is to send multiple messages to the user in a specific order. If the <strong>Pass through</strong> option is not checked the output pin is still present and it&#39;s used to forward the final result of the chain of promises of an output middleware (stripped out of all helpers and data like <em>originalMessage</em>, <em>.api()</em>, <em>.chat()</em>). In the example above it&#39;s used to forward to a downstream node the result of the call to an external custom API.</li> </ul> <p>For a list of all methods available in the <em>msg.chat</em> object see the [[Extend node|Extend-node]].</p> </script> <script type="text/javascript"> $.RedBot.registerType('chatbot-universal-send', { category: $.RedBot.config.name + ' Platforms', color: '#FFCC66', defaults: { bot: { value: "", type: 'chatbot-universal-node', required: true }, botProduction: { value: "", type: 'chatbot-universal-node', required: false }, track: { value: false }, passThrough: { value: false } }, inputs: 1, outputs: 1, icon: 'chatbot-sender.png', paletteLabel: 'Universal Out', label: function () { return 'Universal Sender'; } }); </script> <script type="text/x-red" data-template-name="chatbot-universal-send"> <div class="form-row"> <label for="node-input-bot" style="display:block;width:100%;">Bot configuration <span class="redbot-environment">(development)</span></label> <input type="text" id="node-input-bot" placeholder="Bot"> </div> <div class="form-row" style="margin-top:25px;"> <label for="node-input-botProduction" style="display:block;width:100%;">Bot configuration <span class="redbot-environment">(production)</span></label> <input type="text" id="node-input-botProduction" placeholder="Bot"> </div> <div class="redbot-form-hint"> Bot for <strong>production</strong> will be launched only if the global variable <em>"environment"</em> in <em>settings.js</em> is set to <em>"production"</em>, otherwise will be used the configuration for <strong>development</strong>. </div> <div class="form-row" style="margin-top:25px;"> <label for="node-input-track" style="margin-bottom:0px;">Track</label> <input type="checkbox" value="true" id="node-input-track" style="margin-top:0px;"> <div class="redbot-form-hint"> Track response of the user for this message: any further answer will be redirect to the output pin. </div> <label for="node-input-track" style="margin-bottom:0px;margin-top:15px;">Pass Through</label> <input type="checkbox" value="true" id="node-input-passThrough" style="margin-top:0px;"> <div class="redbot-form-hint"> Forward the message to the output pin after sending (useful to chain messages and keep the right order) </div> </div> </script> <script type="text/x-red" data-help-name="chatbot-universal-send"> <p>Output node for Universal connector.</p> </script>