poserver
Version:
Server for JD Bot
1,166 lines (1,006 loc) • 48.2 kB
JavaScript
/**
* Created by tomdaley on 9/18/16.
*/
;
/**
* P R I V A T E U T I L I T I E S
*/
/**
* @namespace
* @property {object} err - Error thrown in code
* @property {object} err.stack - Stack trace
*/
var params = require("../../poserver-configuration.json");
var prompts = require('./const/prompts.js');
var mongoUtil = require('./classes/mongoUtils');
var Person = require('./classes/clsPerson');
var ParsedAddress = require('./classes/clsParsedAddress');
var ParsedName = require('./classes/clsParsedName');
var ObjectId = require('mongodb').ObjectId;
var https = require('https');
var bot;
var builder = require('botbuilder');
var violentVerbs = [{}];
exports.violentVerbs = violentVerbs;
module.exports =
{
setBot: function (abot)
{
bot = abot;
},
/**
* Try to get short, unique names for each of two people. Examples
*
* First Full Name Second Full Name Returns
* Thomas J. Daley Donald J. Trump Thomas, Donald
* Thomas J. Daley Thomas J. Trump Thomas Daley, Thomas Trump
* Richard J. Daley Richard M. Daley Richard J. Daley, Richard M. Daley
* John Doe John Doe the victim, the perpetrator
*
* @param {object} parsedName1 - Object having name-part properties, e.g. return value of ParsedName.parseName()
* @param {object} parsedName2 - Object having name-part properties, e.g. return value of ParsedName.parseName()
* @param {string=} default1 - Role name to assign to first person if the two names are the same (default=the perpetrator)
* @param {string=} default2 - Role name to assign to second person if the two names are the same (default=the victim)
* @returns {object} - .name1 and .name2 properties uniquely identify the two people
*/
getUniqueNames: function (parsedName1, parsedName2, default1, default2)
{
default1 = default1 || "the perpetrator";
default2 = default2 || "the victim";
var aName = parsedName1.firstName;
var vName = parsedName2.firstName;
if (aName == vName)
{
aName = aName + " " + parsedName1.lastName;
vName = vName + " " + parsedName2.lastName;
}
if (aName == vName)
{
aName = parsedName1.fullName;
vName = parsedName2.fullName;
}
if (aName == vName)
{
aName = default1;
vName = default2;
}
//The actorName and victimName properties are for backward compatibility with a prior version of this
//function.
return {actorName: aName, victimName: vName, name1: aName, name2: vName};
},
/**
* Try to load this user's profile from our database. Users can come to us from a number of channels, so look
* for the channel-specific identity for this user. See the MongoDb users table to see how this works.
*
* @param session
*/
getUserProfile: function (session) //, callback)
{
return new Promise(function (resolve, reject)
{
//var username = session.message.user.name;
var userid = session.message.user.id;
var idsource = session.message.address.channelId;
var query = {"channelIdentities": {$elemMatch: {"source": idsource, "id": userid}}};
var projection = {
"email": 1, "name": 1, "cellPhone": 1, "telephone": 1, "address": 1, "fax": 1,
"ids" : 1, "options": 1, "gender": 1, "birthDate": 1, "channelIdentities": 1,
"_id" : 1
};
var collection = mongoUtil.getDb().collection('users');
collection.find(query).project(projection).limit(1).toArray(function (err, docs)
{
if (err)
{
reject(err);
}
else
{
var isLoaded = false;
if (!err && docs.length > 0)
{
session.userData.userProfile = new Person(docs[0]);
isLoaded = true;
}
if (!session.userData.hasOwnProperty("config")) session.userData.config = {};
resolve(isLoaded);
}
});
});
},
/**
* Add the protective order information to our backend queue for document generation or attorney review.
*
* @param session
*/
queueProtectiveOrder: function (session)
{
var queuedItem = {};
queuedItem.usState = "TX";
queuedItem.form = "FAMVIOPO";
queuedItem.formData = {};
queuedItem.formData.victim = session.userData.victim;
queuedItem.formData.actor = session.userData.actor;
queuedItem.formData.violentActs = session.userData.violentActs;
this.queueItem(session, queuedItem);
},
/**
* Queue an item for later processing, for example by form generation software or placing a phone call, etc.
*
* @param session
* @param item
*
* @return {Promise}
*/
queueItem: function (session, item)
{
//Associate this item with the user who created it.
item.users_id = new ObjectId(session.userData.userProfile.users_id);
//Remember what time we queued this item.
item.queueTime = (new Date()).toJSON();
item.status = "Q"; //Mark item as queued and ready for merging.
if (!item.hasOwnProperty("paid")) item.paid = "N"; //Indicate that this item has not been paid for yet.
//Now to actually insert the item in our processing queue, which for now is a MongoDB collection
return mongoUtil.getDb().collection('queue').insertOne(item);
},
/**
*
* @param {string} id - ObjectId (as string) of queued item to update
* @param {string} flagValue - Y or N to indicate whether item is paid. Default = Y.
*/
setQueuedItemPaymentFlag: function (id, flagValue)
{
flagValue = flagValue || 'Y';
var filter = {_id: new ObjectId(id)};
var update = {$set: {paid: flagValue}};
var options = {};
//No callback or promise because I don't care when this happens.
mongoUtil.getDb().collection('queue').updateOne(filter, update, options)
.catch(function (reason)
{
console.error("JDBotUtil.setQueuedItemPaymentFlag(): Error setting payment flag.");
console.error(reason);
console.error(filter);
console.error(update);
});
},
saveOriginalAnswer: function (session)
{
//First save any new data we captured relating to the user's profile.
this.saveUserProfile(session.userData.userProfile, session)
.then(function (result)
{
//console.log("********** saveOriginalAnswer()");
//console.log(result);
})
.catch(function (reason)
{
console.error("JDBotUtil.saveOriginalAnswer - Error updating user profile");
console.error(reason);
});
//Create the item to queue for document generation
var item = {};
item.usState = session.userData.jurisdiction.state; //"TX";
item.countryCode = session.userData.jurisdiction.country; //"US";
item.form = "ANSWER";
item.formData = session.userData.case;
if (session.userData.case.hasOwnProperty("paid")) item.paid = session.userData.case.paid;
//Save the case information to the cases collection
var aCase = {};
aCase.state = item.usState;
aCase.county = session.userData.case.county;
aCase.country = item.countryCode;
aCase.causeNumber = session.userData.case.causeNumber;
aCase.courtNumber = session.userData.case.courtNumber;
aCase.courtType = session.userData.case.courtType;
aCase.matterTypeDescription = session.userData.case.matterTypeDescription;
aCase.matterType = session.userData.case.matterType;
aCase.petitioner = {};
var pName = new ParsedName(session.userData.case.petitioner.name);
aCase.petitioner.name = pName;
aCase.petitioner.fullName = pName.fullName();
aCase.respondent = {};
pName = new ParsedName(session.userData.userProfile.name);
aCase.respondent.name = pName;
aCase.respondent.fullName = pName.fullName();
aCase.respondent.users_id = new ObjectId(session.userData.userProfile.users_id);
if (session.userData.case.hasOwnProperty("children"))
aCase.children = session.userData.case.children;
var query = {
"state" : aCase.state,
"county" : aCase.county,
"causeNumber": aCase.causeNumber,
"country" : aCase.country
};
var options = {"upsert": true};
mongoUtil.getDb().collection('cases').updateOne(query, aCase, options)
.then(function (document)
{
console.info("JDBotUtil.saveOriginalAnswer(): case saved");
})
.catch(function (reason)
{
console.error("JDBotUtil.saveOriginalAnswer(): Failed to save case data.");
console.error(reason);
console.error(aCase);
});
//Save some current state data, if configured to do so. This is just for debugging.
if (params.saveUserData)
{
console.info("JDBotUtil.saveOriginalAnswer(): Persisting session.userData");
var persistedData = {};
var keys = Object.keys(session.userData);
var newKey;
//Don't try to save the _id property, if there is one. Replace any dots in key names with dashes.
//Note that I don't iterate through every key in every child object because if there are dots in keys,
//I expect the originate from the BotBuilder API and would be saved at a top-level property of the
//userData object.
for (var k in keys)
{
if (keys[k] !== "_id")
{
newKey = keys[k].replace(/\./g, "-").replace(/["']/g, "");
persistedData[newKey] = session.userData[keys[k]];
}
}
mongoUtil.getDb().collection('userData').insertOne(persistedData)
.then(function (result)
{
console.info("JDBotUtil.saveOriginalAnswer(): Successfully persisted session.userData.");
})
.catch(function (reason)
{
console.error("JDBotUtil.saveOriginalAnswer(): Error persisting session.userData");
console.error(reason);
console.error(persistedData);
});
}
//Now queue the form for generation and review
return this.queueItem(session, item);
},
/**
* No callback or promise for now because I think this will complete before the user can answer the next
* series of questions.
*
* @param {Person} user - Person object containing channelIds to be saved
*/
saveIds: function (user)
{
var filter = {_id: new ObjectId(user.users_id)};
var update = {$set: {ids: user.ids}};
var options = {};
mongoUtil.getDb().collection('users').updateOne(filter, update, options)
.then(function (document)
{
console.info("JDBotUtil.saveIds(): Successfully saved ids");
})
.catch(function (reason)
{
console.error("JDBotUtil.saveIds(): Error saving ids");
console.error(reason);
console.error(filter);
console.error(update);
});
},
/**
* Used to remove an id associated with a user. For example, if we find that the user's Stripe id is no longer
* valid, we would remove the "stripe" id from the ids property so we wouldn't try that id again.
*
* @param {string} users_id - _id of users record to update
* @param {string} idName - name of users.ids property to delete
*/
deleteId: function (users_id, idName)
{
var filter = {_id: new ObjectId(users_id)};
var projection = {_id: 1, ids: 1};
var options = {};
mongoUtil.getDb().collection('users').find(filter, projection, options).toArray()
.then(function (docs)
{
if (docs.length === 1)
{
var ids = docs[0].ids;
delete ids[idName];
var filter = {_id: new ObjectId(users_id)};
var update = {$set: {ids: ids}};
var options = {};
mongoUtil.getDb().collection('users').updateOne(filter, update, options)
.then(function (document)
{
//
})
.catch(function (reason)
{
console.error("JDBotUtil.deleteId(): Error saving revised id collection");
console.error(reason);
console.error(filter);
console.error(update);
});
}
}
)
.catch(function (reason)
{
console.error("JDBotUtil.deleteId(): Error deleting %s from ids for %s", idName, users_id);
console.error(reason);
});
},
/**
* Save the user's profile along with the identify information we collect from this channel.
*
* @param {Person} user - Description of person to be persisted
* @param {object} session - Microsoft Botbuilder session object
* @return {Promise}
*/
saveUserProfile: function (user, session)
{
var oPerson = new Person();
//If we don't already have a channel identity, then create a blank object
if (!user.hasOwnProperty("channelIdentities"))
{
user.channelIdentities = [];
}
else
{
//Compensate for the fact that the botbuilder wants to serialize our arrays as higher level objects.
user.channelIdentities = oPerson.convertObjectToArray(user.channelIdentities);
}
//See if we already have this channel in our channelIdentities array
var foundChannelIdentity = false;
for (var cidx in user.channelIdentities)
{
var channelIdentity = user.channelIdentities[cidx];
if (channelIdentity.source == session.message.address.channelId)
{
foundChannelIdentity = true;
break;
}
}
//If we don't already have this channel identity, save it to the channelIdentities array.
if (!foundChannelIdentity)
{
channelIdentity = {
"source": session.message.address.channelId,
"id" : session.message.user.id, "name": session.message.user.name
};
user.channelIdentities.push(channelIdentity);
}
//Make sure the user's options are stored as an array
if (user.hasOwnProperty("options"))
user.options = oPerson.convertObjectToArray(user.options);
//Existing users will have a users_id property on their profile, which is an index into the users collection.
if (user.hasOwnProperty("users_id"))
{
var update = {};
var keys = Object.keys(user);
for (var k = 0; k < keys.length; k++)
{
if (keys[k] !== "channelIdentities")
update[keys[k]] = user[keys[k]];
else
update["channelIdentities"] = (new Person()).convertObjectToArray(user.channelIdentities);
}
//Don't try to update an immutable field.
if (update.hasOwnProperty("_id")) delete update._id;
//Don't add a redundant field
delete update.users_id;
update = {$set: update};
var filter = {_id: new ObjectId(user.users_id)};
var options = {};
console.info("JDBotUtil.saveUserProfile(): Updating `users` record for %s (%s)", user.email, user.users_id);
return mongoUtil.getDb().collection('users').updateOne(filter, update, options);
}
else
{
return mongoUtil.getDb().collection('users').insertOne(user);
}
},
/**
* Load a vocabulary of violent acts, including verb tenses and indicators about whether we should ask for
* more details, i.e. the use of a weapon. This is done when the server first starts up and is not refreshed
* until the server restarts.
*/
loadViolenceDatabase: function ()
{
var collection = mongoUtil.getDb().collection('verbs');
collection.find({}).forEach(function (verbEntry)
{
/**
* @namespace
* @property {object} verbEntry - Entry from MonggoDb verbs collection
* @property {string} verbEntry.verb - present-tense verb ("slap")
* @property {string} verbEntry.pastTense - past-tense form of verb ("slapped")
* @property {string} verbEntry.progressiveTense - progressive-tense form of verb ("slapping")
*/
violentVerbs[verbEntry.verb] = verbEntry;
violentVerbs[verbEntry.pastTense] = verbEntry;
violentVerbs[verbEntry.progressiveTense] = verbEntry;
}, function (err)
{
if (err)
{
console.error("JDBotUtil.loadViolenceDatabase(): Error loading verbs.");
console.error(err.stack);
}
});
},
/**
* See whether the utterance from the user may have been intended as a stop word, such as CANCEL, GO BACK, etc.
*
* @param testPhrase
* @returns {*} - Normalized stop word, if found, otherwise null. Stopwords are defined in prompts.js
*/
getStopWord: function (testPhrase)
{
if (typeof testPhrase !== "string") return null;
var stopWords = Object.keys(prompts.stopWords);
for (var i = 0; i < stopWords.length; i++)
{
var patterns = prompts.stopWords[stopWords[i]].patterns;
for (var p = 0; p < patterns.length; p++)
{
if (testPhrase.match(patterns[p]))
return stopWords[i];
}
}
return null;
},
/**
* Look at the current state map, the current state, and the existence of a stop word to determine which
* dialog the app should display next. Context-sensitive help is a major to-do item.
*
* The statemaps are defined in statemap.js and the user's current statemap is copied to his or her
* userData.
*
* @param session
* @param results - If an empty object (or any object not having a response member) will return the NEXT dialog
* @returns {string} - Path of next dialog to call, whether moving forward, quitting, or moving backward
*/
getNextDialog: function (session, results)
{
var stateMap = session.userData.stateMap;
var thisState = stateMap[session.userData.state];
var nextDialog;
if (!results.hasOwnProperty("response")) return thisState.next;
var stopWord = this.getStopWord(results.response);
switch (stopWord)
{
//Allow branching based on response to a "choice" prompt.
case null:
if (results.response.hasOwnProperty("index"))
{
var idx = results.response.index;
if (thisState.hasOwnProperty("choice" + idx))
{
return thisState["choice" + idx];
}
}
nextDialog = thisState.next;
break;
case "NEXT":
nextDialog = thisState.next;
break;
case "GOBACK":
nextDialog = thisState.prev;
break;
case "CANCEL":
nextDialog = thisState.cancel;
break;
case "HELP":
session.send("Help is on the way . . .");
nextDialog = thisState.next;
break;
default:
console.log("JDBotUtil.getNextDialog(): Bad stop word at state %s = %s", session.userData.state, stopWord);
nextDialog = thisState.next;
}
return nextDialog;
},
/**
* Clean up the userData portion of the session. This mostly helps keep the logs clean. The slow accumulation of
* userData is probably not helpful, but frankly I haven't seen any problems. I just want cleaner logs.
*
* @param session
*/
cleanupSession: function (session)
{
delete session.userData.actor;
delete session.userData.card;
delete session.userData.case;
delete session.userData.feedback;
delete session.userData.identityPrompt;
delete session.userData.paymentAmount;
delete session.userData.selectedService;
delete session.userData.state;
delete session.userData.stateMap;
delete session.userData.victim;
delete session.userData.violentActs;
delete session.userData.violentActIndex;
},
makeOrdinal: function (aString)
{
//remove all non-numeric characters from the string
var numericString = aString.replace(/[^0-9]/, "");
//If the string only had non-numeric characters, just return back whatever we originally received.
if (numericString.length === 0) return aString;
//List of ordinal suffixes the index of which will be the last numeric digit of the court number
var ordinalSuffixes = ["th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"];
var index = numericString.slice(-1, -1) * 1; //coerce into a number
return numericString + ordinalSuffixes[index];
},
/**
*
* @param street
* @param csz
* @param session
* @param callback
*
* @namespace
* @property {string} status - "OK" means we have usable results
* @property {[object]} results - All the good stuff we want
* @property {[{}]} results[].address_components - Each field of the address
* @property {[string]} results[].address_components.types - List of address field types in this component
* @property {string} results[].address_components.long_name - Full/Formal value for this field
* @property {string} results[].address_components.short_name - Abbreviate value for this field
* @property {string} results[].formatted_address - Full address with all fields properly formatted
* @property {boolean} results[].partial_match - True or False (not sure what that means all the time)
* @property {string} results[].place_id - Google-specific place id. Not sure what this is used for.
* @property {[{}]} results[].geometry - Lat/Long information about location and dimension of location
* @property {object} results[].geometry.location
* @property {number} results[].geometry.location.lat - Latitude of address
* @property {number} results[].geometry.location.lng - Longitude of address
* @property {string} results[].geometry.location_type - I always see the value "ROOFTOP"
* @property {object} results[].geometry.viewport - Coordinates of northeast and southwest points (corners) of address
* @property {object} results[].geometry.viewport.northeast - Northeast coordinate of address
* @property {number} results[].geometry.viewport.northeast.lat - Latitude of northeast coordinate
* @property {number} results[].geometry.viewport.northeast.lng - Longitude of northeast coordinate
* @property {object} results[].geometry.viewport.southwest - Southwest coordinate of address
* @property {number} results[].geometry.viewport.southwest.lat - Latitude of southwest coordinate
* @property {number} results[].geometry.viewport.southwest.lng - Longitude of southwest coordinate
* @property {[{string]} results[].types - Array of strings. "subpremise" and "street_address" mean we got a full address
*
* @namespace
* @property {[string]} types
* @property {string} long_name
* @property {string} short_name
* @property {string} formatted_address
* @property {[{}]} address_components
*/
geoCodeAddress: function (street, csz, session, callback)
{
var myUrl = "https://maps.googleapis.com/maps/api/geocode/json?address=%(address)s&key=%(apikey)s";
var searchAddress = (street + "," + csz).replace(/\s/g, '+');
var query = session.gettext(myUrl, {address: searchAddress, apikey: params.GOOGLE_API_KEY});
https.get(query, function (res)
{
var body = '';
res.on('data', function (data)
{
body += data;
});
res.on('end', function ()
{
var parsed = JSON.parse(body);
var result = {};
result.status = parsed.status;
//If not OK, then we have nothing to send back
if (parsed.status != "OK")
{
console.error("JDBotUtil.geoCodeAddress(): Error parsing address.");
console.error(parsed);
callback(session, result);
return;
}
//Go through each address component and assign to a field in our result
for (var i = 0; i < parsed.results[0].address_components.length; i++)
{
var component = parsed.results[0].address_components[i];
switch (true)
{
case (component.types.indexOf("administrative_area_level_1") !== -1):
result.state = component.short_name;
break;
case (component.types.indexOf("administrative_area_level_2") !== -1):
result.county = component.long_name.replace(/\sCounty/i, "");
break;
case (component.types.indexOf("locality") !== -1):
result.city = component.long_name;
break;
case (component.types.indexOf("neighborhood") !== -1):
result.neighborhood = component.long_name;
break;
case (component.types.indexOf("route") !== -1):
result.street = component.long_name;
break;
case (component.types.indexOf("street_number") !== -1):
result.streetNumber = component.long_name;
break;
case (component.types.indexOf("subpremise") !== -1):
result.unit = component.long_name;
break;
case (component.types.indexOf("postal_code") !== -1):
result.postalCode = component.long_name;
break;
case (component.types.indexOf("postal_code_suffix") !== -1):
result.postalCodeSuffix = component.long_name;
break;
}
}
//An address in an unincorporated area may not have a city name
if (!result.hasOwnProperty("city") && result.hasOwnProperty("neighborhood"))
result.city = result.neighborhood;
if (parsed.results[0].hasOwnProperty("formatted_address"))
result.fullAddress = parsed.results[0].formatted_address;
var street1 = "";
if (result.hasOwnProperty("streetNumber")) street1 += result.streetNumber;
if (result.hasOwnProperty("street")) street1 += " " + result.street;
if (result.hasOwnProperty("unit")) street1 += " #" + result.unit;
result.street1 = street1.replace(/\s+/g, " ").trim();
var csz = "";
if (result.hasOwnProperty("city")) csz += result.city;
if (result.hasOwnProperty("city") && result.hasOwnProperty("state")) csz += ", ";
if (result.hasOwnProperty("state")) csz += result.state;
if (result.hasOwnProperty("postalCode")) csz += " " + result.postalCode;
if (result.hasOwnProperty("postalCode") && result.hasOwnProperty("postalCodeSuffix")) csz += "-" + result.postalCodeSuffix;
result.csz = csz;
//set quality flags
result.hasCompleteStreet = (result.hasOwnProperty("streetNumber") &&
result.hasOwnProperty("street"));
result.hasCompleteCsz = (result.hasOwnProperty("city") && result.hasOwnProperty("state") &&
result.hasOwnProperty("postalCode"));
callback(session, result);
})
});
},
lookupCounty: function (country, state, county)
{
state = state || "TX";
country = country || "US";
var result = {"userString": county};
var myUrl = "https://maps.googleapis.com/maps/api/geocode/json?address=%(address)s&key=%(apikey)s";
var searchAddress = county.replace(/county/i, "").replace(/\b/g, "+") + "+COUNTY,+" + state + ",+" + country;
myUrl = myUrl.replace(/%\(address\)s/, searchAddress);
myUrl = myUrl.replace(/%\(apikey\)s/, params.GOOGLE_API_KEY);
return new Promise(function (resolve, reject)
{
https.get(myUrl, function (res)
{
var body = '';
res.on('data', function (data)
{
body += data;
});
res.on('end', function ()
{
var parsed = JSON.parse(body);
result.status = parsed.status;
if (parsed.status != "OK")
{
reject(result);
return;
}
//Commented out the loop because for now, I just want to process the first potential match.
//for (var a = 0; a < parsed.results.length; a++)
{
var a = 0;
var address = {};
//Go through each address component and assign to a field in our result
for (var i = 0; i < parsed.results[a].address_components.length; i++)
{
var component = parsed.results[a].address_components[i];
switch (true)
{
case (component.types.indexOf("administrative_area_level_1") !== -1):
address.state = component.short_name;
break;
case (component.types.indexOf("administrative_area_level_2") !== -1):
address.county = component.long_name.replace(/\sCounty/i, "");
break;
case (component.types.indexOf("country") !== -1):
address.countryCode = component.short_name;
address.country = component.long_name;
break;
}
}
address.formatted_address = parsed.results[a].formatted_address;
address.partialMatch = parsed.results[a].hasOwnProperty("partial_match");
}
result.address = address;
resolve(result);
});
});
});
},
gparseAddress: function (session, address, callback)
{
var myUrl = "https://maps.googleapis.com/maps/api/geocode/json?address=%(address)s&key=%(apikey)s";
var searchAddress = address.replace(/\s/g, '+');
var query = session.gettext(myUrl, {address: searchAddress, apikey: params.GOOGLE_API_KEY});
var result = {"userString": address.replace(/\s+/g, " ").trim()};
var addresses = [];
https.get(query, function (res)
{
var body = '';
res.on('data', function (data)
{
body += data;
});
res.on('end', function ()
{
var parsed = JSON.parse(body);
result.status = parsed.status;
//If not OK, then we have nothing to send back
if (parsed.status != "OK")
{
console.log("JDBotUtil.gparseAddress(): Error parsing address.");
console.log(parsed);
callback(session, result);
return;
}
for (var a = 0; a < parsed.results.length; a++)
{
var address = {};
//Go through each address component and assign to a field in our result
for (var i = 0; i < parsed.results[a].address_components.length; i++)
{
var component = parsed.results[a].address_components[i];
switch (true)
{
case (component.types.indexOf("administrative_area_level_1") !== -1):
address.state = component.short_name;
break;
case (component.types.indexOf("administrative_area_level_2") !== -1):
address.county = component.long_name.replace(/\sCounty/i, "");
break;
case (component.types.indexOf("locality") !== -1):
address.city = component.long_name;
break;
case (component.types.indexOf("neighborhood") !== -1):
address.neighborhood = component.long_name;
break;
case (component.types.indexOf("route") !== -1):
address.street = component.long_name;
break;
case (component.types.indexOf("street_number") !== -1):
address.streetNumber = component.long_name;
break;
case (component.types.indexOf("subpremise") !== -1):
address.unit = component.long_name;
break;
case (component.types.indexOf("country") !== -1):
address.countryCode = component.short_name;
address.country = component.long_name;
break;
case (component.types.indexOf("postal_code") !== -1):
address.postalCode = component.long_name;
break;
case (component.types.indexOf("postal_code_suffix") !== -1):
address.postalCodeSuffix = component.long_name;
break;
}
}
//An address in an unincorporated area may not have a city name
if (!address.hasOwnProperty("city") && address.hasOwnProperty("neighborhood"))
address.city = address.neighborhood;
if (parsed.results[a].hasOwnProperty("formatted_address"))
address.fullAddress = parsed.results[a].formatted_address;
var street1 = "";
if (address.hasOwnProperty("streetNumber")) street1 += address.streetNumber;
if (address.hasOwnProperty("street")) street1 += " " + address.street;
if (address.hasOwnProperty("unit")) street1 += " #" + address.unit;
address.street1 = street1.replace(/\s+/g, " ").trim();
var csz = "";
if (address.hasOwnProperty("city")) csz += address.city;
if (address.hasOwnProperty("city") && address.hasOwnProperty("state")) csz += ", ";
if (address.hasOwnProperty("state")) csz += address.state;
if (address.hasOwnProperty("postalCode")) csz += " " + address.postalCode;
if (address.hasOwnProperty("postalCode") && address.hasOwnProperty("postalCodeSuffix")) csz += "-" + address.postalCodeSuffix;
address.csz = csz;
//set quality flags
address.hasCompleteStreet = (address.hasOwnProperty("streetNumber") &&
address.hasOwnProperty("street"));
address.hasCompleteCsz = (address.hasOwnProperty("city") && address.hasOwnProperty("state") &&
address.hasOwnProperty("postalCode"));
addresses.push(address);
}
result.addresses = addresses;
callback(session, result);
})
});
},
getCase: function (causeNumber, county, state, country)
{
if (!causeNumber) throw "jdBotUtil.getCase(): causeNumber is required.";
if (!county) throw "jdBotUtil.getCase(): county is required";
state = state || "TX";
country = country || "USA";
return new Promise(function (resolve, reject)
{
var collection = mongoUtil.getDb().collection('cases');
var query = {country: country, causeNumber: causeNumber, county: county, state: state};
collection.find(query).limit(1).toArray()
.then(function (result)
{
resolve(result);
})
.catch(function (reason)
{
console.error("JdBotUtil.getCase(): Error retrieving case.");
console.error(reason);
reject(reason);
});
});
},
/**
* Take a matterType abbreviation, e.g. DIVC, DIVN, and return a description of that type of case.
*
* @param {string} matterType - Matter Type abbreviation, probably from the cases collection
* @returns {*}
*/
getMatterTypeDescription: function (matterType)
{
for (var cidx in prompts.causes.all)
{
var coa = prompts.causes.all[cidx];
if (coa.id == matterType)
{
coa.description = cidx;
return coa;
}
}
return {};
},
/**
* Try to determine the type of court and the court number or name based on the cause number. Each county formats
* its cause numbers differently. Most seem to be "intelligent keys" in that they contain at least the court
* number and often the court type. Some, like DALLAS COUNTY, make it easy to figure out the court TYPE, but leave
* the court NUMBER obscured.
*
* @param {string} causeNumber - The cause number from the pleadings or citation. REQUIRED.
* @param {string} county - The county in which the case was filed. REQUIRED, but defaults to "COLLIN"
* @param {string=} state - The two-character state code (e.g. AR, CA, TX, NY). Defaults to "TX"
* @param {string=} country - The two-character country code (e.g. US, UK, DE). Defaults to "US"
*
* The resolve method produces an object that has these properties:
*
* type: False means we could not find any applicable rules OTHERWISE the type of court, e.g. "District Court"
* courtNumber: False means we could not determine the court number OTHERWISE the court number, e.g. 470
* county: The county we searched for
* state: The state we searched for
* country: The country we searched for
*
* @returns {Promise}
*/
recognizeCourt: function (causeNumber, county, state, country)
{
var myCounty = (county || 'COLLIN').toUpperCase();
var myState = (state || 'TX').toUpperCase();
var myCountry = (country || 'US').toUpperCase();
var result = {
"type" : false,
"courtNumber": false,
"county" : myCounty,
"state" : myState,
"country" : myCountry,
"ruleCount" : 0,
"causeNumber": causeNumber
};
var ruleIndex = myCountry + ":" + myState + ":" + myCounty;
var query = {"county": ruleIndex};
var projection = {"rules": 1};
var collection = mongoUtil.getDb().collection('courtRecognitionRules');
var processRules = function (docs)
{
try
{
//If we don't have any rules for this political geography, exit with a blank result
if (docs.length === 0)
{
result.message = "Docs length was zero - no rules found.";
return result;
}
var rules = docs[0].rules;
result.ruleCount = rules.length;
//Go through the recognition rules
var i;
var courtNumber = false;
var foundMatch = false;
for (i = 0; i < rules.length && foundMatch === false; i++)
{
//Check for a match
/** @property {RegExp} pattern - Regex that if matched fires this rule */
var matches = causeNumber.match(rules[i].pattern);
//If we have a match . . .
if (matches !== null)
{
foundMatch = true;
//Dereference the rule to make it easier to work with.
var rule = rules[i];
//See if we got a capture result. At this time, the only capture group in the patterns is for the
//court number.
if (matches.length >= 2)
{
//We captured the court number. See if it needs to be translated.
//E.G. in Dallas County, District Court #256 is designated as "Z" rather than "256" in the cause number
/** @property {[]} translations - maps cause number segment to actual court number */
if (rule.hasOwnProperty("translations"))
{
//Translate the court number. If we got to here, it means we HAD to translate the court number.
//Either return the translated court number OR false, which indicates that required translation failed.
//Returning this false value signals to the invoking dialog the need to ask the human to try to
//find a court number.
courtNumber = rule.translations.hasOwnProperty(matches[1]) ? rule.translations[matches[1]] : false;
}
else
{
courtNumber = matches[1];
}
}
result.type = rule.type;
result.courtNumber = courtNumber;
break;
}
}
return result;
}
catch (err)
{
console.error("JDBotUtil.recognizeCourt(): Error recognizing court [1] ('%s', '%s', '%s', '%s'",
causeNumber, myCounty, myState, myCountry);
console.error(err.stack);
}
};
return new Promise((resolve, reject) =>
{
collection.find(query).project(projection).limit(1).toArray((err, docs) =>
{
if (err)
{
console.error("JDBotUtil.recognizeCourt(): Error recognizing court [2] ('%s', '%s', '%s', '%s'",
causeNumber, myCounty, myState, myCountry);
console.error(err);
reject(result);
}
else
{
resolve(processRules(docs));
}
});
});
},
getRandomElement(arr)
{
return arr[Math.floor(Math.random() * arr.length)];
},
/**
* Implements the Luhn algorighm for validating a number based on it's ending check-digit. Works for
* credit card numbers, US Social Security Numbers, etc. Rf. https://en.wikipedia.org/wiki/Luhn_algorithm
*
* @param value
* @returns {boolean}
*/
validateCheckDigit: function (value)
{
//Reject input containing other than digits, space, and dash
if (!value.match(/^[0-9-\s]+$/)) return false;
//Luhn[2] Algorightm from https://gist.github.com/DiegoSalazar/4075533
//TJD: I cleaned this up just a little.
var nCheck = 0;
var nDigit = 0;
var cDigit = 0;
var bEven = false;
value = value.replace(/\D/g, "");
for (var n = value.length - 1; n >= 0; n--)
{
cDigit = value.charAt(n);
nDigit = parseInt(cDigit, 10);
if (bEven)
{
if ((nDigit *= 2) > 9) nDigit -= 9;
}
nCheck += nDigit;
bEven = !bEven;
}
return (nCheck % 10) == 0;
}
};