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
302 lines (289 loc) • 14.7 kB
HTML
<script type="text/javascript">
RED.nodes.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 = '';
if (RED.settings.httpNodeRoot) {
nodeRedUrl = RED.settings.httpNodeRoot;
}
// 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">
RED.nodes.registerType('chatbot-universal-receive', {
category: 'RedBot 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'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'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>'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: '+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="lang-javascript">node.chat.onChatId(payload => payload.sms.from);
node.chat.onMessageId(payload => payload.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 practise to use the chat context for storing this information, for example</p>
<pre><code class="lang-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 type and content 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>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">
RED.nodes.registerType('chatbot-universal-send', {
category: 'RedBot 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
},
outputs: {
value: 0
}
},
inputs: 1,
outputs: 1,
icon: 'chatbot-sender.png',
paletteLabel: 'Universal Out',
label: function () {
return 'Universal Sender';
},
oneditsave: function() {
var track = $('#node-input-track').is(':checked');
var passThrough = $('#node-input-passThrough').is(':checked');
this.outputs = track || passThrough ? 1 : 0;
}
});
</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>