7

I am trying to make an assistant app and was using the cloud firestore service of firebase to send the response back to my app using webhook as fulfilment. I have used 'session' parameter in request JSON according to this documentation and sending fulfilmentText as response to the user. But whenever user launches the app, a new session is created which I don't want. I simply want, just a single entry for each user in my database so how to achieve that using dialogflow.

In Alexa Skill, we have deviceId as parameter by which we can uniquely identify the user irrespective of the session id but is there any parameter in the dialogflow request JSON. If not, then how to achieve this task without it.

The request JSON I am getting from Dialogflow has a userID in it, so can I use the userId or should I go with userStorage provided the userStorage parameter is not available in the request JSON.

request.body.originalDetectIntentRequest { source: 'google',   version: '2',   payload:     { surface: { capabilities: [Object] },
     inputs: [ [Object] ],
     user: 
      { locale: 'en-US',
        userId: 'ABwppHG5OfRf2qquWWjI-Uy-MwfiE1DQlCCeoDrGhG8b0fHVg7GsPmaKehtxAcP-_ycf_9IQVtUISgfKhZzawL7spA' },
     conversation: 
      { conversationId: '1528790005269',
        type: 'ACTIVE',
        conversationToken: '["generate-number-followup"]' },
     availableSurfaces: [ [Object] ] } }

EDIT : Thank You @Prisoner for the answer but I am unable to send the random ID generated in the response and set in in the payload. Below is the code where I am generating the uuid and storing it in firestore. What I am doing wrong in the below code due to which new uuid is generated for returning user and therefore response is shown as No document found in the database. I suppose I am not sending uuid appropriately. Please help.

