Facebook Chatbots with OpenWhisk

This weekend I decided to take a quick look at running Facebook Chatbot, aka Facebook Messenger Platform, on the OpenWhisk platform. In general, it worked just fine and 90% of the issues I had were me not reading the docs correctly and making code screw ups. But I can say that I've got it working successfully now and it isn't difficult at all.

Look - it works!

In this blog post I'm going to explain what you would need to do to get a Facebook Chatbot running on OpenWhisk. I will not cover the entire process of creating a bot. For that, you want to read Facebook's Getting Started guide which covers the things you need to setup and discusses the Node code. The code I'm going to share is a modified version of their Node code. It isn't very clean, but it should be enough to get you started if you want to use OpenWhisk for your bot.

The first thing you'll need to do is create a new OpenWhisk action. You are welcome to use my code to start off with, just remember what I said - it is a bit messy. When you create your action, you must use the web action flag so that Facebook can hit it. Given an action name of fbbot, you would enable web action support like so:

wsk action update fbbot --web true

Now you need the URL. You can get that like so:

wsk action get fbbot --url

When you follow Facebook's guide, this is the URL you use for the webhook. Do not add JSON to the end! I always do that as I'm used to building JSON-responding services, but Facebook requires a non-JSON response for verification. Just use the URL as is.

Now for the code. I'm going to share the whole thing at the end, but first I want to share a few bits and pieces. Facebook will either send you a GET or POST request. The GET is always used for verification. You can handle this like so:

 if(args["__ow_method"] === "get") {
        let challenge = args["hub.challenge"];
        let token = args["hub.verify_token"];

        if(token === 'test_token') {
            return { body:challenge };
        } else {
            //todo: return an error condition
        }
    } else if(args.__ow_method === "post") {

Notice I sniff for the HTTP method first, get the values out of the args, and then check to ensure the token is correct. "test_token" was set on the Facebook side and should probably be a bit more secure. As the comment clearly says, I should add an error result there.

The next part of the code will handle responding back to the message. For the most part I followed Facebook's Node sample, but I modified things a bit. Facebook doesn't really how you respond to its HTTP call to your server. Your response means nothing essentially. Instead your code makes its own HTTP request to Facebook's API to respond.

However - if you follow Facebook's lead and "respond early" while the response HTTP call is about to go out, that will not work on OpenWhisk. When OpenWhisk things you're done, it's going to shut down your serverless code. Therefore, the code I used basically waits till everything is done before creating a response.

Since Facebook can, and will, batch responses, that meant using Promise.all as a way of listening for all the possible calls to finish. So here is the entire code snippet. Once again, please remember this leaves a lot of polish out.

const rp = require('request-promise');
const PAGE_ACCESS_TOKEN = 'CHANGE ME TO YOUR TOKEN';

function main(args) {
    console.log('fbbot ow activated');

    if(args["__ow_method"] === "get") {
        let challenge = args["hub.challenge"];
        let token = args["hub.verify_token"];

        if(token === 'test_token') {
            return { body:challenge };
        } else {
            //todo: return an error condition
        }
    } else if(args.__ow_method === "post") {
        console.log('post call');

        //ensure it's a page, todo: return an error?
        if(!args.object === "page") return;

        return new Promise((resolve, reject) => {

            let promises = [];
            args.entry.forEach((entry) => {
                //process each entry
                entry.messaging.forEach((event) => {
                    if(event.message) promises.push(process(event));
                });

            });

            Promise.all(promises).then(() => {
                resolve({result:1});
            })
            .catch((e) => {
                console.log('error in all call');
                console.log('message='+e.message);
                console.log('error='+JSON.stringify(e.error));
                reject({error:e.error});
            });

        });

    }

}

function process(msg) {
    let senderID = msg.sender.id;
    let recipientID = msg.recipient.id;
    let timeOfMessage = msg.timestamp;
    let message = msg.message;

    console.log(`Received message for user ${senderID} and page ${recipientID} at ${timeOfMessage} with message:`);
    console.log(JSON.stringify(message));

    let messageID = message.mid;
    let messageText = message.text;
    //todo: attachments

    var messageData = {
        recipient: {
            id: senderID
        },
        message: {
            text: 'You sent: '+messageText
        }
    };

    return callSendApi(messageData);
}

function callSendApi(messageData) {
    let options = {
            uri: 'https://graph.facebook.com/v2.6/me/messages',
            qs: { access_token: PAGE_ACCESS_TOKEN },
            method: 'POST',
            json: messageData
    }
    return rp(options);
}

The process function is where I did the logic of "You sent:" in response to your message. A real bot would do a bit more work obviously. callSendApi is basically just the API call to Facebook. I've modified it to use request-promise since I have to know when it's done.

In theory you can test this on the page I set up for it, BotTest1, but I'm not promising to keep that site up for long. Now that I've got this done, I'm going to look into integrating it with Watson for more advanced demos.

Let me know if you have any questions!

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

See Also