poserver
Version:
Server for JD Bot
666 lines (580 loc) • 24.1 kB
JavaScript
/**
* Created by tomdaley on 9/18/16.
*/
/**
* U S E R P R O F I L E
*/
/**
* @namespace
* @property {object} results
* @property {object} results.response
* @property {number} results.response.index
* @property {string} results.response.entity
*/
var params = require("../../../poserver-configuration.json");
var prompts = require('./../const/prompts.js');
var statemaps = require('./../const/statemaps');
var Util = require('./../JdBotUtil');
var ParsedName = require('./../classes/clsParsedName');
var Person = require('./../classes/clsPerson');
var builder = require('botbuilder');
var consts = require('./../node_modules/botbuilder/lib/consts');
var authy = require('authy')(params.AUTHY_API_KEY);
const library = new builder.Library('userProfile');
library.dialog('/firstRun',
[
function (session)
{
Util.getUserProfile(session)
.then(function (isLoaded)
{
if (!isLoaded)
{
session.beginDialog("userProfile:/");
}
else
{
/** @param {Person} session.userData.userProfile */
var prompt = session.gettext(prompts.welcomeBackMessage, {
uname : session.userData.userProfile.name.salutation,
attorneyName: "Tom Daley"
});
session.send(prompt);
session.beginDialog("userProfile:/upgrade");
}
});
},
function (session)
{
session.replaceDialog("*:/");
}
]);
library.dialog('/upgrade',
[
function (session, args)
{
args = args || {};
var fromVersion = session.userData[consts.Data.FirstRunVersion];
var toVersion = (args.hasOwnProperty("toVersion") ? args.toVersion : 0);
//Only prompt if we obviously need to do an upgrade.
if (toVersion > fromVersion)
{
var prompt = session.gettext(prompts.sayProfileUpdateRequired, {"ov": fromVersion, "nv": toVersion});
session.send(prompt);
}
//Check for an upgrade anyway in order to fix a corrupted userprofile.
session.beginDialog('userProfile:/upgradeFields');
}
]);
library.dialog('/upgradeFields',
[
function (session)
{
//Collection of string fields to make sure we have
var requiredFields =
[
{name: "email", dialog: "userProfile:/Email"},
{name: "birthDate", dialog: "userProfile:/DOB"},
{name: "telephone", dialog: "userProfile:/Telephone"}
];
var field;
var fieldIdx;
var detectedMissingField = false;
//Search through the required fields and see if we find one that is missing.
for (fieldIdx in requiredFields)
{
field = requiredFields[fieldIdx];
if (!hasProfileField(session, field.name))
{
detectedMissingField = true;
break;
}
}
//If we found a missing field, prompt for it.
if (detectedMissingField)
{
session.beginDialog("userProfile:/");
}
//Finally, if all fields are present, then save the user's profile and head to the main dialog
else
{
Util.saveUserProfile(session.userData.userProfile, session)
.then(function (result)
{
if (result.hasOwnProperty("insertedId"))
{
session.userData.userProfile.users_id = result.insertedId;
}
session.endDialog(); //"##### profile saved");
})
.catch(function (reason)
{
console.error("userProfile:/upgradeFields: Error updating userProfile");
console.error(reason);
session.endDialog(); //"##### error saving profile");
});
}
}
]);
//Internal utility method to check for the presence of a non-null, non-blank field
function hasProfileField(session, fieldName)
{
return session.userData.userProfile.hasOwnProperty(fieldName) &&
session.userData.userProfile[fieldName] !== null &&
session.userData.userProfile[fieldName].trim().length > 0;
}
library.dialog('/',
[
function (session, args, next)
{
var hasAllFields = true;
var requiredFields =
[
{name: "name", dialog: "userProfile:/UserName"},
{name: "email", dialog: "userProfile:/Email"},
{name: "birthDate", dialog: "userProfile:/DOB"},
{name: "gender", dialog: "userProfile:/Gender"},
{name: "telephone", dialog: "userProfile:/Telephone"}
];
for (var p in requiredFields)
{
var field = requiredFields[p];
if (!hasProfileField(session, field.name))
{
session.beginDialog(field.dialog);
hasAllFields = false;
}
}
if (hasAllFields)
next({profileComplete: hasAllFields});
},
function (session, args)
{
if (args.profileComplete)
session.beginDialog("userProfile:/Verify");
else
session.replaceDialog("userProfile:/");
}
]);
function formatDate(dateString)
{
var myDate = new Date(dateString);
return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][myDate.getDay()] + ", " +
["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][myDate.getMonth()] + " " +
myDate.getDate() + ", " + myDate.getFullYear();
}
//Called from main menu when user wants to update his/her user profile. Returns to main menu.
library.dialog('/Verify',
[
function (session)
{
session.userData.state = "userProfile/Verify";
session.userData.stateMap = statemaps.userprofile;
var profile = session.gettext(prompts.userProfile, {
"uname" : session.userData.userProfile.name.fullNameWithTitle(),
"dob" : formatDate(session.userData.userProfile.birthDate),
"uemail": session.userData.userProfile.email,
"uphone": session.userData.userProfile.telephone,
"gender": prompts.gender[session.userData.userProfile.gender]
});
session.send(profile);
builder.Prompts.confirm(session, prompts.askConfirmProfile, session.userData.config.PromptChoiceOptions);
},
function (session, results)
{
if (results.response)
{
Util.saveUserProfile(session.userData.userProfile, session)
.then(function (result)
{
if (result.hasOwnProperty("insertedId"))
{
session.userData.userProfile.users_id = result.insertedId;
}
session.endDialog(prompts.sayUserProfileUpdateOk);
})
.catch(function (reason)
{
console.log("Error updating userProfile");
console.log(reason);
session.endDialog(prompts.sayUserProfileUpdateFail);
});
}
else
{
var choices = session.gettext(prompts.userProfileChoices, {
"uname" : session.userData.userProfile.name.fullNameWithTitle(),
"dob" : formatDate(session.userData.userProfile.birthDate),
"uemail": session.userData.userProfile.email,
"uphone": session.userData.userProfile.telephone,
"gender": prompts.gender[session.userData.userProfile.gender]
});
builder.Prompts.choice(session, prompts.askWhichToChange, choices, session.userData.config.PromptChoiceOptions);
}
},
function (session, results)
{
var choiceIdx = results.response.index;
switch (choiceIdx)
{
case 0:
session.beginDialog('userProfile:/UserName');
break;
case 1:
session.beginDialog('userProfile:/DOB');
break;
case 2:
session.beginDialog('userProfile:/Gender');
break;
case 3:
session.beginDialog('userProfile:/Email');
break;
case 4:
session.beginDialog('userProfile:/Telephone');
break;
case 5:
session.endDialog(prompts.sayLetsGetStarted);
//session.replaceDialog("*:/");
break;
}
},
function (session)
{
session.replaceDialog('userProfile:/Verify');
}
]);
library.dialog('/Email',
[
function (session)
{
builder.Prompts.text(session, prompts.askEmailAddress);
},
function (session, results)
{
if (!session.userData.hasOwnProperty("userProfile")) session.userData.userProfile = new Person({});
session.userData.userProfile.email = results.response;
session.endDialog();
}
]);
library.dialog('/DOB',
[
function (session)
{
builder.Prompts.time(session, prompts.askDateOfBirth);
},
function (session, results)
{
if (!session.userData.hasOwnProperty("userProfile")) session.userData.userProfile = new Person({});
session.userData.userProfile.birthDate = (builder.EntityRecognizer.resolveTime([results.response])).toJSON();
session.endDialog();
}
]);
library.dialog('/Gender',
[
function (session)
{
var prompt = prompts.askGender;
builder.Prompts.choice(session, prompt, prompts.gender, session.userData.config.PromptChoiceOptions);
},
function (session, results)
{
if (!session.userData.hasOwnProperty("userProfile")) session.userData.userProfile = new Person({});
session.userData.userProfile.gender = results.response.index;
session.endDialog();
}
]);
library.dialog('/Telephone',
[
//Prompt for a telephone number
function (session)
{
builder.Prompts.text(session, prompts.askPhoneNumber);
},
//Retrieve and verify the telephone number
function (session, results)
{
if (!session.userData.hasOwnProperty("userProfile")) session.userData.userProfile = new Person({});
session.userData.userProfile.telephone = results.response.replace(/[^0-9]/g, "");
// The /lookupPhone dialog will fill in the userProfile.cellPhone property if the telephone number
// provided is actually a cell phone.
session.beginDialog("userProfile:/LookupPhone");
},
//See if phone registration was successful
function (session, args)
{
if (args.success === false)
{
session.replaceDialog("userProfile:/Telephone");
}
else
{
session.endDialog();
}
}
]);
library.dialog('/UserName',
[
function (session)
{
builder.Prompts.text(session, prompts.askUserName);
},
function (session, results)
{
if (results.response.indexOf(" ") === -1)
{
session.send(prompts.sayINeedYourFullName);
session.replaceDialog("/UserName");
}
else
{
var parsedName = new ParsedName({});
if (!session.userData.hasOwnProperty("userProfile")) session.userData.userProfile = new Person({});
session.userData.userProfile.setName(parsedName.parseName(results.response, session.message.user.name));
session.endDialog();
}
}
]);
/**
* Determine whether phone is valid and what type of phone it is (cell, land).
*
* Cannot invoke /LookupPhone until userProfile has a telephone property.
*/
library.dialog('/LookupPhone',
[
function (session, args)
{
args = args || {};
//Error checking
if (!session.userData.userProfile.hasOwnProperty("telephone") && !args.hasOwnProperty("telephone"))
{
throw (new Error("userProfile must have a telephone property before /LookupPhone is invoked"));
}
var telephone = args.hasOwnProperty("telephone") ? args.telephone : session.userData.userProfile.telephone;
//Callback executed after server looks up the cellphone
var callback = function (err, res)
{
res = res || {success: false};
if (res.success)
{
if (res.type === "cellphone")
{
session.userData.userProfile.cellPhone = telephone;
session.replaceDialog('userProfile:/VerifyPhone', {
telephone: telephone,
type: "cellphone",
provider: res.provider
});
}
else
{
session.send(prompts.sayOnlyCellphonesSupported, {
phone: telephone,
type : res.type
});
res.success = false;
session.endDialogWithResult(res);
}
}
else
{
session.send(err.message);
session.endDialogWithResult(res);
}
};
authy.phones().info(telephone, '1', callback);
}
]);
/**
* Cannot invoke /RegisterPhone until userProfile has an email property and a cellPhone property.
*/
library.dialog('/RegisterPhone',
[
function (session, args)
{
args = args || {};
//Error checking
if (!session.userData.userProfile.hasOwnProperty("email"))
{
throw (new Error("userProfile must have an email property before /RegisterPhone is invoked"));
}
if (!session.userData.userProfile.hasOwnProperty("cellPhone"))
{
throw (new Error("userProfile myust have a cellPhone property before /RegisterPhone is invoked"));
}
/**
*
* @param err
* @param {object} res - Response from underlying Authy API
* @property {object} res.user - User object from Authy Server
* @property {string} res.user.id - UserId assigned by Authy Server. Save this in channelIdentities[]
*/
var callback = function (err, res)
{
if (err)
{
console.error("userProfile:/RegisterPhone: Error registering user.");
console.error(err);
console.error(res);
session.endDialogWithResult({success: false, response: res, error: err});
}
else
{
try
{
if (!session.userData.userProfile.hasOwnProperty("channelIdentities"))
session.userData.userProfile.channelIdentities = {};
var identity = {
"source" : "authy.jdbot",
"id" : res.user.id,
"name" : session.userData.userProfile.email,
"verified": new Date()
};
if (args.provider) identity.provider = args.provider;
if (args.type) identity.type = args.type;
//see if we have an existing channel identity from this source
//NOTE: We create and save channel identities as an array, but botbuilder's
//serialization converts it to an object having keys equal to the array indices.
//For example, we create [{source:a,id:a,name:a}, {source:b,id:b,name:b}] but
//the serialization converts it to {'0':{source:a,id:a,name:a},'1':{source:b,id:b,name:b}}.
var replacedChannelIdentity = false;
/** @property {number} session.userData.userProfile.channelIdentities.cidx */
for (var cidx in session.userData.userProfile.channelIdentities)
{
var channel = session.userData.userProfile.channelIdentities[cidx];
if (channel.source == identity.source)
{
session.userData.userProfile.channelIdentities[cidx] = identity;
replacedChannelIdentity = true;
break;
}
}
//If we didn't update an existing channel identity, then add a new new one to the end
//of the list.
if (!replacedChannelIdentity)
{
var newIdx = session.userData.userProfile.channelIdentities.length;
session.userData.userProfile.channelIdentities[newIdx] = identity;
}
session.endDialogWithResult({success: true});
}
catch (error)
{
console.error("userProfile:/RegisterPhone: Error saving channel identity.");
console.error(error.stack);
}
}
};
//Here to actually register the user
authy.register_user(session.userData.userProfile.email, session.userData.userProfile.cellPhone, callback);
}
]);
/**
* Must have a cellPhone property on the userProfile object before calling this dialog.
*/
library.dialog('/VerifyPhone',
[
//Send SMS Code to user
function (session, args)
{
args = args || {};
if (args.provider) session.dialogData.provider = args.provider;
if (args.type) session.dialogData.type = args.type;
//Error checking and identity lookup
if (!session.userData.userProfile.hasOwnProperty("cellPhone"))
{
throw (new Error("userProfile must have cellPhone property before /VerifyPhone is invoked. Did you invoke /LookupPhone already?"));
}
var callback = function (err, res)
{
res = res || {success: false};
if (res.success)
{
builder.Prompts.text(session, prompts.sayEnterSmsCode);
}
else
{
session.endDialogWithResult(res);
}
};
//Custom messages are not allowed at this time. It might be because my subscription level is too low and
//Authy wants more $$ per month in order to enable this feature. Fuckers.
authy.phones().verification_start(session.userData.userProfile.cellPhone, '1', /*{custom_message: "Here is your JDBOT verification code."},*/ callback);
},
//Retrieve SMS Code from user
function (session, results)
{
var token = results.response;
var callback = function (err, res)
{
res = res || {success: false};
if (err)
{
console.log(err);
session.endDialogWithResult({success: false, response: res, error: err});
}
else
{
session.send(prompts.sayVerificationCodeValid);
var props = {};
if (session.dialogData.provider) props.provider = session.dialogData.provider;
if (session.dialogData.type) props.type = session.dialogData.type;
session.replaceDialog('userProfile:/RegisterPhone', props);
}
};
authy.phones().verification_check(session.userData.userProfile.cellPhone, '1', token, callback);
}
]);
/**
* Authenticate User - basically the same as /VerifyPhone except the phone # to use comes from args rather than
* the user profile. I could have refactored /VerifyPhone but I am suffering from an allergy attack that prevents
* me from wanting to mess with it. :-(
*/
library.dialog('/AuthenticateUser',
[
//Send SMS Code to user
function (session, args)
{
args = args || {};
//Error checking and identity lookup
if (!args.hasOwnProperty("telephone"))
{
throw (new Error("Args object must have a 'telephone' property."));
}
var callback = function (err, res)
{
res = res || {success: false};
if (res.success)
{
builder.Prompts.text(session, prompts.sayEnterSmsCode);
}
else
{
session.endDialogWithResult(res);
}
};
//Custom messages are not allowed at this time. It might be because my subscription level is too low and
//Authy wants more $$ per month in order to enable this feature. Fuckers.
authy.phones().verification_start(args.telephone, '1', /*{custom_message: "Here is your JDBOT verification code."},*/ callback);
},
//Retrieve SMS Code from user
function (session, results)
{
var token = results.response;
var callback = function (err, res)
{
res = res || {success: false};
if (err)
{
console.log(err);
session.endDialogWithResult({success: false, response: res, error: err});
}
else
{
session.send(prompts.sayVerificationCodeValid);
session.endDialogWithResult({success: true});
}
};
authy.phones().verification_check(session.userData.userProfile.cellPhone, '1', token, callback);
}
]);
module.exports = library;