Skip to content

Universal Connector for Dialog chat platform

Guidone edited this page Nov 20, 2018 · 7 revisions

This is a tutorial on how to use the Universal Connector node to support the Dialog chat platform.

We'll use the Dialog Bot SDK: open a terminal window in the Node-RED home dir (generally $HOME/.node-red) and install it with

npm install @dlghq/[email protected]

Install the 2.0.2 version, newer version is not compatible with the code below. Edit the settings.js to make the library available in Node-RED in the global context

{
  // ...
  functionGlobalContext: {
    // ...
    environment: 'development',
    DialogBot: require('@dlghq/dialog-bot-sdk')
  }
}

Node-RED must be restarted now.

Download the Dialog client and use the Dialog Bot to create a new chatbot

/bot new mybotnickname mybotname

Support table

and grab the access token. Next add a Universal Connector node and store the token in the configuration

Support table

Then add an Extend Node, this is where the magic happens. The strategy is to make the Dialog Bot up and running in Node-RED and discovery what payload is sent for each message and then implement all necessary middlewares to in the Extend node.

Add an Extend node and select Universal Connector from the drop down:

const DialogBotSDK = node.context().global.get('DialogBot');
node.chat.onStart(function() {
  var chatServer = this;
  var options = chatServer.getOptions();
  options.connector = new DialogBotSDK.Bot(options.token);
  options.connector.onMessage(function(peer, message) {  
    chatServer.receive(message);
  });
  return true;    
});

The onStart is executed once at the startup and it's the right place to initialize the Dialog Bot library using the access token stored in the connector's configuration (_this.getOptions()). When a message is received by the .onMessage() callback is injected in the middlewares chain with the method chatServer.receive(), if you're unsure about what a middleware chain is, check here.

Just send a test message from the Dialog client and inspect the received payload, it should look like this

{ 
  rid: '46016707201745159',
  mid: '4f4176c0-cbf0-11e8-db0a-f43a15df867f',
  sortKey: '1539109361708',
  sender: { 
    peer: { type: 'user', id: 246093853, key: 'u246093853' },
    type: 'user',
    title: 'Guido Bellomo',
    userName: null,
    avatar: null,
    bigAvatar: null,
    placeholder: 'yellow' 
  },
  isOut: false,
  date: '20:22',
  fullDate: 2018-10-09T18:22:41.708Z,
  state: 'unknown',
  isOnServer: true,
  content: { 
    type: 'text',
    text: 'test message',
    media: [],
    extensions: [] 
  },
  attachment: null,
  reactions: [],
  sortDate: 1539109361.708,
  isEdited: false 
}

In order to make the Universal connector to work we have to extract the chatId from the payload, in the payload above a good candidate is the peer key in the sender hash

node.chat.onChatId(message => message.sender.peer.id);

Now we are ready to write the first middleware to handle inbound text message

node.chat.in(function(message) {
  const chat = message.chat();
  if (message.originalMessage.content.type === 'text') {
    message.payload.type = 'message';
    message.payload.content = message.originalMessage.content.text;
  }
  return message;  
});

The originalMessage key of message contains the original payload previously injected in the middlewares chain by chatServer.receive(), the middleware should now extract data from originalMessage and fill in message.payload appropriately: in RedBot the content of the message is always stored in message.payload.content while the type the message is stored in message.payload.type.

The middleware above takes into account translating textual messages (in RedBot the corresponding type is "message"), if the incoming message is not the right type in just returns the message variable as is, continuing the middleware chain. The middleware chain continues untile a middleware fill-in the message type, resolving in this way the message that will then emerge from the ouput pin of the Universal connector. Remember that every middleware must return the message variable or a promise returning the message variable.

Next step is to enable outgoing text message, that means handling message of type message (speaking in RedBot terms):

node.chat.out('message', function(message) {
  const bot = this.getConnector();
  const chat = message.chat();
  const peer = { type: 'user', id: message.payload.chatId };  
  return bot.sendTextMessage(peer, message.payload.content)
    .then(mid => chat.set('messageId', mid))
    .then(() => message);
});

We're going to use the ,sendTextMessage() method of the Dialog Bot SDK (it's crucial here to understand what method/convention belongs to RedBot and what belongs to the DSK we're trying to interface with). We previously stored the Dialog's client instance in the connector key, we can easily get it back with the .getConnetor() method. Using the chatId we create a peer object required by Dialog API, this method returns a promise so we can easily fullfil the requirement of the RedBot middleware which is to always return a promise.

