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
314 lines (303 loc) • 17.2 kB
HTML
<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 Receiver',
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’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 <a href="https://www.notion.so/04668c7a415547bc9f34be57dd063db2">Extend node</a> .</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><p>Extract a <strong>chatId</strong> from the payload: it’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) </p>
</li>
<li><p>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. </p>
</li>
<li><p>Extract the message content it also should (but is not mandatory) </p>
</li>
<li><p>Extract the <em>timestamp</em> of the message </p>
</li>
<li><p>Extract a <em>messageId</em> to properly reference inbound and outbound messages in the external service timeline</p>
</li>
</ol>
<p>Like explained in <a href="https://www.notion.so/04668c7a415547bc9f34be57dd063db2">Extend node</a> a connector handles inbound messages with a chain of middlewares: chunk of codes executed sequentially in order to accomplish steps 1 to 3. In order to keep the code simple and maintainable 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>’s flow.</p>
<p>For example suppose that a SMS gateway calls the <strong>Node-RED</strong> instance web-hook with this payload</p>
<pre><code class="language-javascript">{
sms: {
from: '+39347123456',
to: '+39338654321',
id: '_xyz',
text: 'Hello there!',
sender_name: 'Alan Turing'
}
}
</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="language-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 => message.sms.from);
node.chat.onMessageId(message => message.sms.id);
node.chat.in(message => {
return new Promise((resolve, reject) => {
// check that the incoming payload is an actual "text" message
if (message.originalMessage.sms != null && message.originalMessage.sms.text != '') {
message.payload.type = 'message';
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’s a good practice to use the chat context for storing this information, for example</p>
<pre><code class="language-javascript">node.chat.onChatId(payload => payload.sms.from);
node.chat.onMessageId(payload => payload.sms.id);
node.chat.in(message => {
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('name', message.originalMessage.sms.sender_name))
.then(() => {
message.payload.type = 'message';
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="language-javascript">// this middleware handles the message of type 'message'
node.chat.out('message', message => {
return request(
'https://my.api/send',
data: {
text: message.payload.content,
to: message.payload.chatId
}
).then(response => {
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><p><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></p>
</li>
<li><p><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’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’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’s used to forward to a downstream node the result of the call to an external custom API.</p>
</li>
</ul>
<p>For a list of all methods available in the <em>msg.chat</em> object see the <a href="https://www.notion.so/04668c7a415547bc9f34be57dd063db2">Extend node</a> .</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 Sender',
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>