3

I've got my Lex bot configured with a Lambda function and integrated with Facebook messenger. That is all working well. Now I'm trying to get the Facebook user information in order to create a personalized welcome response using the user's name.

I can get the user information, so that is working well. Here's my Lambda code:

exports.handler = (event, context, callback) => {
console.log("EVENT= "+ JSON.stringify(event));
var start = new Date().getTime();

if (event.sessionAttributes == null){ event.sessionAttributes = {}; } 
if (event.requestAttributes == null){ event.requestAttributes = {}; }

// get messenger platform type that user used to access this Lex bot
if (event.requestAttributes['x-amz-lex:channel-type']){
    var accessType = event.requestAttributes['x-amz-lex:channel-type']; // 'Facebook' or 'Twilio-SMS'
} else {
    var accessType = "other";
}
if (!event.sessionAttributes['userInfo']) {

    if (accessType == "Facebook"){
        var pageAccessToken = event.requestAttributes['x-amz-lex:facebook-page-id'];
        var PSID = event.requestAttributes['x-amz-lex:user-id'];
    request({
                uri: 'https://graph.facebook.com/v2.6/'+PSID+'?fields=first_name,last_name,gender&access_token='+pageAccessToken
            },
            function (error, response, body){
                var end = new Date().getTime();
                console.log(end - start);

                body = JSON.parse(body);

                if (!error && response.statusCode == 200) {
                    event.sessionAttributes['userInfo'] = {
                        "first_name": body['first_name'],
                        "last_name": body['last_name'],
                        "gender": body['gender'],
                        "id": body['id']
                    };

                    console.log("FB USERINFO:"+ body);
                } else {
                    console.log("FB ERROR: "+error+" +++++++RESPONSE: "+response);
                }
            }
        );
    try {
            intentProcessor(event,
                (response) => {
                    console.log("RESPONSE= "+ JSON.stringify(response));
                    callback(null, response);
                });
        } catch (err) {
            callback(err);
        }
} else {
    try {
        intentProcessor(event,
            (response) => {
                console.log("RESPONSE= "+ JSON.stringify(response));
                callback(null, response);
            });
    } catch (err) {
        callback(err);
    }
}
};

In the code above I am doing try synchronously and Lex is responding to facebook before the callback (the no name function) even starts, and so sessionAttributes are never set with the userInfo.

But if I do this asynchronously, and place the try inside the callback by the time the user info is received and handled in the callback function, Facebook times out and the response never makes it back to the user.

Why does the asynchronous way time out in Facebook? Is there something wrong with this method? Is there a way to stall Facebook with a "one moment please" message until the callback returns?

Jay A. Little
  • 3,239
  • 2
  • 11
  • 32
  • Is this bot responding to webhook message events for Facebook? I know they have a 20 second requirement on responses, and a cold-start + roundtrip on the request for user info _might_ be long enough to timeout. Also, if you're responding to webhooks, you should simply be passing your data along to be processed and immediately send your 200 response for the webhook having been processing (avoid trying to do other operations like HTTP requests before responding the the webhook request). – mootrichard Feb 08 '18 at 19:10
  • @mootrichard Sounds great. Can you offer an answer with the implementation of sending a 200 response? – Jay A. Little Feb 09 '18 at 07:12
  • I've tried `res.sendStatus(200);` and `res.status(200).send('OK');` both error with "...is not a function" – Jay A. Little Feb 09 '18 at 08:15
  • I've installed Express, so that `res.status(200).send('OK');` no longer errors. But the outcome is the same as original question. Facebook fails to receive response even though the response is being built in under 20 seconds. – Jay A. Little Feb 09 '18 at 13:58
  • Could you confirm that you're using Amazon API Gateway for processing the request? You should be able to send your response using `lambda-proxy` and setting your response in the `callback(null, response)` to have `statusCode: 200`. If you can confirm that, I can write up an answer with an example that should work. – mootrichard Feb 09 '18 at 17:26
  • @mootrichard I am not using API Gateway. Facebook's webhook is pointed directly at the Lex bot Endpoint URL. But I may be able to configure API Gateway and start using it that way if that is needed to get this working. – Jay A. Little Feb 10 '18 at 03:06
  • @mootrichard If the try is placed within the no name function, as it is, does the callback actually reach the parent function? I've just tried `return callback()` but that didn't solve it. But I think this could be the real issue here. – Jay A. Little Feb 11 '18 at 04:29

1 Answers1

3

Apparently you were not the only one with this problem. The issue is here:

event.sessionAttributes['userInfo'] = {
                    "first_name": body['first_name'],
                    "last_name": body['last_name'],
                    "gender": body['gender'],
                    "id": body['id']
                };

Lex does not support multi-depth arrays/objects in the sessionAttributes value. Remove the ['userInfo'] level (both here and in any referencing code, of course) and everything should work.

Source: https://docs.aws.amazon.com/lex/latest/dg/context-mgmt.html#context-mgmt-complex-attributes

Disclaimer: I know @Jay personally and we spent a few hours video-chatting about this problem tonight to debug it. That doesn't appear to be against the rules, but I defer to the mods/admins for how to handle reputation.

Bing
  • 3,071
  • 6
  • 42
  • 81
  • as a side note: you can pass the session attributes `userInfo` as a string and later on converting it back to json to overcome milti-depth problem. – sid8491 Feb 13 '18 at 11:30
  • Yeah, or you could use a delimiting character (ex: `-`) to "group" your objects. Ex: `"userInfo-first_name": body['first_name']`. Plenty of work-arounds (I only provided the most simple one in my original response), but the underlying problem was the object being more than one level deep. – Bing Feb 13 '18 at 20:25