This guide shows you how to harness the power of native JavaScript promises to build a chat bot on AWS Lambda and integrate it with Facebook Messenger platform.

This is not a “from scratch” tutorial for creating a Facebook bot using Lambda. Igor Khomenko has a useful guide on Bot Tutorials that covers the essentials. Instead it shows you how to use promises to control the sequence of events such as loading the state of a conversation from a database or reliably sending a group of messages in order.

AWS Lambda lets you run code without provisioning or managing servers. You pay only for the compute time you consume. Which makes it great for building chat bots. Lambda functions are containerised and should be almost entirely stateless. For this reason to be useful our bot needs to persist the state of a conversation to a database so it can process the next message in context.

I say almost entirely stateless, it is recommended that you instantiate database connections outside of the Lambda handler function so that they can be reused across invocations. This reduces invocation time which reduces costs.

The Messenger platform guidelines suggest sending a few separate messages instead of one long one when we want to communicate multiple things during a conversation. This requires our bot to be able to send multiple messages in response to a single message and for the person we are interacting with to receive those messages in order. To ensure this we need to wait for a response from the Send API before sending the next message.

The problem we need to solve is that in Node.js the callbacks for database CRUD operations and HTTP requests, for example, are asynchronous. We need them to behave synchronously, otherwise we loose control of the flow. This is where promises come in. If you are unfamiliar with promises there is a useful introduction on the Google Developers site.

Essentially a promise is a little bit like a single use event handler. A promise can only succeed or fail once. It cannot succeed or fail more than once, neither can it switch from success to failure or vice versa. In the terminology of promises, a promise is either pending or settled. A settled promise is either fulfilled (the promise succeeded) or rejected (the promise failed). A pending promise is one that hasn’t fulfilled or rejected yet. Their real power comes from the way that promises can be sequenced and their resolution handlers chained.

To demonstrate this I’m going to extend the code from Igor Khomenko’s tutorial to read the state of the conversation from a database, respond to the latest message based on the state and then update the database with the new state. If no state exists, the person who sent the message will be asked whether or not they want to join.

Database

I’m using DynamoDB as the database. So the first we need to create a new table with the following details:

  • Table name: Conversations
  • Primary key
    • Partition key: senderId (Number)
    • Sort key: none
  • Table settings: Use default settings

createdynamodbtable

If you’re not familiar with AWS DynamoDB, check out the Developer Guide. Once you’ve created the table, copy down the table ARN, we need this for the IAM role policy. From the navigation pane on the left-hand side of the console, choose Tables. Click on the table name (Conversations) and then choose the Overview tab.

Update The IAM Role Policy

Before we can access the database we need to add the necessary permissions to the security role our Lambda function uses. We do this via the IAM console.

From the navigation pane on the left-hand side of the console, choose Roles. Click on the role name used by your Lambda function (in my case processMessagesRole) and then choose the Permissions tab.

None of the managed policies quite fit our requirements, so we are going to add an inline role policy. To do this choose Create Roll Policy. The select Policy Generator and add a statement with the following details:

  • Effect: Allow
  • AWS Service: Amazon DynamoDB
  • Actions: DeleteItem, GetItem, PutItem, UpdateItem
  • ARN: the ARN of your table

iamroleinlinepolicydocument

The final policy document should look similar to this.

iamroleinlinepolicydocument2

Lambda Function

A quick word of warning: there are some great libraries around for kickstarting your chat bot development. I won’t be using them. I’m going to use the vanilla AWS Lambda Node.js (v4.3.2) environment. That way you can paste the code into the Code tab in the Lambda console.

I’ve made a couple of changes to the code from Igor Khomenko’s tutorial to store the page token and verify token as environment variables.

const PAGE_TOKEN = process.env.PAGE_TOKEN;
const VERIFY_TOKEN = process.env.VERIFY_TOKEN;

To access our database table I’m using the DynamoDB DocumentClient which is part of the AWS JavaScript SDK. The client is instantiated outside the handler to allow it to be reused across invocations.

// AWS DynamoDB
const AWS = require("aws-sdk");
const docClient = new AWS.DynamoDB.DocumentClient();
const table = "Conversations";

Next, our first promise looks up the state of a conversation based on the senderId of the message.

// Look up conversation in database
function getConversation(senderId) {
  var params = {
    TableName: CONVERSATIONS_TABLE,
    Key: {
      "senderId": senderId
    }
  };

  return docClient.get(params).promise();
}

Our second promise stores the state of a conversation.

// Store conversation in database
function putConversation(conversation) {
  var params = {
    TableName: CONVERSATIONS_TABLE,
    Item: player
  };
 
  return docClient.put(params).promise();
}

Since we will be sending quick reply messages as well as text messages. Our third promise replaces the sendTextMessage function with a generic call to the Messenger Send API.

