///
import context = require('../core/Context');
import helper = require('../core/Helper');
import validator = require('../core/Validator');
import list = require('../core/List');
import presence = require('./Presence');
import contact = require('./Contact');
export class Call extends helper.Helper {
private list:list.List;
private presence:presence.Presence;
private contact:contact.Contact;
constructor(context:context.Context) {
super(context);
this.list = list.$get(context);
this.contact = contact.$get(context);
this.presence = presence.$get(context);
}
createUrl(options?:ICallOptions, id?:string) {
options = options || {};
if (!('personal' in options) && !('extensionId' in options)) options.personal = true;
return '/account/~/' +
(options.personal || options.extensionId ? ('extension/' + (options.extensionId || '~') + '/') : '') +
(options.active ? 'active-calls' : 'call-log') +
(id ? '/' + id : '');
}
getSessionId(call:ICall) {
return (call && call.sessionId);
}
isInProgress(call:ICall) {
return (call && call.result == 'In Progress');
}
isAlive(call:ICall) {
return (call && call.availability == 'Alive');
}
isInbound(call:ICall) {
return (call && call.direction == 'Inbound');
}
isOutbound(call:ICall) {
return !this.isInbound(call);
}
isMissed(call:ICall) {
return (call && call.result == 'Missed');
}
isFindMe(call:ICall) {
return (call && call.action == 'FindMe');
}
getCallerInfo(call:ICall):ICallerInfo {
return this.isInbound(call) ? call.from : call.to;
}
getAllCallerInfos(call:ICall):ICallerInfo[] {
return [this.getCallerInfo(call)].concat(this.isInbound(call) ? call.to : call.from);
}
formatDuration(call:ICall) {
function addZero(v) {
return (v < 10) ? '0' + v : v;
}
var duration = parseInt(call.duration),
hours = Math.floor(duration / (60 * 60)),
mins = Math.floor((duration % (60 * 60)) / 60),
secs = Math.floor(duration % 60);
return (hours ? hours + ':' : '') + addZero(mins) + ':' + addZero(secs);
}
filter(options?:ICallFilterOptions):(call:ICall)=>boolean {
options = this.utils.extend({
alive: true,
direction: '',
type: ''
}, options);
return this.list.filter([
//{condition: options.alive, filterFn: this.isAlive},
{filterBy: 'direction', condition: options.direction},
{filterBy: 'type', condition: options.type}
]);
}
comparator(options?:list.IListComparatorOptions) {
return this.list.comparator(this.utils.extend({
sortBy: 'startTime'
}, options));
}
/**
* Injects contact field with appropriate {IContact} data structure into all callerInfos found in
* all calls Warning, this function may be performance-consuming, reduce the amount of items passed to contacts
* and calls
*/
attachContacts(contacts:contact.IContact[], calls:ICall[], options?:contact.IContactMatchOptions) {
// Flatten all caller infos from all messages
var callerInfos = calls.reduce((callerInfos, call) => {
return callerInfos.concat(this.getAllCallerInfos(call));
}, []);
this.contact.attachToCallerInfos(callerInfos, contacts, options);
}
/**
* Check whether pair of calls are two legs of RingOut
*/
checkMergeability(outboundRingOutCall:ICall, inboundCall:ICall, options?:ICallProcessingOptions):boolean {
var getTime = (dateString) => {
return (new Date(dateString)).getTime();
};
return (
(!options.strict || outboundRingOutCall.action && outboundRingOutCall.action.toLowerCase().indexOf('ringout') != -1) &&
// Check directions
outboundRingOutCall.direction == 'Outbound' &&
inboundCall.direction == 'Inbound' &&
// Check that start times are equal or close enough
((!inboundCall.startTime && !outboundRingOutCall.startTime) || Math.abs(getTime(inboundCall.startTime) - getTime(outboundRingOutCall.startTime)) < (options.maxStartTimeDiscrepancy || 5000)) &&
// Check that numbers match
inboundCall.from.phoneNumber == outboundRingOutCall.to.phoneNumber &&
(inboundCall.to.phoneNumber == outboundRingOutCall.from.phoneNumber || inboundCall.to.name == outboundRingOutCall.from.name) //TODO Maybe name check is not required
);
}
combineCalls(outboundRingOutCall:ICall, inboundCall:ICall, options?:ICallProcessingOptions):ICall[] {
options = options || {};
var result = [];
outboundRingOutCall.hasSubsequent = true;
if (options.merge) {
outboundRingOutCall.duration = (outboundRingOutCall.duration > inboundCall.duration) ? outboundRingOutCall.duration : inboundCall.duration;
// TODO Usually information from inbound call is more accurate for unknown reason
outboundRingOutCall.from = inboundCall.to;
outboundRingOutCall.to = inboundCall.from;
// Push only one "merged" outbound call
result.push(outboundRingOutCall);
} else {
// Mark next call as subsequent
inboundCall.subsequent = true;
inboundCall.startTime = outboundRingOutCall.startTime; // Needed for sort
// Push both calls, first outbound then inbound
result.push(outboundRingOutCall);
result.push(inboundCall);
}
return result;
}
/**
* (!) Experimental (!)
*
* Calls in Recent Calls (Call Log) or Active Calls arrays can be combined if they are, for example, two legs of
* one RingOut. The logic that stands behind this process is simple:
*
* - Calls must have opposite directions
* - Must have been started within a certain limited time frame
* - Must have same phone numbers in their Caller Info sections (from/to)
*
* ```js
* var processedCalls = Call.processCalls(callsArray, {strict: false, merge: true});
* ```
*
* Flags:
*
* - if `strict` is `true` then only calls with RingOut in `action` property will be affected
* - `merge` — controls whether to merge calls (reducing the length of array) or give them `subsequent`
* and `hasSubsequent` properties
*/
processCalls(calls:ICall[], options?:ICallProcessingOptions):ICall[] {
var processedCalls = [],
callsToMerge = [],
self = this;
// Iterate through calls
calls.forEach((call:ICall) => {
var merged = false;
call.subsequent = false;
call.hasSubsequent = false;
// Second cycle to find other leg
// It is assumed that call is the outbound, secondCall is inbound
calls.some((secondCall:ICall) => {
if (call === secondCall) return false;
if (self.checkMergeability(call, secondCall, options)) {
// Push to result array merged call
processedCalls = processedCalls.concat(self.combineCalls(call, secondCall, options));
// Push to array calls that are merged
callsToMerge.push(call);
callsToMerge.push(secondCall);
merged = true;
}
return merged;
});
});
// After all calls are merged, add non-merged calls
calls.forEach((call:ICall) => {
if (callsToMerge.indexOf(call) == -1) processedCalls.push(call);
});
return processedCalls;
}
/**
* Converts Presence's ActiveCall array into regular Calls array
*/
parsePresenceCalls(activeCalls:presence.IPresenceCall[]):ICall[] {
return activeCalls.map((activeCall:presence.IPresenceCall):ICall => {
return {
id: activeCall.id,
uri: '',
sessionId: activeCall.sessionId,
from: {phoneNumber: activeCall.from},
to: {phoneNumber: activeCall.to},
direction: activeCall.direction,
startTime: '',
duration: 0,
type: '',
action: '',
result: this.presence.isCallInProgress(activeCall) ? 'In Progress' : activeCall.telephonyStatus,
telephonyStatus: activeCall.telephonyStatus // non-standard property for compatibility
};
});
}
getSignature(call:ICall) {
var cleanup = (phoneNumber) => {
return (phoneNumber || '').toString().replace(/[^0-9]/ig, '');
};
return call.direction + '|' + (call.from && cleanup(call.from.phoneNumber)) + '|' + (call.to && cleanup(call.to.phoneNumber));
}
mergePresenceCalls(presenceCalls:ICall[], presence:presence.IPresence):ICall[] {
var currentDate = new Date(),
activeCalls = this
.parsePresenceCalls(presence && presence.activeCalls || [])
.map((call:ICall) => {
// delete property to make sure it is skipped during merge
delete call.startTime;
return call;
});
presenceCalls = this.merge(presenceCalls || [], activeCalls, this.getSessionId, true);
presenceCalls.forEach((call:ICall) => {
if (!call.startTime) call.startTime = currentDate.toISOString();
});
return presenceCalls;
}
mergeAll(presenceCalls:ICall[], calls:ICall[], activeCalls:ICall[]):ICall[] {
// First, merge calls into presence calls
var presenceAndCalls = this.merge(presenceCalls || [], calls || [], this.getSessionId, true);
// Second, merge activeCalls into previous result
return this.merge(presenceAndCalls, activeCalls || [], this.getSessionId, true);
}
}
export function $get(context:context.Context):Call {
return context.createSingleton('Call', ()=> {
return new Call(context);
});
}
export interface ICall extends helper.IHelperObject {
sessionId?:string;
availability?:string;
startTime?:string;
duration?:number;
type?:string;
direction?:string;
action?:string;
result?:string;
to?:ICallerInfo;
from?:ICallerInfo;
subsequent?:boolean; // added during processing
hasSubsequent?:boolean; // added during processing
telephonyStatus?:string; // added during processing
}
export interface ICallOptions {
extensionId?:string;
active?:boolean;
personal?:boolean;
}
export interface ICallProcessingOptions {
maxStartTimeDiscrepancy?:number;
strict?:boolean;
merge?:boolean;
}
export interface ICallFilterOptions {
extensionId?:string;
direction?:string;
type?:string;
}
/**
* @see http://platform-dev.dins.ru/artifacts/documentation/latest/webhelp/dev_guide_advanced/CallerInfo.html
*/
export interface ICallerInfo {
phoneNumber?:string;
extensionNumber?:string;
name?:string;
location?:string;
contact?:contact.IContact; // corresponding contact (added by attachContacts methods)
contactPhone?:string; // contact's phone as written in contact (added by attachContacts methods)
}