Newer
Older
Johannes Goderbauer
committed
import("system.entities");
Johannes Goderbauer
committed
import("system.datetime");
import("system.util");
import("system.notification");
import("system.logging");
import("KeywordRegistry_basic");
import("system.tools");
import("system.db");
import("Sql_lib");
import("system.cti");
Johannes Goderbauer
committed
import("system.indexsearch");
Johannes Goderbauer
committed
Johannes Goderbauer
committed
/**
* object for processing cti-calls
* Within the constructor, data is collected but not further processed. To perform operations, different methods are provide (lke the .execute-function)
*
* @class
* @param {Object} pCallData object with all the basic call information, the object needs the following parameters: <ul>
* <li>action:for example: vars.get("$local.action") </li>
* <li>callId:uid of the call, requried, for example: vars.get("$local.callID") </li>
* <li>localAddress:the target phone address, for example: vars.get("$local.localAddress") </li>
* <li>localId:id of the call target, for example: vars.get("$local.localID") </li>
* <li>callAddress:the source phone address, for example: vars.get("$local.callAddress") </li>
* <li>isIncomingCall:Boolean:is the call an incoming or outgoing call, for example: vars.getString("$local.callIsIncoming") == "true" </li>
* <li>state:Number:callstate-constant (Ringing, talking, etc.), for example: Number(vars.get("$local.callState")) </li>
* <li>privateData:Object: additional telephony data when the tapi does provide it, for example: vars.exists("$local.privateData2") ? vars.get("$local.privateData2") : null </li>
* <li>isConnectedCall:Boolean:specifies if the call was redirected ("connected") from somebody else</li>
* </ul>
*
* @example
*
* var ic = new IncomingCallExecutor(callData);
* //most of the logic happens in the handler-fuctions
* ic.setHandlerRinging(ringingHandlerFn);
* ic.setHandlerTalking(talkingHandlerFn);
* ic.setHandlerDisconnect(disconnectingHandlerFn);
* ic.execute();
*/
/**
* object for processing cti-calls
* Within the constructor, data is collected but not further processed. To perform operations, different methods are provided.
*
* @class
* @param {Object} pCallData object with all the basic call information, the object needs the following parameters: <ul>
* <li>action:for example: vars.get("$local.action") </li>
* <li>callId:uid of the call, requried, for example: vars.get("$local.callID") </li>
* <li>localAddress:the target phone address, for example: vars.get("$local.localAddress") </li>
* <li>localId:id of the call target, for example: vars.get("$local.localID") </li>
* <li>callAddress:the source phone address, for example: vars.get("$local.callAddress") </li>
* <li>isIncomingCall:Boolean:is the call an incoming or outgoing call, for example: vars.getString("$local.callIsIncoming") == "true" </li>
* <li>state:Number:callstate-constant (Ringing, talking, etc.), for example: Number(vars.get("$local.callState")) </li>
* <li>privateData:Object: additional telephony data when the tapi does provide it, for example: vars.exists("$local.privateData2") ? vars.get("$local.privateData2") : null </li>
* <li>isConnectedCall:Boolean:specifies if the call was redirected ("connected") from somebody else</li>
* </ul>
*
* @example
*
* var ic = new IncomingCallExecutor(callData);
* //most of the logic happens in the handler-fuctions
* ic.setHandlerRinging(ringingHandlerFn);
* ic.setHandlerTalking(talkingHandlerFn);
* ic.setHandlerDisconnect(disconnectingHandlerFn);
* ic.execute();
*/
Johannes Goderbauer
committed
function IncomingCallExecutor(pCallData)
{
this.callData = pCallData;
this.processPrivateData();
//has no other use than better readability when logging the object:
this.callData.stateName = IncomingCallExecutor._callstateToText(this.callData.state);
//collect contact and user data from *US*
this.contactsLocal = null;
this.usersLocal = null;
this.collectDataFromLocalInfo();
Johannes Goderbauer
committed
//collect contact and user data from *THEM*
this.contactsCall = null;
this.usersCall = null;
this.collectDataFromCallInfo();
Johannes Goderbauer
committed
//key-value pairs of callstates-functions
Johannes Goderbauer
committed
this._handlerFunctions = {};
this.notificationContentId = null;
Johannes Goderbauer
committed
}
/**
* transform the whole object into string representation, good for debugging and logging purposes
* This will list the own properties of the object and it's values. will not resolve functions (so no callback-functions are resolved)
*
* @return {String} stringified object-instance
*/
Johannes Goderbauer
committed
IncomingCallExecutor.prototype.toString = function()
{
return JSON.stringify(this, null, " ");
}
/**
* method for processing the callData.privateData - if it has been specified
* Since the tapi privateData supplies additional informations about the call, additional assumptions can be made for questions like:
* "who is the calling number?", "is the call a connected (=redirected) call?", etc.
*
* The object will set properties within the callData-member of your object and not return any value.
*
* There is no need to call this function your own because it's done in the constructor automatically.
*
* @return undefined
*/
Johannes Goderbauer
committed
IncomingCallExecutor.prototype.processPrivateData = function()
{
// handling tapi-drivers which support privateData: extending the call information
if (this.callData.privateData)
{
var connectedNumber = this.callData.privateData.connectedNumber;
var callerNumber = this.callData.privateData.callerNumber;
var calledNumber = this.callData.privateData.calledNumber;
if (connectedNumber)
{
this.callData.isIncomingCall = true;
this.callData.callAddress = connectedNumber;
}
else if (this.callData.isIncoming)
this.callData.callAddress = callerNumber;
else
this.callData.callAddress = calledNumber;
}
};
/**
* searches for additional information to the given local-phone address and stores them in the following property in the object:
* <ul>
* <li>contactsLocal</li>
* </ul>
*
* //TODO: describe what is set there exactly
* @return undefined
*/
IncomingCallExecutor.prototype.collectDataFromLocalInfo = function()
{
this.usersLocal = [];
var users = tools.getUsersByAttribute(tools.PHONE_ADDRESS, [this.callData.localAddress], tools.PROFILE_DEFAULT);
var userContactIds = [];
for (var i = 0, l = users.length; i < l; i++)
{
var user = users[i];
if (user[tools.PARAMS][tools.ISACTIVE] == "true")
{
this.usersLocal.push(user);
if (user[tools.PARAMS][tools.CONTACTID])
userContactIds.push(user[tools.PARAMS][tools.CONTACTID]);
}
}
this.contactsLocal = IncomingCallExecutor._getContactsFromNumber(null, userContactIds);
};
/**
* searches for additional information to the given call-phone address and stores them in the following properties in the object:
* <ul>
* <li>contactsCall</li>
* <li>usersCall</li>
* </ul>
*
* //TODO: describe what is set there exactly
* @return undefined
*/
IncomingCallExecutor.prototype.collectDataFromCallInfo = function()
{
this.contactsCall = IncomingCallExecutor._getContactsFromNumber(this.callData.callAddress);
this.usersCall = IncomingCallExecutor._getUsersFromContacts(this.contactsCall);
};
/**
* helper function that will log different callData and collected data to standard-output
* The function does not specify any module, importance or else
* Needed for live-analytics of problems and errors.
*
* @return undefined
*/
Johannes Goderbauer
committed
IncomingCallExecutor.prototype.logData = function()
{
logging.log("ctiServerEvents");
logging.log("callData>>" + JSON.stringify(this.callData));
logging.log("contactsLocal>>" + JSON.stringify(this.contactsLocal));
logging.log("usersLocal>>" + JSON.stringify(this.usersLocal));
logging.log("contactsCall>>" + JSON.stringify(this.contactsCall));
Johannes Goderbauer
committed
/**
* helper function that will log different callData and collected data to standard-output
* The function does not specify any module, importance or else
* Needed for live-analytics of problems and errors.
*
* @return undefined
*/
Johannes Goderbauer
committed
IncomingCallExecutor.prototype.getNotificationBaseConfig = function(pUserName)
{
if (!pUserName || !this.notificationContentId)
Johannes Goderbauer
committed
return null;
var notificationConfig = notification.createConfig()
Johannes Goderbauer
committed
.addUserWithId(pUserName)
.contentId(this.notificationContentId)//group all notifications of one call together
Johannes Goderbauer
committed
.notificationType("_____SYSTEM_NOTIFICATION_PHONECALL");
Johannes Goderbauer
committed
return notificationConfig;
};
/**
* sets a callback function for a given call state (for example the state ringing)
*
* @param {primitive} pCallState the state for the given function that will be executed whenver the state is reached
* @param {Function} pFunction the callback-function that will be executed; this function will get no parameter but has access to all object instance properties
*
* @private this function should not be called from outside
*
* @return undefined
*/
Johannes Goderbauer
committed
IncomingCallExecutor.prototype._setHandlerFn = function(pCallState, pFunction)
{
this._handlerFunctions[pCallState] = pFunction;
}
/**
* Sets a callback function which is called when an in- or outcoming call has has the state ringing/connection-rining.
* In the given function could be done some logging of the call for example.
*
* @param pFunction the callback-function that will be executed; this function will get no parameter but has access to all object instance properties
*
* @return undefined
*/
Johannes Goderbauer
committed
IncomingCallExecutor.prototype.setHandlerRinging = function(pFunction)
{
this._setHandlerFn(cti.CALLSTATE_RINGING, pFunction);
this._setHandlerFn(cti.CALLSTATE_CONNECTION_RINGING, pFunction);
}
/**
* Sets a callback function which is called when an in- or outcoming call has has the state talking
* In the given function could be done some logging of the call for example.
*
* @param pFunction the callback-function that will be executed; this function will get no parameter but has access to all object instance properties
*
* @return undefined
*/
Johannes Goderbauer
committed
IncomingCallExecutor.prototype.setHandlerTalking = function(pFunction)
{
this._setHandlerFn(cti.CALLSTATE_TALKING, pFunction);
}
/**
* Sets a callback function which is called when an in- or outcoming call has has the state disconnected.
* In the given function could be done some logging of the call for example.
*
* @param pFunction the callback-function that will be executed; this function will get no parameter but has access to all object instance properties
*
* @return undefined
*/
Johannes Goderbauer
committed
IncomingCallExecutor.prototype.setHandlerDisconnect = function(pFunction)
{
this._setHandlerFn(cti.CALLSTATE_DISCONNECTED, pFunction);
}
/**
* After all the data is initialized, collected and so on this method actually calls the correct handler depending on the callstate that is set
* If no handler function for that state is defined, nothing is done
*
* @return undefined
*/
Johannes Goderbauer
committed
IncomingCallExecutor.prototype.execute = function()
{
var state = this.callData.state;
if (state)
{
var fn = this._handlerFunctions[state];
if (fn)
fn.call(this);
}
}
/**
* helper function that converts a callstate into easy read- and understandable text
*
* @param {cti.CALLSATE_***} pCallstate the callstate as number/constant that shall be converted
*
* @return {String} descriptive shorttext of the callstate
*
* @private
*/
Johannes Goderbauer
committed
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
IncomingCallExecutor._callstateToText = function(pCallstate)
{
var callstateName;
switch(pCallstate)
{
case cti.CALLSTATE_RINGING:
callstateName = "CALLSTATE_RINGING"
break;
case cti.CALLSTATE_CONNECTION_RINGING:
callstateName = "CONNECTION_RINGING"
break;
case cti.CALLSTATE_TALKING:
callstateName = "CALLSTATE_TALKING"
break;
case cti.CALLSTATE_CONNECTING:
callstateName = "CALLSTATE_CONNECTING"
break;
case cti.CALLSTATE_BUSY:
callstateName = "CALLSTATE_BUSY"
break;
case cti.CALLSTATE_CREATED:
callstateName = "CALLSTATE_CREATED"
break;
case cti.CALLSTATE_DISCONNECTED:
callstateName = "CALLSTATE_DISCONNECTED"
break;
case cti.CALLSTATE_ON_HOLD:
callstateName = "CALLSTATE_ON_HOLD"
break;
case cti.CALLSTATE_UNKNOWN:
callstateName = "CALLSTATE_UNKNOWN"
break;
default:
callstateName = "CALLSTATE_UNKNOWN: " + pCallstate;
break;
}
return callstateName;
};
/**
* helper function to load a contact by a given number (or contactId) as they are required for formatting of a callers name for example
* will use the index for a fast phone number search; will load the actual data from the database
*
* either the number or contactIds have to be given to the function. you can specify both if you want to extent the search
* (look for the phoneNumber XXX and also look for those YYY contactIds; Note that this is an extension and not a an addtional filter and therefor
* will result in an SQL-OR-condtion)
*
* @param {String} pNumber phone number that shall be searched, formatting is ignored for searching
* @param {Array} pContactIds additional contactIds to load
*
* @return {Array} array of contacts where each element is an object with these properites
* <ul>
* <li>CONTACTID</li>
* <li>ORGANISATION_ID</li>
* <li>ORGANISATION_NAME</li>
* <li>PERSON_ID</li>
* <li>LANGUAGE</li>
* <li>PERSON_FULL_NAME</li>
* </ul>
*
* @private
* @static
*/
IncomingCallExecutor._getContactsFromNumber = function(pNumber, pContactIds)
Johannes Goderbauer
committed
{
var phoneNumber = pNumber;
Johannes Goderbauer
committed
if (!phoneNumber && (!pContactIds || pContactIds.length == 0))//either of one needs to be specified, otherwise we wouldn't know what to search
Johannes Goderbauer
committed
return [];
Johannes Goderbauer
committed
var contactIds = pContactIds || [];
if (pNumber)
{
/*
Searching for a number is done via the index because
- it's fast
- there is no need to sanitize stored phone-addresses or the phone-address we are looking for
(this is done by the api automatically when the indexFieldType is configured as "TELEPHONE")
- the pattern and terms can be configured very detailed (fuzzy, wildcards, boosting) etc. to ensure that the best results are delivered
(currently a basic search is done because this has proven to return the best results till now; however this may need to be adjusted to a connected telephy-system)
Searching the index is done via the indexsearch-methods and not an index-record container because we need to serach for both personContacts
and organisationContacts. There exists a entity "AnyContact_entity" which represents personContacts- and organisationContacts-data
but that entity has no index record container defined at the moment. So instead searching multiple IndexGroups is done here.
Because different indexGroup store data differently, we cannot directly load usage-data like the organisation-name, person-name and so on
from the index. Instead we are only loading the ID-field (which is the contactId in both groups) and then load the usage-data for these
contactIds.
*/
var patternConfig = indexsearch.createPatternConfig();
var searchTerm = indexsearch.createTerm(pNumber).setIndexField("phone");
patternConfig.plus(searchTerm);
Johannes Goderbauer
committed
var pattern = indexsearch.buildPattern(patternConfig);
Johannes Goderbauer
committed
var indexQuery = indexsearch.createIndexQuery().setPattern(pattern)
.addIndexGroups("Person", "Organisation")
.addResultIndexFields([indexsearch.FIELD_ID]);
var indexResult = indexsearch.searchIndex(indexQuery);
if (indexResult.HITS)
contactIds = contactIds.concat(indexResult.HITS.map(function (e){return e[indexsearch.FIELD_ID];}));
}
if (contactIds.length == 0)
return [];
//load entities does not work in serverProcesses at the moment, so instead use a traditional sql-query: //TODO: change this after #1047680 is done
/*
var config = entities.createConfigForLoadingRows().entity("AnyContact_entity")
.fields(["CONTACTID", "ORGANISATION_ID", "ORGANISATION_NAME", "PERSON_ID", "ISOLANGUAGE", "PERSON_FULL_NAME"])
.uids(contactIds);
rows = entities.getRows(config);
Johannes Goderbauer
committed
return rows;
*/
//load entities does not work here, so instead use a traditional sql-query:
var contacts = newSelect("CONTACT.CONTACTID, CONTACT.ORGANISATION_ID, ORGANISATION.NAME, CONTACT.PERSON_ID, CONTACT.ISOLANGUAGE, PERSON.LASTNAME, PERSON.FIRSTNAME")
.from("CONTACT")
.join("ORGANISATION", "ORGANISATION.ORGANISATIONID = CONTACT.ORGANISATION_ID")
.leftJoin("PERSON", "CONTACT.PERSON_ID = PERSON.PERSONID")
Johannes Goderbauer
committed
.where("CONTACT.STATUS", $KeywordRegistry.contactStatus$active())
Johannes Goderbauer
committed
.and("CONTACT.CONTACTID", contactIds, SqlBuilder.IN())
//order by import if more than one record would be returned to give a statement: the first created, active contact is used
.orderBy("CONTACT.DATE_NEW asc")
Johannes Goderbauer
committed
Johannes Goderbauer
committed
//map to the result how the entities-methods would return it to have less effort later when the mentioned ticket is done
return contacts.map(function (e){
Johannes Goderbauer
committed
return {
CONTACTID: e[0],
ORGANISATION_ID: e[1],
Johannes Goderbauer
committed
ORGANISATION_NAME: e[2].trim(),
Johannes Goderbauer
committed
PERSON_ID: e[3],
Johannes Goderbauer
committed
PERSON_FULL_NAME: e.slice(5).join(" ").trim() //quick solution until the ticket above (#1047680) is solved
Johannes Goderbauer
committed
};
});
};
/**
* helper function to load user data models by given contacts as they are returned in IncomingCallExecutor._getContactsFromNumber
* the CONTACTID-property of the contact will be used to search within the users
* Only active users are returned
*
* either the number or contactIds have to be given to the function. you can specify both if you want to extent the search
* (look for the phoneNumber XXX and also look for those YYY contactIds; Note that this is an extension and not a an addtional filter and therefor
* will result in an SQL-OR-condtion)
*
* @param {Array} pContacts contacts as they are returned in IncomingCallExecutor._getContactsFromNumber(...)
*
* @return {Array} array of user data models (PROFILE_DEFAULT)
*
* @private
* @static
*/
Johannes Goderbauer
committed
IncomingCallExecutor._getUsersFromContacts = function (pContacts)
{
var users = tools.getUsersByAttribute(tools.CONTACTID, pContacts.map(function(e){
return e.CONTACTID;
}), tools.PROFILE_DEFAULT);
users = users.filter(function (e){
return e[tools.PARAMS][tools.ISACTIVE] == "true";
});
return users;
};
/**
* will search within the contacts for a given user and then return the contacts language
* The CONTACTID of the user is used for finding the matching contact
*
* @param {Object} pUserObject user data model whose contact shall be searched
*
* @return {String} language as isocode of the contact or null if no language is set/the user was not found in the "contactsLocal" property
*
* @private
*/
Johannes Goderbauer
committed
IncomingCallExecutor.prototype.getLocaleFromUser = function(pUserObject)
{
var contactId = pUserObject[tools.CONTACTID];
var contact = this.contactsLocal.find(function (e){
return e.CONTACTID == contactId;
});
//language for output like notifications, this is most likely used within the callback-functions (action handlers):
if (contact && contact.ISOLANGUAGE)
return contact.ISOLANGUAGE;
Johannes Goderbauer
committed
else
return null;
};
/**
* helper function to format the local address (source phone address)
* Will replace special cti-phone elements like the provider (e.g. SIP/203 is changed to 203)
*
* @return {String} the formatted local-address or the original replaced number when the nubmber could not be formatted
*/
Johannes Goderbauer
committed
IncomingCallExecutor.prototype.getFormattedLocalAddress = function()
{
return IncomingCallExecutor.formatAddress(this.callData.localAddress);
Johannes Goderbauer
committed
}
/**
* helper function to format the call address (target phone address)
* Will replace special cti-phone elements like the provider (e.g. SIP/203 is changed to 203)
*
* @return {String} the formatted call-address or the original replaced number when the nubmber could not be formatted
*/
Johannes Goderbauer
committed
IncomingCallExecutor.prototype.getFormattedCallAddress = function()
{
return IncomingCallExecutor.formatAddress(this.callData.callAddress);
Johannes Goderbauer
committed
}
/**
* helper function to format a cti-phone address
* Will replace special cti-phone elements like the provider (e.g. SIP/203 is changed to 203)
*
* @static
* @param {String} pAddress the cti phone address that shall be formatted
*
* @return {String} the formatted number or the original replaced number when the nubmber could not be formatted
*/
IncomingCallExecutor.formatAddress = function (pAddress)
{
return cti.formatPhoneNumber(pAddress.replace(/P?J?SIP\//, ""), true, null);
}