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
210 lines (202 loc) • 13.7 kB
HTML
<script type="text/javascript">
RED.nodes.registerType('chatbot-extend', {
category: 'RedBot',
color: '#FFCC66',
defaults: {
codeJs: {
value: 'node.chat.in(function(message) {\n'
+ ' return new Promise(function(resolve, reject) {\n'
+ ' resolve(message);\n'
+ ' });\n'
+ '});'
},
platform: {
value: '',
required: true
},
noerr: {
value: 0,
required: true,
validate: function(v) {
return ((!v) || (v === 0)) ? true : false;
}}
},
inputs: 0,
outputs: 0,
paletteLabel: 'Extend Chat Server',
icon: 'chatbot-extend.png',
label: function() {
return 'Extend Chat Server';
},
oneditprepare: function() {
var _this = this;
this.editor = RED.editor.createEditor({
id: 'node-input-func-editor',
mode: 'ace/mode/javascript',
value: $('#node-input-codeJs').val(),
globals: {
node: true,
msg:true,
context:true,
RED: true,
util: true,
flow: true,
global: true,
console: true,
Buffer: true,
setTimeout: true,
clearTimeout: true,
setInterval: true,
clearInterval: true
}
});
// get base url
var nodeRedUrl = '';
if (RED.settings.httpNodeRoot) {
nodeRedUrl = RED.settings.httpNodeRoot;
}
// load platforms
$.get(nodeRedUrl + 'redbot/platforms/classes')
.done(function(response) {
var platforms = response.platforms;
// build transport options
var idx;
var transportOptions = '';
for(idx = 0; idx < platforms.length; idx++) {
transportOptions += '<option value="' + platforms[idx].id + '">'
+ (platforms[idx].name != null ? platforms[idx].name : platforms[idx].id)
+ '</option>';
}
$('#node-input-platform')
.html('<option value="">Select which platform to extend</option>' + transportOptions)
.val(_this.platform);
});
},
oneditresize: function(size) {
var dialogForm = $('#dialog-form');
var formRowSelect = $('.form-row-select', dialogForm);
var height = dialogForm.height() - formRowSelect.height() - 30;
$('.node-text-editor').css('height', height + 'px');
this.editor.resize();
},
oneditsave: function() {
var annot = this.editor.getSession().getAnnotations();
this.noerr = 0;
$('#node-input-noerr').val(0);
for (var k=0; k < annot.length; k++) {
if (annot[k].type === 'error') {
$('#node-input-noerr').val(annot.length);
this.noerr = annot.length;
}
}
$('#node-input-codeJs').val(this.editor.getValue());
this.editor.destroy();
delete this.editor;
}
});
</script>
<script type="text/x-red" data-template-name="chatbot-extend">
<div class="form-row form-row-select">
<label for="node-input-platform" style="width: auto;margin-right:10px;">Extend Platform</label>
<select id="node-input-platform" placeholder="Select which platform to extend">
</select>
</div>
<div class="form-row">
<input type="hidden" id="node-input-codeJs" autofocus="autofocus">
<div style="height: 250px; min-height:150px;margin-top: 25px;" class="node-text-editor" id="node-input-func-editor" ></div>
</div>
</script>
<script type="text/x-red" data-help-name="chatbot-extend"><p>At the heart of <strong>RedBot</strong> there's <em>chat-platform.js</em>, a library to translate messages from different platform (<strong>Telegram</strong>, <strong>Slack</strong>, <strong>Facebook Messenger</strong>, etc) to common payloads in order to be used inside <strong>Node-RED</strong>.
In this way a text message containing <em>"Hello world!"</em> from <strong>Facebook</strong> or <strong>Telegram</strong> will be translated into the same payload by the receiver nodes and injected into the flow, the answer to this message (<em>"Hello to you!"</em>) will be then translated and sent back to the platform using the appropriate API call.</p>
<p>The <em>chat-platform.js</em> it's a kind of framework to build connectors for different platforms, it provides basic functionalities (like handling the chat context, etc) while the implementation details for the specific platform is delegated to chunck of codes called <em>middlewares</em>. It's very similar to <strong>Express.js</strong> in it's philosophy, a middleware is something like this</p>
<pre><code class="lang-javascript">node.chat.in(message => {
return new Promise((resolve, reject) => {
if (message.type === 'my-type') {
// do something
resolve(message);
}
});
});
</code></pre>
<p>A <em>middleware</em> is a function that returns a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">promise</a>, it receives a message as argument and - in general - resolves the promise with a message or, if an error occured during the computation, it rejects with an error.</p>
<p>The implementation of chat connector with <em>chat-platform.js</em> consists in two chains of middlewares, one for inbound messages and one for outbound messages, that are executed sequentially when a message is received <strong>from</strong> the chat platform and when a message is to be sent <strong>to</strong> the chat platform.</p>
<p>Each middleware generally takes care of one type of message. Keeping up with all chat platform's API is hard, for this reason keeping the code small, modular and maintable is vital.
For example a middleware that handles incoming photo message could be something like this:</p>
<pre><code class="lang-javascript">node.chat.in(message => {
return new Promise((resolve, reject) => {
if (message.originalMessage.type === 'picture' && message.originalMessage.url != null) {
fetch(message.originalMessage.url)
.then(
buffer => {
message.payload.content = buffer;
message.payload.type = 'photo';
resolve(message);
},
error => reject(`Error downloading ${message.originalMessage.url}`)
);
} else {
resolve(message);
}
});
});
</code></pre>
<p>The <code>message</code> variable contains the <strong>Node-RED</strong> message that will go out of the node receiver, this middleware has to detect if the incoming message is a picture, in that case it has to download the photo file and store the retrieved data in a message attribute otherwise let the message flow through the next middleware (the <em>.resolve()</em> in the <em>else</em> block).</p>
<p>The received payload is always stored in the <code>originalMessage</code> key of the message, the content depends on the specific chat platform we're dealing with and it's usually where the middleware would sniff for particular keys to try to understand which kind of message is arrived.
In case the middleware decides to handle the message, it has to</p>
<ol>
<li>store the content of the message (a string, a buffer, etc.) in the key <code>message.payload.content</code></li>
<li>assign a type to the message, <em>RedBot</em> supports a number of types out of the box (<em>audio</em>, <em>buttons</em>, <em>command</em>, <em>contact</em>, <em>dialog</em>, <em>document</em>, <em>inline-buttons</em>, <em>inline-query</em>, <em>invoice</em>, <em>invoice-shipping</em>, <em>location</em>, <em>message</em>, <em>photo</em>, <em>payment</em>, <em>request</em>, <em>response</em>, <em>video</em>), but new types can be defined in order to expand the platform.</li>
<li>resolve the promise with the new message</li>
</ol>
<p>In case a middleware has handled a message (it has changed the type with <code>message.payload.type = 'a-type';</code>), the rest of the inbound chain of middlewares will be skipped since the incoming message has been already <em>resolved</em>.
In case of fatal error, like a broken link, a proper error must be raised with <code>reject(my_error)</code>: the error will be shown in the system console and the <strong>Node-RED</strong> debug panel, so it should be verbose enough to understand want went wrong.</p>
<p>In case the <code>Extend node</code> is used to add a custom message type (like in the first example), it's reccomended to register the message type with</p>
<pre><code class="lang-javascript">node.chat.registerMessageType('my-type', 'My Type');
</code></pre>
<p>in this way the new message type will appear in the drop down menu of the <code>Rules node</code>.</p>
<p>In a very similar way works the outbound chain: in order to extend the chat platform to support a new message type for example <em>bitcoin</em>:</p>
<pre><code class="lang-javascript">node.chat.out('bitcoin', message => {
return new Promise((resolve, reject) => {
fetch(`http://transfer-bitcoin.ahah/to/${message.payload.chatId}`, { amount: message.payload.content })
.then(
() => resolve(message),
error => reject(`Not enough funds`)
);
});
});
</code></pre>
<p>The outbound chain of middleware it's different since it accepts also a <code>type</code>: the middleware will be executed only for this kind of messages, in this case the middleware should</p>
<ol>
<li>execute the specific API call using the <code>message.payload.chatId</code> and <code>message.payload.content</code></li>
<li>resolve the message when the operation is completed</li>
</ol>
<p>The value of <code>message.payload.chatId</code> is filled by <em>chat-platform.js</em> and it's a unique identifier of the user in the chat platform (it could be a string or a number, depends on the chat platform implementation), <code>message.payload.content</code> is the content of the message (in this example the amount of bitcoin).</p>
<p>The <em>message</em> variable is the <strong>Node-RED</strong> object that runs through the flow, so for example it's possible to access the chat context in the same way as in <code>Function node</code>:</p>
<pre><code class="lang-javascript">node.chat.out(message => {
var chat = message.chat();
return new Promise((resolve, reject) => {
chat.set('lastMessageSent', (new Date).toString())
.then(resolve)
.catch(e => reject('Error on storing the timestamp'));
});
});
</code></pre>
<p>The <code>.out()</code> method without a type as argument will be executed for all message types, in this example a timestamp is stored in the chat context for all outgoing messages. It's important to always resolve or reject the promise to pass the control to the next middleware or break the chain with an meaningful error, failing to do this, the message will never reach the API.</p>
<p>These are the method available to add middlewares and extend the chat platform:</p>
<dl class="message-properties">
<dt>node.chat.use(func)<dd>The middleware will be execute for inbound and outbound messages, before any middleware registered with <code>.in()</code> and <code>.out()</code></dd>
<dt>node.chat.in(func)<dd>The middleware will be execute for inbound, after all middlewares registered with <code>.use()</code></dd>
<dt>node.chat.out(func)<dd>The middleware will be execute for all outbounds messages, after all middlewares registered with <code>.use()</code></dd>
<dt>node.chat.out(myType, func)<dd>The middleware will be execute for outbound messages of type <em>myType</em>, after all middlewares registered with <code>.use()</code> and <code>.out()</code> without a type</dd>
<dt>node.chat.registerEvent(name[, label])<dd>Register the an event in order to be available in the drop-down of the <code>Rules node</code></dd>
<dt>node.chat.registerMessageType(type[, label])<dd>Register the a message type in order to be available in the drop-down of the <code>Rules node</code></dd>
<dt>node.chat.registerPlatform(name[, label])<dd>Register a messaging platform, use the <code>Support Table node</code> to show platforms/available message types</dd>
<dt>node.chat.onChatId(func)<dd>Callback to extract the <em>chatId</em> in a <code>Universal Connector node</code>. The passed function takes the incoming payload as a paramenter and should return a string (a unique identifier of the conversation)</dd>
<dt>node.chat.onTimestamp(func)<dd>Callback to extract the _ts_ (timestamp) in a <code>Universal Connector node</code>. The passed function takes the incoming payload as a paramenter and should return a moment.js value (the date and time of the message), leave blank to get the current date and time</dd>
<dt>node.chat.onUserId(func)<dd>Callback to extract the <em>userId</em> in a <code>Universal Connector node</code>. The passed function takes the incoming payload as a paramenter and should return a string (a unique identifier of the user)</dd>
<dt>node.chat.onMessageId(func)<dd>Callback to extract the <em>messageId</em> in a <code>Universal Connector node</code>. The passed function takes the incoming payload as a paramenter and should return a string (a unique identifier of the incoming message)</dd>
<dt>node.chat.onStart(func)<dd>Callback when the node is initialized, must return a promise.</dd>
<dt>node.chat.onStop(func)<dd>Callback when <strong>Node-RED</strong> is shut down, must return a promise</dd>
</dl>
<p>This node is available for all platforms</p>
</script>