exports.webhook = functions.https.onRequest((request, response) => {


    console.log("request.body.queryResult.parameters", request.body.queryResult.parameters);
    console.log("request.body.originalDetectIntentRequest.payload", request.body.originalDetectIntentRequest.payload);

    let userStorage = request.body.originalDetectIntentRequest.payload.user.userStorage || {};
    let userId;
    console.log("userStorage", userStorage);

    if (userId in userStorage) {
      userId = userStorage.userId;
    } else {
      var uuid = require('uuid/v4');
      userId = uuid();
      userStorage.userId = userId
    }

    console.log("userID", userId);

    switch (request.body.queryResult.action) {
      case 'FeedbackAction': {

            let params = request.body.queryResult.parameters;

            firestore.collection('users').doc(userId).set(params)
              .then(() => {

              response.send({
                'fulfillmentText' : `Thank You for visiting our ${params.resortLocation} hotel branch and giving us ${params.rating} and your comment as ${params.comments}.` ,
                'payload': {
                  'google': {
                    'userStorage': userStorage
                  }
                }

                });
                return console.log("resort location", params.resortLocation);
            })
            .catch((e => {

              console.log('error: ', e);

              response.send({
             'fulfillmentText' : `something went wrong when writing to database`,
             'payload': {
               'google': {
                 'userStorage': userStorage
               }
             }
                });
            }))

        break;
      }
        case 'countFeedbacks':{

          var docRef = firestore.collection('users').doc(userId);

          docRef.get().then(doc => {
              if (doc.exists) {
                  // console.log("Document data:", doc.data());
                  var dat = doc.data();
                  response.send({
                    'fulfillmentText' : `You have given feedback for ${dat.resortLocation} and rating as ${dat.rating}`,
                    'payload': {
                      'google': {
                        'userStorage': userStorage
                      }
                    }
                  });

              } else {
                  // doc.data() will be undefined in this case
                  console.log("No such document!");

                  response.send({
                    'fulfillmentText' : `No feedback found in our database`,
                    'payload': {
                      'google': {
                        'userStorage': userStorage
                      }
                    }
                  });

              }
              return console.log("userStorage_then_wala", userStorage);
          }).catch((e => {
              console.log("Error getting document:", error);
              response.send({
                'fulfillmentText' : `something went wrong while reading from the database`,
                'payload': {
                  'google': {
                    'userStorage': userStorage
                  }
                }
              })
          }));

          break;
        }
Lakshay Nagpal
  • 305
  • 5
  • 10

1 Answers1

7

You have a couple of options, depending on your exact needs.

Simple: userStorage

Google provides a userStorage object which is persisted across conversations when it can identify a user. This lets you store your own identifier when you need to track when a user returns.

The easiest way to do this is to check the userStorage object for the identifier when your webhook is called. If it doesn't exist, create one using something like a v4 UUID and save it in the userStorage object.

If you are using the actions-on-google library, the code might look something like this:

let userId;
// if a value for userID exists un user storage, it's a returning user so we can
// just read the value and use it. If a value for userId does not exist in user storage,
// it's a new user, so we need to generate a new ID and save it in user storage.
if (userId in conv.user.storage) {
  userId = conv.user.storage.userId;
} else {
  // Uses the "uuid" package. You can get this with "npm install --save uuid"
  var uuid = require('uuid/v4');
  userId = uuid();
  conv.user.storage.userId = userId
}

If you are using the dialogflow library, you can use the above, but you'll need this line first:

let conv = agent.conv();

If you're using the multivocal library, it does all of the above for you and will provide a UserID in the environment under the path User/Id.

If you're handling the JSON directly, and you are using the Dialogflow v2 protocol, you can get the userStorage object by examining originalDetectIntentRequest.payload.user.userStorage in the JSON request object. You'll set the payload.google.userStorage object in the JSON response. The code is similar to the above and might look something like this:

let userStorage = body.originalDetectIntentRequest.payload.user.userStorage || {};
let userId;
// if a value for userID exists un user storage, it's a returning user so we can
// just read the value and use it. If a value for userId does not exist in user storage,
// it's a new user, so we need to generate a new ID and save it in user storage.
if (userId in userStorage) {
  userId = userStorage.userId;
} else {
  // Uses the "uuid" package. You can get this with "npm install --save uuid"
  var uuid = require('uuid/v4');
  userId = uuid();
  userStorage.userId = userId
}

// ... Do stuff with the userID

// Make sure you include the userStorage as part of the response
var responseBody = {
  payload: {
    google: {
      userStorage: JSON.stringify(userStorage),
      // ...
    }
  }
};

Note the first line of the code - if userStorage doesn't exist, use an empty object. It won't exist until you send a response that includes storing something in it for the first time, which will happen in the last few lines of this code.

Advanced: Account Linking

You can request users to sign in to your Action using Google Sign In. This can be done just using voice for the simplest of cases and would only interrupt the flow the first time.

After this, your Action is given a JWT which contains their Google ID which you can use as their identifier.

If you're using the actions-on-google library, you can get the ID from the decoded JWT with a line such as:

const userId = conv.user.profile.payload.sub;

In the multivocal library, the ID from the decoded JWT is available in the environment under the path User/Profile/sub

Deprecated: Anonymous User ID

You'll see some answers here on StackOverflow that reference an Anonymous User ID. Google has deprecated this identifier, which was not always a reliable way to verify returning users, and will be removing it 1 Jun 2019.

This code is currently still being sent, but this will be removed starting 1 Jun 2019.

pegasuspect
  • 991
  • 4
  • 15
Prisoner
  • 49,922
  • 7
  • 53
  • 105
  • Thanks for your reply. I'll refrain myself from using google account. I would like to know more about the first option and how to go on implement the same via dialogflow using the request they send to the server. Currently, I am saving 'session' as the parameter in the database which gets changed everytime a user uses the app, close it and again use it after some time. – Lakshay Nagpal Jun 10 '18 at 15:37
  • I've updated my answer with code. As the name "session" suggests - this does not contain something that would be long-lasting. – Prisoner Jun 11 '18 at 02:11
  • Hi @Prisoner Thanks for the above sample code but it's not working for me. Error shown to me is "TypeError: Cannot read property 'userStorage' of undefined" and the request I am receiving does not contain userStorage key in it. – Lakshay Nagpal Jun 11 '18 at 17:58
  • I am receiving the request as ` request.body.originalDetectIntentRequest { source: 'google', version: '2', payload: { surface: { capabilities: [Object] }, inputs: [ [Object] ], user: { locale: 'en-IN', userId: 'ABwppHEs6gM3C-pm_wy68nINa-DQzI7WUYzhsvjkyasf5dzaHK0cMGibAfCwfDK4cojQZ0ABbtS0UTCj8IavouTq' }, conversation: { conversationId: '1528729829045', type: 'ACTIVE', conversationToken: '["generate-number-followup"]' }, availableSurfaces: [ [Object] ] } }` – Lakshay Nagpal Jun 11 '18 at 17:59
  • Please update your question with the code you're using, since it sounds like it is different than any of my examples, and with the JSON you're getting. trying to show code and errors in comments are very difficult to read. – Prisoner Jun 11 '18 at 18:59
  • Updated!! @Prisoner – Lakshay Nagpal Jun 12 '18 at 09:07
  • Tried to clarify the "userStorage" section further with more explanation after the code that should address your issue. As I said, and I repeat in that section, do not use the userId since it is deprecated. – Prisoner Jun 12 '18 at 10:08
  • Thanks for the help @Prisoner. But I am facing another issue that I have updated in the question. Please help me regarding the above updated issue. Thanks again :) – Lakshay Nagpal Jun 13 '18 at 11:50
  • Your question doesn't seem updated, but if this is a new question, then asking it as a new question is typically best rather than editing a previous question, particularly if this does answer your question. – Prisoner Jun 13 '18 at 11:53
  • Sorry but just now I updated my question and I didn't start a new thread as it was linked to what you have answered which I am unable to implement in my code and therefore edited my question. – Lakshay Nagpal Jun 13 '18 at 11:59
  • It isn't clear what error you're getting (giving us the error might help), but I did find an error in the code I provided (which I have now corrected in my code). In the JSON you send back, the value for `userStorage` must be a string, so typically we JSON encode the object into a string with `JSON.stringify()`. – Prisoner Jun 13 '18 at 12:15
  • 1
    Hi @Prisoner, I have implemented the code in my app 'Voice Cricket' which is live on Assistant Skill Store. I did a bit of modification on the sample code you provided. You can check the modified code here. Thanks for the immense help! https://gist.github.com/LakshayNagpal/0c68a90173ccc8a072ef6a03d3d1f8ff – Lakshay Nagpal Jun 15 '18 at 08:30