Best way to support multiple users in a NodeJS based system?

Viewed 2413

I'm creating a chatbot in Facebook messenger. I want the chatbot to be able to support several users, obviously. The first time a user accesses the chatbot they are supposed to create a user account. The process for creating the user account is as follows:

  1. When an unregistered user interacts with the chatbot, it gets the user's first name from the Facebook Graph API, and then says "Hello (first name)". It explains that the user needs to create a user profile.
  2. The chatbot asks the user what they want to be called. The options are either (first name) or "Something else"
  3. If the user chooses (first name), the chatbot goes straight to number (5).
  4. If the user chooses "Something else", the chatbot asks what they want to be called, and they type it in.
  5. The chatbot asks the user to select their gender (male, female, other, prefer not to say)
  6. The chatbot asks the user to select their age (from a selection of ranges e.g. 18-25, 26-30 etc.)
  7. Once the profile has been created the chatbot tells the user that in future if they want to talk to the chatbot, they can just say "hi" or "hello".
  8. The chatbot then asks the user what they want to do now and presents the main functions.

Here is what is supposed to happen normally when any user interacts with the chatbot:

  1. User interacts with chatbot.
  2. Chatbot checks to see if user is loaded into memory
  3. If user is loaded into memory, chatbot interacts with user as normal
  4. If user is not loaded, the chatbot tries to get the user from a database
  5. If the user exists in the database, the user's profile is loaded into memory and the chatbot interacts with the user as normal
  6. If the user doesn't exist in the database, they have never used the chatbot before, so they have to create a user profile.

The problem is, this ISN'T happening. When more than one user tries to set up their profile at the same time, the following is happening (let's assume two users - "user A" and "user B":

For person A:

  1. The chatbot gets the user's first name, and says "Hello (person A)", and explains that they need to create a user profile.

  2. The chatbot then asks the user what they want to be called. The options it gives are either (person B) or "Something else".

  3. No matter what the user chooses, they cannot progress any further, because the chatbot keeps repeating stage 2.

So, my code:

User data is stored in a "data" object in a file called user.js:

data: {
    fbID: undefined,    // senderID (user's Facebook PSID)
    firstName: undefined,   // User's first name (only used during user profile creation)
    userID: undefined,  // User ID (i.e. first name or nickname)
    gender: undefined,  // Gender
    age: undefined, //  Age
}

User.js also contains a "reload" function that is used to reload a user. You'll see how this works later:

reload: function(data) {
    self.data.fbID = data.fbid; /** @namespace data.fbid */
    self.data.userID = data.userid; /** @namespace data.userid */
    self.data.gender = data.gender; /** @namespace data.gender */
    self.data.age = data.age;   /** @namespace data.age */
    console.log("Profile reloaded for user '%s'.", self.data.fbID);
},

When the user sends a message to the chatbot, this triggers a webhook event, within app.js:

// Accept POST requests to webhook
app.post('/webhook/', function (req, res) {
    // Parse the request body from the POST
    let body = req.body;
    // Check the webhook event is from a Page subscription
    if (body.object === 'page') {
        body.entry.forEach(function(entry) {     /** @namespace body.entry */
            // Gets the body of the webhook event
            let webhookEvent = entry.messaging[0];      /** @param entry.messaging[] */
            // Get sender ID
            let senderID = webhookEvent.sender.id;
            // Set session ID
            session.set(senderID);
            // Handle webhook event
            handler.handleWebhookEvent(senderID, webhookEvent);
        });
        // Return a '200 OK' response to all events
        res.status(200).send('EVENT_RECEIVED');
    } else {
        // Return a '404 Not Found' if event is not from a page subscription
        res.sendStatus(404);
    }
});

The webhook event is passed to handler.js, where it is handled by the handleWebhookEvent function:

handleWebhookEvent: function(senderID, webhookEvent) {
    let greetingText, delay;
    let handleEvent = function() {
        /**
         * @param webhookEvent
         * @param webhookEvent.message
         * @param webhookEvent.optin
         * @param webhookEvent.delivery
         * @param webhookEvent.postback
         * @param webhookEvent.read
         * @param webhookEvent.account_linking
         * */
        if (webhookEvent.optin) {
            handleOptIn(webhookEvent);
        } else if (webhookEvent.message) {
            handleMessage(senderID, webhookEvent.message);
        } else if (webhookEvent.delivery) {
            handleDelivery(webhookEvent);
        } else if (webhookEvent.postback) {
            handlePostback(senderID, webhookEvent.postback);
        } else if (webhookEvent.read) {
            handleRead(webhookEvent);
        } else if (webhookEvent.account_linking) {
            handleAccountLink(senderID, webhookEvent.account_linking);
        } else {
            console.log("Webhook received unknown event: ", webhookEvent);
        }
    };
    if (common.isDefined(user.data.fbID) && user.data.fbID === senderID) { 
        // User is loaded - handle events as normal
        console.log("User '%s' is loaded.", senderID);
        //logging.logEvent(senderID, webhookEvent, function() {
            handleEvent();
        //});
    } else {    // User is not loaded
        getUser(senderID, function(result) {   // First try and load user
            if (common.isDefined(result)) { // Result returned
                user.reload(result);
                //logging.logEvent(senderID, webhookEvent, function() {
                    handleEvent();
                //});
            } else {    
                // User was not found in database.  We need to 
                create a new profile for the user
                common.getFBUser(senderID, function(name) {
                    console.log("User '%s' was not found in database.", senderID);
                    user.data.fbID = senderID;
                    user.data.firstName = name; // Stores user's first name 
                    in the user object
                    greetingText = strings.initialGreeting.replace("%USER", 
                    user.data.firstName);
                    delay = common.setDelay(greetingText);
                    messaging.sendTextMessage(senderID, greetingText);
                    setTimeout(function() {
                        // Pause before kicking off the 'uprofile' intent
                        dialogflow.sendRequest(session.getIDs(), senderID, 
                        strings.createUser);
                    }, delay);
                });
            }
        });
    }
}

I think the issue here is that I thought that by using an object within user.js, the chatbot would just create multiple instances of this object, one for each user. Clearly I'm mistaken. What would be the best way to create multiple instances and thus prevent a situation where the user data object is being shared by multiple users? Or should I just dispense with objects entirely and simply read from the database each time? I want to avoid doing this as it will require a lot of reads/writes.

1 Answers

OK, so I ended up using a Map().

My user.js file now contains the following:

loadedUsers: new Map()

When I want to create a new user (who doesn't exist in the database) I use the following object:

newUser: {
    firstName: undefined,
    userID: undefined,
    gender: undefined,
    age: undefined,
},

And when I want to reload an existing user, I use the following object:

user: {
    userID: undefined,
    gender: undefined,
    age: undefined,
},

I use add() and addNewUser() functions to create users in the map:

add: function(senderID, data) {
    self.loadedUsers.set(senderID, self.user);
    self.setProperty(senderID, 1, data.userid); /**@namespace data.userid */
    self.setProperty(senderID, 2, data.gender);
    self.setProperty(senderID, 3, data.age);
    console.log ("User '%s' added to map", senderID);
},

addNewUser: function(senderID, name) {
    self.loadedUsers.set(senderID, self.newUser);
    self.setProperty(senderID, 0, name);
    console.log("New User '%s' created", senderID);
},

And if I want to set a user property I use a setter function:

setProperty: function(senderID, property, value) {
    switch (property) {
        case 0: {
            // First Name
            self.loadedUsers.get(senderID).firstName = value;
            break;
        } case 1: {
            // UserID
            self.loadedUsers.get(senderID).userID = value;
            break;
        } case 2: {
            // Gender
            self.loadedUsers.get(senderID).gender = value;
            break;
        } case 3: {
            // Age
            self.loadedUsers.get(senderID).age = value
        }
    }
},

And I use a getter function to get a property:

getProperty: function(senderID, property) {
    switch (property) {
        case 0: {
            // First Name
            return self.loadedUsers.get(senderID).firstName;
        } case 1: {
            // UserID
            return self.loadedUsers.get(senderID).userID;
        } case 2: {
            // Gender
            return self.loadedUsers.get(senderID).gender;
        } case 3: {
            // Age
            return self.loadedUsers.get(senderID).age
        }
    }
},

Finally if I want to see if a user is loaded (before I try to read from database), I use this:

isLoaded(senderID) {
    return self.loadedUsers.has(senderID);
},
Related