// Call FB Messanger Send API
function callSendAPI(messageData) {
  return new Promise(function(resolve, reject) {
    var body = JSON.stringify(messageData);
    var path = '/v2.6/me/messages?access_token=' + PAGE_TOKEN;
    var options = {
      host: "graph.facebook.com",
      path: path,
      method: 'POST',
      headers: {'Content-Type': 'application/json'}
    };
 
    var callback = function(response) {
      if (response.statusCode != 200) {
        reject(Error(response.statusMessage));
      }

      var d = [];
      response.on('data', function (chunk) {
        d.push(chunk);
      });
      response.on('end', function () {
        resolve(JSON.parse(d.join('')));
      });
    };
 
    var req = https.request(options, callback);
    req.on('error', function(err) {
      reject(err);
    });
    req.write(body);
    req.end();

    console.log('Sent message');
  });
}

Using our promises, the messages are now handled like this.

for (var i = 0; i < messagingEvents.length; i++) {
  var messagingEvent = messagingEvents[i];
 
  console.log("Received a message event:", JSON.stringify(messagingEvent, null, 2));
  if (messagingEvent.message && !messagingEvent.message.is_echo) {
    var sender = messagingEvent.sender.id;
    getConversation(sender).then(function(data) {
      console.log("Stored conversation:", JSON.stringify(data, null, 2));
 
      var conversation = data.Item;
      // Is known?
      if (conversation) {
        // Is known: yes
        conversation.lastMessage = messagingEvent.message;
        // If message contains text echo it, otherwise send a thumbs up
        var messageText = "\ud83d\udc4d";
        if (messagingEvent.message.text) {
          messageText = "Text received, echo: " +
            messagingEvent.message.text.substring(0, 200);
        }
 
        return {
          conversation: conversation,
          messageData: createTextMessageData(sender, messageText),
          updateDB: conversation.hasJoined
        };
      } else {
        // Is known: no
        return processJoinMessage(messagingEvent.message, sender);
      }
    }).then(function(result) {
      if (result.messageData) {
        callSendAPI(result.messageData);
      }
 
      return result;
    }).then(function(result) {
      if (result.updateDB) {
        console.log("There is an update to store:", JSON.stringify(result.conversation, null, 2));
        return putConversation(result.conversation);
      }
    }).catch(function(error) {
      console.error("Error:", JSON.stringify(error, null, 2));
    });
 
    callback(null, "Done");
  }
}

The full code is available on Pastebin.

A sample conversation might look like this.

img_1719
With a little more work we can improve our bot by making the conversation more personal. It would be nice if instead of say “Hey! Would you like to join?”, we called the sender by their first name. It would also be nice to break this into two messages rather than one long message.

Once a person has messaged our bot we can get basic profile information such as his or her name using the User Profile API.

function getSenderProfile(senderId) {
  return new Promise(function(resolve, reject) {
    var fields = 'fields=first_name,locale,gender';
    var path = '/v2.6/' + senderId + '?' + fields +
      '&access_token=' + PAGE_TOKEN;
    var options = {
      host: "graph.facebook.com",
      path: path,
      method: 'GET'
    };
 
    var callback = function(response) {
      if (response.statusCode != 200) {
        reject(Error(response.statusMessage));
      }

      var d = [];
      response.on('data', function (chunk) {
        d.push(chunk);
      });
      response.on('end', function () {
        resolve(JSON.parse(d.join('')));
      });
    };
 
    var req = https.request(options, callback);
    req.on('error', function(err) {
      reject(err);
    });
    req.end();

    console.log('Sent profile request');
  });
}

One of the powerful benefits of promises is what happens when you return something from a then() callback. If you return a value, the next then() is called with that value. However, if you return something promise-like, the next then() waits on it, and is only called when that promise settles.

We can then rewrite the code to split the first of the then() callback. The first callback now returns a value if the database contained a conversation, otherwise a user profile promise is returned. The second then() callback either receives the conversation state or a user profile.

getConversation(sender).then(function(data) {
  console.log("Stored conversation:", JSON.stringify(data, null, 2));
 
  if (data.Item) {
    return data;
  } else {
    return getSenderProfile(sender);
  }
}).then(function(data) {
  console.log("getConversation/getSenderProfile returned:", JSON.stringify(data, null, 2));
 
  var conversation = data.Item;

To ensure that our multi message responses are received in the correct order we create a sequence of promises, one for each message. Since each message is a promise, the next message in the sequence will only be sent once the previous message promise has been settled.

However, this time we don’t want to delay calling the next then() callback. That would only increase the execution time and thus the cost. Instead we want to update the database in parallel. So instead of returning our message promise sequence, we simply pass on the “result” value.

if (result.messageData) {
  result.messageData.reduce(function(sequence, messageData) {
    // Add these actions to the end of the sequence
    return sequence.then(function() {
      console.log('There is a message to send:', JSON.stringify(messageData, null, 2));
      return callSendAPI(messageData);
    }).then(function(response) {
      console.log('Send response:', JSON.stringify(response, null, 2));
    });
  }, Promise.resolve());
}
 
return result;

Our sample conversation now looks like this.

img_1720

And the database item for this conversation now looks like this.

conversationsitem

The full code is available on Pastebin.

Advertisements