A promise is a JavaScript object that holds a value that will be available in the future at some point after an asynchronous execution. It's a good pattern with the asynchronous nature of NodeJS. One useful feature of promises is that they can be chained to get a more complex promise, that's the purpose of the next .then() functions: the first stores the unique message id of Dialog in the messageId chat context (this is a RedBot convention), the second ensures that the final chained promise returns the middleware's message (also this is a RedBot convention). For more detail about JavaScript promises please check here.

Next step is to support the Buttons node, here what we should do:

  1. first understand how RedBot is storing internally a message with buttons (with a simple Debug node)
  2. read the Dialog's documentation about sending message with buttons (in that case the method is .sendInteractiveMessage())
  3. write the middleware that translated the first into the second

Here is the code

node.chat.out('inline-buttons', function(message) {
  const bot = this.getConnector();
  const chat = message.chat();
  const peer = { type: 'user', id: message.payload.chatId };
  const buttons = [{
    actions: message.payload.buttons
      .filter(button => button.type === 'postback')
      .map(button => ({
        id: button.value,
        widget: {
          type: 'button',
          value: button.value,
          label: button.label
        }
      }))
  }];  
  return bot.sendInteractiveMessage(peer, message.payload.content, buttons)
    .then(mid => chat.set('messageId', mid))
    .then(() => message);  
});

This middleware is called only if the outgoing message type is inline_buttons. Nothing new compared with the middleware above except the method .sendInteractiveMessage() and .filter/.map sequence that maps an array of buttons in RedBot's format to the Dialog's format. In this example only postback are supported.

At this point buttons are displayed correctly, but it's not working yet since Dialog sends the buttons notifications with the .onInteractiveEvent() callback, so we have to fix the initialization of the Universal connector node:

node.chat.onStart(function() {
  var chatServer = this;
  var options = this.getOptions();
  options.connector = new DialogBotSDK.Bot(options.token);
  options.connector.onMessage(function(peer, message) {  
    chatServer.receive(message);
  });
  options.connector.onInteractiveEvent(function(event) {
    // console.log('Button event', event);
    chatServer.receive({
      mid: event.mid,
      sender: { 
        peer: { type: 'user', id: event.uid },
      },
      isOut: false,
      content: { 
        type: 'text', 
        text: event.value
      }
    });
  });
  return true;    
});

The callback basically takes the event returned by the Dialog SDK and translate it into a message payload that the chain of middlewares is able to understand understand. It is basically faking a inbound message. A convenient console.log in the callback can reveal how the button event is shaped by Dialog SDK.

The last middleware of this tutorial will support the incoming photo message: exploring the Dialog SDK we discover that the payload contains a fileURL address of the image, we'll use it to fetch the image and place the obtained binary buffer in the message.payload.content along with the type "photo" (as per RedBot convention):

node.chat.in(function(message) {
  const request = this.request;
  if (message.originalMessage.content.type === 'photo') {
    return request({ url: message.originalMessage.content.fileUrl})
      .then(image => {
        message.payload.type = 'photo';
        message.payload.content = image;  
        return message;
      });
  } 
  return message;    
});

RedBot exposes the library Request as this.request in the middlewares and in this example is used to make http calls.

Middlewares can be used to send and receive messages but also to fix and correct some data before other middlewares kicks in, remember it's a chain of middlewares and are execute sequentially. Some testing with the connector reveals that Dialog SDK always wants a numeric chatId, everything is fine with messages receveide directly from Dialog, but it breaks with Conversation node: chatIds inserted here are always string, we'll use another middleware to fix it

node.chat.out(function(message) {
  if (message.payload.chatId != null && typeof message.payload.chatId === 'string') {
    message.payload.chatId = parseInt(message.payload.chatId, 10);
  } 
  return message;    
});

Pay attention to put this middleware at the beginning: middlewares are executed in the same order they are defined.

The last touch to our Dialog's connector is to declare the name of the platform and the supported message types

node.chat.registerPlatform('dialog', 'Dialog');
node.chat.registerMessageType('message');
node.chat.registerMessageType('photo');
node.chat.registerMessageType('inline-buttons');

This is not strictly necessary, if this step is omitted the transport varable will be defaulted to universal. In case of multiple extend node could be useful to register the name of the platform and the type of message these extensions can handle. It's always possible to dump in the system console a compatibility chart for each platforms with Support table node.

Some tricks related to this example:

  • Always use function() {} middlewares and not arrow functions () => {}: the arrow functions will tie the scope of the middleware to the script and not to the chat instance, methods like this.getConnector() or this.getOptions() will not be available.
  • Dialog SDK doesn't shut down correctly, so for every changes in the Extend node it will be necessary to restart the Node-RED server

Here the complete JavaScript code for the Extend node. Here the complete flow for Node-RED.

Clone this wiki locally