lisa-api
Version:
Lisa Javascript XMPP Library =============
1,618 lines (1,406 loc) • 50.8 kB
JavaScript
// This is a basic and incomplete Javascript implementation of the
// data model of Compass.
//
// It handles events for users, calls and queues.
// The Compass data model, codenamed Lisa, is internally represented
// by Javascript objects.
// JS namespace declaration
var exports = exports || {};
Lisa = {};
exports.Lisa = Lisa;
// XML Namespaces
Strophe.addNamespace('LISA_REQUESTS', 'http://iperity.com/compass');
Strophe.addNamespace('LISA_NOTIFICATIONS', 'http://iperity.com/compass');
Strophe.addNamespace('PING', 'urn:xmpp:ping');
/*
* Observer - Observable pattern
*/
Lisa.Observable = function() {
this.observers = [];
this.addObserver = function(callback) {
if (jQuery.isFunction(callback)) {
this.observers.push(callback);
} else {
Lisa.Connection.logging
.log("WARNING: Lisa.Observable.addObserver: callback "
+ callback + " not a function. Not added");
}
}
this.removeObserver = function(callback) {
var index = this.observers.indexOf(callback);
delete this.observers[index];
}
this.notify = function() {
var args = arguments;
this.observers.forEach(function(f) {
if (f != undefined) {
f.apply(this, args);
}
});
}
}
/*
* User object
*/
Lisa.User = function() {
this.id = "";
this.name = "";
this.username = "";
this.jid = "";
this.extension = "";
this.calls = {};
this.queues = {};
this.pausedForQueue = {};
this.loggedIn = false;
this.addQueue = function(queue) {
if (!queue) {
Lisa.Connection.logging.warn("Tried to add queue, but queue is NULL");
return;
}
this.queues[queue.id] = queue;
this.observable.notify(this, Lisa.User.EventTypes.QueueAdded, queue);
queue.observable.notify(queue, Lisa.Queue.EventTypes.UserAdded, this);
}
this.removeQueue = function(queue) {
if (!queue) {
Lisa.Connection.logging.warn("Tried to remove queue, but queue is NULL");
return;
} else if (!this.queues[queue.id]) {
Lisa.Connection.logging.warn("Tried to remove queue, but queue " +
queue + " not present in model " + this);
return;
}
delete this.queues[queue.id];
this.observable.notify(this, Lisa.User.QueueRemoved, queue);
queue.observable.notify(queue, Lisa.Queue.EventTypes.UserRemoved, this);
}
this.toString = function() {
return "(User:" + this.id + ":" + this.name + ",l:" + this.loggedIn
+ ")";
}
this.observable = new Lisa.Observable();
}
Lisa.User.EventTypes = {
CallAdded : "CallAdded",
CallRemoved : "CallRemoved",
QueueAdded : "QueueAdded",
QueueRemoved : "QueueRemoved",
AmAdded : "AmAdded",
AmRemoved : "AmRemoved",
PropertyChanged : "PropertyChanged"
};
Lisa.RemovedCalls = function() {
this.callIds = {};
this.position = {};
this.currentPosition = 0;
this.maxPosition = 127;
this.addRemovedCall = function (callId){
// Empty the currentPosition.
var oldCallId = this.position[this.currentPosition];
if (oldCallId) delete this.callIds[oldCallId];
// Store the new call-id in currentPosition.
this.callIds[callId] = callId;
this.position[this.currentPosition] = callId;
this.currentPosition++;
// Wrap around if currentPosition > maxPosition
if (this.currentPosition > this.maxPosition) {
this.currentPosition = 0;
}
Lisa.Connection.logging.log("Currently storing " + _.size(this.callIds) + " removed calls.");
}
this.callIsRemoved = function(callId) {
return (this.callIds[callId] != undefined);
}
}
/*
* Queue object
*/
Lisa.Queue = function() {
this.id = "";
this.name = "";
this.users = {};
this.calls = {};
this.callsOrdered = [];
this.maxWait = 0;
this.averageWait = 0;
this.totalWait = 0;
this.handledCalls = 0;
this.totalCalls = 0;
this.paused = false;
this.addUser = function(user) {
if (!user) {
Lisa.Connection.logging.warn("Tried to add user, but user is NULL");
return;
}
this.users[user.id] = user;
this.observable.notify(this, Lisa.Queue.EventTypes.UserAdded, user);
user.observable.notify(user, Lisa.User.EventTypes.QueueAdded, this);
}
this.removeUser = function(user) {
if (!user) {
Lisa.Connection.logging.warn("Tried to remove user, but user is NULL");
return;
} else if (!this.users[user.id]) {
Lisa.Connection.logging.warn("Tried to remove user, but user " +
user + " not present in model " + this);
return;
}
delete this.users[user.id];
this.observable.notify(this, Lisa.Queue.EventTypes.UserRemoved, user);
user.observable.notify(user, Lisa.User.EventTypes.QueueRemoved, this);
}
this.addCall = function(call) {
if (!call) {
Lisa.Connection.logging.warn("Tried to add call, but call is NULL");
return;
}
Lisa.Connection.logging.log("Adding call " + call + " to queue " + this);
if (!this.calls[call.id]) {
this.calls[call.id] = call;
this.callsOrdered.push(call);
call.queue = this;
this.observable.notify(this, Lisa.Queue.EventTypes.CallAdded, call);
call.observable.notify(call);
}
}
this.removeCall = function(call) {
if (!call) {
Lisa.Connection.logging.warn("Tried to remove call, but call is NULL");
return;
} else if (!this.calls[call.id]) {
Lisa.Connection.logging.warn("Tried to remove call, but call " +
call + " not present in model " + this);
return;
}
Lisa.Connection.logging.log("Removing call " + call + " from queue " + this);
call.queue = null;
delete this.calls[call.id];
var orderedIndexToRemove = this.callsOrdered.indexOf(call);
this.callsOrdered.splice(orderedIndexToRemove, 1);
this.observable.notify(this, Lisa.Queue.EventTypes.CallRemoved, call);
call.observable.notify(call);
}
this.toString = function() {
return "(Queue:" + this.id + ":" + this.name + ")";
}
this.observable = new Lisa.Observable();
}
Lisa.Queue.EventTypes = {
CallAdded : "CallAdded",
CallRemoved : "CallRemoved",
UserAdded : "UserAdded",
UserRemoved : "UserRemoved",
AmAdded : "AmAdded",
AmRemoved : "AmRemoved",
PropertyChanged : "PropertyChanged"
};
/*
* Call object
*/
Lisa.Call = function() {
this.id = "";
this.queue = null;
this.source = null;
this.sourceUser = null;
this.sourceObj = null;
this.destination = null;
this.destinationUser = null;
this.destinationObj = null;
this.observable = new Lisa.Observable();
this.queueCallForCall = null;
this.userHasChanged = false;
this.toString = function() {
var srcStr = (this.sourceObj) ? this.sourceObj.toString() : this.source ;
var dstStr = (this.destinationObj) ? this.destinationObj.toString() : this.destination ;
return "(Call:" + this.id + ",f:" + srcStr + ",t:"
+ dstStr + ")";
}
this.removeUser = function(user) {
if (!user) {
Lisa.Connection.logging.warn("MODEL::Call - Tried to remove user, but user is NULL");
return;
}
if (this.sourceUser == user) {
delete this.sourceUser.calls[this.id];
}
if (this.destinationUser == user) {
delete this.destinationUser.calls[this.id];
}
user.observable.notify(user, Lisa.User.EventTypes.CallRemoved, this);
}
}
Lisa.Call.EventTypes = {
AmAdded: "AmAdded",
AmRemoved: "AmRemoved"
}
/**
* The Lisa Model object.
*
* The library gives access to an instance of this object through the getModel() call,
* and constantly keeps it updated with the the state of the platform.
*/
Lisa.Model = function() {
this.users = {}; // Array of users, keyed by id.
this.queues = {}; // Array of queues, keyed by id.
this.calls = {}; // Array of calls, keyed by id.
this.pingSuccess = false;
this.removedCalls = new Lisa.RemovedCalls();
this.userListObservable = new Lisa.Observable();
this.queueListObservable = new Lisa.Observable();
this.callListObservable = new Lisa.Observable();
this.pingSuccessObservable = new Lisa.Observable();
this.addCall = function(call) {
var newCall = (this.calls[call.id] == null);
this.calls[call.id] = call;
Lisa.Connection.logging.log("MODEL: " + ((newCall) ? "Added" : "Updated") + " call " + call);
if (newCall) {
this.callListObservable.notify(call, 'add');
} else {
call.observable.notify(call, Lisa.Call.EventTypes.AmAdded);
}
// Attach the call to any involved users.
if (call.sourceUser) {
call.sourceUser.calls[call.id] = call;
call.sourceUser.observable.notify(call.sourceUser, Lisa.User.EventTypes.CallAdded, call);
}
if (call.destinationUser) {
call.destinationUser.calls[call.id] = call;
call.destinationUser.observable.notify(call.destinationUser, Lisa.User.EventTypes.CallAdded, call);
}
if (newCall) {
call.firstSource = call.source;
call.firstDestination = call.destination;
call.firstSourceObj = call.sourceObj;
call.firstDestinationObj = call.destinationObj;
}
}
this.removeCall = function(theCall) {
Lisa.Connection.model.removedCalls.addRemovedCall(theCall.id);
var call = this.calls[theCall.id];
if (call == null) {
Lisa.Connection.logging
.log("MODEL: WARNING: Request to remove call, but call not in data model."
+ call);
return;
}
// Delete the call.
delete this.calls[call.id];
// Delete the call from all objects that might refer to it.
if (call.sourceUser) {
delete call.sourceUser.calls[call.id];
call.sourceUser.observable.notify(call.sourceUser, Lisa.User.EventTypes.CallRemoved, call);
}
if (call.destinationUser) {
delete call.destinationUser.calls[call.id];
call.destinationUser.observable.notify(call.destinationUser, Lisa.User.EventTypes.CallRemoved, call);
}
if (call.queue) {
call.queue.removeCall(call);
}
call.observable.notify(call, Lisa.Call.EventTypes.AmRemoved);
Lisa.Connection.logging.log("MODEL: Removed call " + call);
this.callListObservable.notify(call, 'remove');
}
this.addQueue = function(queue) {
Lisa.Connection.logging.log("MODEL: Added or updated queue " + queue);
var newQueue = (this.queues[queue.id] == null);
// Add the queue to the global model.
this.queues[queue.id] = queue;
// Add all calls that are part of the queue to the model.
for ( var callId in queue.calls) {
this.addCall(queue.calls[callId]);
}
queue.observable.notify(queue, Lisa.Queue.EventTypes.AmAdded);
if (newQueue)
this.queueListObservable.notify(this, queue);
}
this.addUser = function(user) {
if (user == null) {
return;
}
Lisa.Connection.logging.log("MODEL: Added user " + user);
var newUser = (this.users[user.id] == null);
this.users[user.id] = user;
// --- Send Notifications ---
user.observable.notify(user, Lisa.User.EventTypes.AmAdded);
for ( var queueId in user.queues) {
var queue = this.queues[queueId];
queue.observable.notify(queue, Lisa.Queue.EventTypes.UserAdded, user);
user.observable.notify(user, Lisa.User.EventTypes.QueueAdded, queue);
}
if (newUser) {
Lisa.Connection.model.userListObservable.notify(this, user);
}
}
this.removeUser = function(id) {
Lisa.Connection.logging.log("Removing user with id " + id);
var user = this.users[id];
delete this.users[id];
if (user) {
for ( var queueId in user.queues) {
// Delete user from queue as well.
var queue = this.queues[queueId];
delete queue.users[user.id];
Lisa.Connection.logging.log("Deleted user " + user
+ "from queue" + queue);
// Notify any queues that the user used to be in
queue.observable.notify(queue, Lisa.Queue.EventTypes.UserRemoved, user);
}
}
this.userListObservable.notify(this, user);
}
this.getUser = function(id) {
return this.users[id];
}
this.getQueue = function(id) {
return this.queues[id];
}
this.getCall = function(id) {
return this.calls[id];
}
}
function randomstring(L) {
var s = '';
var randomchar = function() {
var n = Math.floor(Math.random() * 62);
if (n < 10)
return n; // 1-10
if (n < 36)
return String.fromCharCode(n + 55); // A-Z
return String.fromCharCode(n + 61); // a-z
}
while (s.length < L)
s += randomchar();
return s;
}
/**
* The Lisa Connection object.
*
* This is the main interface to the library.
*/
Lisa.Connection = function() {
// === Static variables
Lisa.Connection.connectionStatusObservable = new Lisa.Observable();
Lisa.Connection.model = new Lisa.Model();
Lisa.Connection.myUserId = 0; // Lisa user id.
Lisa.Connection.jid = ""; // bare jid (username@domain, no resource)
Lisa.Connection.userName = ""; // Compass username. May be with or without domain.
Lisa.Connection.password = "";
Lisa.Connection.server = ""; // the XMPP server hostname
Lisa.Connection.domain = ""; // the domain of the user (domain part of jid)
Lisa.Connection.restServer = "";
Lisa.Connection.restUserUrl = "";
Lisa.Connection.restIdentityUrl = "";
Lisa.Connection.serverTimeOffset = 0;
// === Local variables
var self = this;
var connection;
var companyId;
var initDeferred = $.Deferred();
var modelCompleteDeferred = $.Deferred();
var usersDone = false;
var queuesDone = false;
var callsDone = false;
var restDone = false;
var pingTimer = null;
var manuallyDisconnected = false;
// === Public members
this.retrieve_model = true;
this.log_xmpp = false;
this.connectionStatusObservable = Lisa.Connection.connectionStatusObservable;
this.model = Lisa.Connection.model;
// Start pinging after the initialisation-procedure is started for the first time.
initDeferred.done(function() {
pingXmpp();
});
/**
* Connect to the XMPP server through BOSH
* @param server The hostname of the BOSH server
* @param jid Either the bare jid (username@hostname),
* in which case username@server is assumed.
* @param password Compass password for the account. (used for XMPP and REST)
* @param resource (optional) XMPP resource string, automatically generated when left empty.
*/
this.connect = function(server, jid, password, resource) {
manuallyDisconnected = false;
this.internalConnect(server, jid, password, resource);
}
this.internalConnect = function(server, jid, password, resource) {
this.setupConnection(server, jid);
Lisa.Connection.password = password;
// Configuration
resource = resource || "LisaApi" + "_" + randomstring(10);
var fullJid = jid + '/' + resource;
// .. and connect.
Lisa.Connection.connection.connect(fullJid, password, connectionStatusChanged);
}
/** Attach to a previous BOSH session
* Note: Doesn't store the password, and as such disables
* use of functions that use the REST api such as
* dialNumber, dialUser, queueLogin, and queueLogout, and
* disables auto-reconnect.*/
this.attach = function(server, jid, sid, rid) {
this.setupConnection(server, jid);
// Attach callbacks...
connection.addHandler(callback(function(stanza) {
onMessageReceived(stanza);
return true;
}), null, 'message', null, null, null);
connection.addHandler(callback(function(stanza) {
onPubsubReceived(stanza);
return true;
}), null, 'message', null, null, connection.PubSub.service);
// Attach
Lisa.Connection.logging.log("Attaching to session with sid: " + sid
+ " ,jid: " + jid + " ,rid: " + rid);
Lisa.Connection.connection.attach(jid, sid, rid, connectionStatusChanged);
connectionStatusChanged(Strophe.Status.CONNECTED);
}
/** Disconnect from the server. */
this.disconnect = function() {
manuallyDisconnected = true;
this.internalDisconnect();
}
function clearObject(object) {
for (var member in object) delete object[member];
}
this.internalDisconnect = function() {
Lisa.Connection.logging.log("Disconnecting..");
Lisa.Connection.connection.disconnect(); // Disconnect.
Lisa.Connection.connection.flush(); // Immediately send the disconnection request.
Lisa.Connection.connection.reset(); // Allow the connection to be re-used.
// Clear the model
clearObject(Lisa.Connection.model.users);
Lisa.Connection.model.userListObservable.notify();
clearObject(Lisa.Connection.model.queues);
Lisa.Connection.model.queueListObservable.notify();
clearObject(Lisa.Connection.model.calls);
Lisa.Connection.model.callListObservable.notify();
}
this.sendIQ = function(iq) {
var deferred = $.Deferred();
connection.sendIQ(iq, callback(function(res) {
res = $(res);
deferred.resolve(res);
}), callback(function(res) {
res = $(res);
deferred.reject(res);
}));
return deferred;
}
this.setupConnection = function(server, jid) {
if (jid.indexOf("@") == -1) {
var msg = "JID does not contain an \"@\", please specify the jid, not just the username! - Interrupting connection.";
Lisa.Connection.logging.log(msg);
console.error(msg);
return;
}
// configuration
Lisa.Connection.server = server;
Lisa.Connection.domain = jid.substring(jid.indexOf('@') + 1);
Lisa.Connection.jid = jid;
// Setup strophe connection
var bosh_service = 'https://' + server + '/http-bind';
Lisa.Connection.connection = new Strophe.Connection(bosh_service);
connection = Lisa.Connection.connection;
if (this.log_xmpp) {
connection.rawInput = this.logging.logInput;
connection.rawOutput = this.logging.logOutput;
}
// Attach callbacks...
connection.addHandler(callback(function(stanza) {
onMessageReceived(stanza);
return true;
}), null, 'message', null, null, null);
// If pubsub plugin loaded
if (connection.PubSub) {
connection.addHandler(callback(function(stanza) {
onPubsubReceived(stanza);
return true;
}), null, 'message', null, null, connection.PubSub.service);
}
// create jQuery deferred to communicate initialisation success or
// error.
initDeferred.fail(Lisa.Connection.logging.log);
initDeferred.fail(function() {
modelCompleteDeferred.reject.apply(this, arguments);
});
return initDeferred;
};
this.getStropheConnection = function() {
return Lisa.Connection.connection;
};
/*
* Actions
*/
/** Have the current user dial a phone-numer.*/
this.dialNumber = function(number) {
var deferred = jQuery.Deferred();
Lisa.Connection.logging.log("Calling " + number);
restAjaxRequest(
Lisa.Connection.restUserUrl + "/dialNumber",
{destination: number},
function (response){
Lisa.Connection.logging.log("Issued call to " + number);
deferred.resolve();
},
function() {
deferred.reject();
}
);
return deferred;
}
/** Have the current user dial another user */
this.dialUser = function(user) {
var deferred = jQuery.Deferred();
Lisa.Connection.logging.log("Calling " + user);
var targetUserUrl = "https://" + Lisa.Connection.restServer + "/user/" + user.id;
restAjaxRequest(
Lisa.Connection.restUserUrl + "/dialUser",
{callee: targetUserUrl},
function (response){
Lisa.Connection.logging.log("Issued call to " + user);
deferred.resolve();
},
function () {
deferred.reject();
}
);
return deferred;
}
function getQueueId(queue) {
var queueId = "https://" + Lisa.Connection.restServer + "/queue/" + queue.id;
return queueId;
}
/** Pause the current user in a queue **/
this.queuePause = function(queue) {
var deferred = jQuery.Deferred();
var queueId = getQueueId(queue);
Lisa.Connection.logging.log("Pausing queue " + queueId + " for user." );
restAjaxRequest(
Lisa.Connection.restIdentityUrl + "/pauseQueue",
{queue:queueId},
function (response){
Lisa.Connection.logging.log("paused queue " + queue);
deferred.resolve();
},
function (response){
Lisa.Connection.logging.log("Failed pausing queue " + queue);
deferred.reject();
}
)
}
/** UnPause the current user in a queue **/
this.queueUnpause = function(queue) {
var deferred = jQuery.Deferred();
var queueId = getQueueId(queue);
Lisa.Connection.logging.log("Unpausing queue " + queueId + " for user." );
restAjaxRequest(
Lisa.Connection.restIdentityUrl + "/unpauseQueue",
{queue:queueId},
function (response){
Lisa.Connection.logging.log("unpaused queue " + queue);
deferred.resolve();
},
function (response){
Lisa.Connection.logging.log("Failed Unpausing queue " + queue);
deferred.reject();
}
)
}
/** Pause the current user in all logged-in queues */
this.pauseAllQueues = function() {
Lisa.Connection.logging.log("pausing user in all queues." );
for (var queueKey in Lisa.Connection.model.users[Lisa.Connection.myUserId].queues) {
var queue = Lisa.Connection.model.users[Lisa.Connection.myUserId].queues[queueKey];
this.queuePause(queue);
}
}
/** Unpause the current user in all logged-in queues*/
this.unpauseAllQueues = function() {
Lisa.Connection.logging.log("unpausing user in all queues." );
for (var queueKey in Lisa.Connection.model.users[Lisa.Connection.myUserId].queues) {
var queue = Lisa.Connection.model.users[Lisa.Connection.myUserId].queues[queueKey];
this.queueUnpause(queue);
}
}
/** Have the current user logon to a queue*/
this.queueLogin = function(queue) {
// First, see whether we have any previous settings stored for the queue.
var fromLs = localStorage.getItem(localStorageKeyForQueue(queue));
var settingsObj = (fromLs) ? JSON.parse(fromLs) : {};
var deferred = this.queueLoginWithSettings(queue, settingsObj.priority || 1, settingsObj.callForward || false);
return deferred;
}
/** Have the current user logon to a queue with known settings*/
this.queueLoginWithSettings = function(queue, priority, callForward) {
var deferred = jQuery.Deferred();
var queueId = getQueueId(queue);
Lisa.Connection.logging.log("Logging in to queue " + queueId
+ " with priority " + priority + " and follow-forwards " + callForward);
restAjaxRequest(
Lisa.Connection.restIdentityUrl + "/loginQueue",
{queue:queueId, priority:priority, callForward:callForward},
function (response){
Lisa.Connection.logging.log("Logged onto queue" + queue);
deferred.resolve();
},
function (response){
Lisa.Connection.logging.log("Failed logging onto queue" + queue);
deferred.reject();
}
)
return deferred;
}
/** Have the current user log out of a queue */
this.queueLogout = function(queue) {
var deferred = jQuery.Deferred();
// First, retrieve any queue membership settings, so we can
// set these settings correctly upon logging in again.
restAjaxRequest(
Lisa.Connection.restIdentityUrl + "/queueMemberships",
null,
function(queue) {
return function (response){
// Store current queue settings
var queueId = getQueueId(queue);
var currentSettings = _.where(response, {queue: queueId})[0];
var settingsObj = {priority: currentSettings.priority, callForward: currentSettings.callForward};
Lisa.Connection.logging.log("Storing settings " + JSON.stringify(settingsObj) + " for queue " + queue + " on logout.");
localStorage.setItem(localStorageKeyForQueue(queue), JSON.stringify(settingsObj));
// then, logout of the queue.
Lisa.Connection.logging.log("Logging out of queue " + queueId);
restAjaxRequest(
Lisa.Connection.restIdentityUrl + "/logoutQueue",
{queue:queueId},
function (response){
Lisa.Connection.logging.log("Logged out of queue" + queue);
deferred.resolve();
},
function (response){
Lisa.Connection.logging.log("Failed logging out of queue" + queue);
deferred.reject();
}
)
}
}(queue),
null,
"GET"
)
return deferred;
}
function localStorageKeyForQueue(queue) {
return "queueSettings_" + queue.id;
}
/*
* One-off getters
*
* Each of these functions returns a JQuery.deferred()
* http://api.jquery.com/category/deferred-object/
*/
/**
* get the current company-id.
*
* @returns a JQuery.deferred that will resolve to the
* company-id of the current user when available.
*/
this.getCompanyId = function() {
var deferred = $.Deferred();
modelCompleteDeferred.done(function(deferred) {
return function() {
deferred.resolve(companyId);
};
}(deferred)); // Use closure to bring local var (deferred) into
// callback.
return deferred;
}
/**
* get the current company-name.
*
* @returns a JQuery.deferred that will resolve to the
* company-name of the current user when available.
*/
this.getCompanyName = function() {
var deferred = $.Deferred();
modelCompleteDeferred.done(function(deferred) {
return function() {
deferred.resolve(companyName);
};
}(deferred)); // Use closure to bring local var (deferred) into
// callback.
return deferred;
}
/**
* get the current data-model.
*
* The data-model is automatically kept up-to-date with the
* state of the platform.
*
* @returns a JQuery.deferred that will resolve to the
* Lisa Api data model when available.
*/
this.getModel = function() {
var deferred = $.Deferred();
modelCompleteDeferred.done(function(deferred) {
return function() {
deferred.resolve(Lisa.Connection.model);
};
}(deferred));
modelCompleteDeferred.fail(function(deferred) {
return function() {
deferred.reject.apply(this, arguments);
};
}(deferred));
return deferred;
}
/** Logging * */
// Allow logging to be accessed statically (Lisa.Connection.logging) as well
// as through the instance (this.logging).
this.logging = Lisa.Connection.logging;
/** XMPP event callbacks * */
function connectionStatusChanged(status) {
if (status == Strophe.Status.CONNECTING) {
Lisa.Connection.logging.log('STATUS: Strophe is connecting.');
Lisa.Connection.logging.status('Connecting...');
} else if (status == Strophe.Status.CONNFAIL) {
Lisa.Connection.logging.log('STATUS: Strophe failed to connect.');
Lisa.Connection.logging.status('Connection failed.');
initDeferred.reject("Connection failed.");
} else if (status == Strophe.Status.DISCONNECTING) {
Lisa.Connection.logging.log('STATUS: Strophe is disconnecting.');
Lisa.Connection.logging.status('Disconnecting...');
} else if (status == Strophe.Status.DISCONNECTED) {
Lisa.Connection.logging.log('STATUS: Strophe is disconnected.');
Lisa.Connection.logging.status('Disconnected...');
} else if (status == Strophe.Status.CONNECTED) {
Lisa.Connection.logging.log('STATUS: Strophe is connected!');
Lisa.Connection.logging.status('Connected.');
onConnected();
} else if (status == Strophe.Status.AUTHFAIL) {
Lisa.Connection.logging.log('STATUS: authentication failed');
Lisa.Connection.logging.status('Authentication Failed.');
initDeferred.reject("Authentication Failed.");
}
Lisa.Connection.connectionStatusObservable.notify(status);
}
function pingNotReceived() {
Lisa.Connection.model.pingSuccessObservable.notify(Lisa.Connection.model.pingSuccess = false);
Lisa.Connection.logging.warn("XMPP PING answer not received in time, reconnecting...");
self.internalDisconnect();
_.delay(function(){
self.internalConnect(Lisa.Connection.server, Lisa.Connection.jid, Lisa.Connection.password);
}, 1000);
}
function pingXmpp() {
// Ping every 20 seconds.
pingTimer = _.delay(callback(pingXmpp), 20000);
if (manuallyDisconnected) return; // Client application disconnected manually; don't ping.
var cancelIfPingReceived = _.delay(pingNotReceived, 5000); // If no ping result after 5 seconds, call pingNotReceived.
iq = $iq({
to : 'phone.' + Lisa.Connection.domain,
type : 'get',
id : connection.getUniqueId('lisa')
}).c('ping', {
xmlns : Strophe.NS.PING
});
connection.sendIQ(iq,
callback( function() {
Lisa.Connection.logging.log("XMPP PING success");
Lisa.Connection.model.pingSuccessObservable.notify(Lisa.Connection.model.pingSuccess = true);
clearTimeout(cancelIfPingReceived);
}),
callback( function() {
Lisa.Connection.logging.log("XMPP PING error");
Lisa.Connection.model.pingSuccessObservable.notify(Lisa.Connection.model.pingSuccess = false);
console.log(arguments);
})
);
}
function onConnected() {
if (!self.retrieve_model)
return;
// set invisible, we don't want our user to get online
// NOTE: wait for invisible to be ack'ed (iq result) before
// sending presence; https://github.com/processone/ejabberd/issues/2652
self.setInvisible().then(function() {
self.sendInitialPresence();
});
// Get company
var iq = $iq({
to : 'phone.' + Lisa.Connection.domain,
type : 'get',
id : connection.getUniqueId('lisa')
}).c('request', {
xmlns : Strophe.NS.LISA_REQUESTS,
type: 'GET_COMPANY'
});
connection.sendIQ(iq, callback(onGetCompany), function() {
initDeferred.reject("Service is down.");
});
// Get users
iq = $iq({
to : 'phone.' + Lisa.Connection.domain,
type : 'get',
id : connection.getUniqueId('lisa')
}).c('request', {
xmlns : Strophe.NS.LISA_REQUESTS,
type: "GET"
}).c('filter', {
type: "user"
});
connection.sendIQ(iq, callback(processUser), function() {
initDeferred.reject("get user failed - Lisa problem?");
});
// Get queues
iq = $iq({
to : 'phone.' + Lisa.Connection.domain,
type : 'get',
id : connection.getUniqueId('lisa')
}).c('request', {
xmlns : Strophe.NS.LISA_REQUESTS,
type: "GET"
}).c('filter', {
type: "queue"
});
connection.sendIQ(iq, callback(processQueue), function() {
initDeferred.reject("get queue failed - Lisa problem?");
});
// Get queues
iq = $iq({
to : 'phone.' + Lisa.Connection.domain,
type : 'get',
id : connection.getUniqueId('lisa')
}).c('request', {
xmlns : Strophe.NS.LISA_REQUESTS,
type: "GET"
}).c('filter', {
type: "call"
});
connection.sendIQ(iq, callback(processCall), function() {
initDeferred.reject("get call failed - Lisa problem?");
});
// Get time
iq = $iq({
to : Lisa.Connection.domain,
type : 'get',
id : connection.getUniqueId('lisa')
}).c('time', {
xmlns : "urn:xmpp:time"
});
connection.sendIQ(iq, callback(processTime), function() {
});
}
function setupRest() {
// Setup REST server & often-used urls
Lisa.Connection.restServer = getEnvWithPrefix("rest");
Lisa.Connection.restUserUrl = "https://" + Lisa.Connection.restServer + "/user/" + Lisa.Connection.myUserId;
Lisa.Connection.logging.log("Found rest-user url: " + Lisa.Connection.restUserUrl);
Lisa.Connection.restAuthHeader = "Basic " + btoa(Lisa.Connection.userName + ":" + Lisa.Connection.password);
// Retrieve the identity
restAjaxRequest(
Lisa.Connection.restUserUrl + "/identities",
null,
function(response) {
Lisa.Connection.restIdentityUrl = _.where(response, {order: 0})[0].self;
Lisa.Connection.logging.log("Found rest-identity url: " + Lisa.Connection.restIdentityUrl);
restDone = true;
isModelComplete();
},
function(err) {
Lisa.Connection.logging.log("ERROR: /identities request failed - " + err);
},
"GET"
)
}
function restAjaxRequest(url, data, success, error, method) {
$.ajax
({
type: method || "POST",
headers: {
"Accept" : "application/vnd.iperity.compass.v2+json",
"Authorization": Lisa.Connection.restAuthHeader,
"X-No-Redirect": true
},
url: url,
dataType: 'json',
data: JSON.stringify(data),
success: success,
error: error
});
}
function processUser(xml) {
$(xml).find('result').children().each(
function(idx, user) {
var userModel = xmlToUser($(user));
if (userModel == null)
return;
Lisa.Connection.model.addUser(userModel);
// Are we this user?
if (userModel.jid == Lisa.Connection.jid) {
Lisa.Connection.myUserId = userModel.id;
Lisa.Connection.userName = userModel.username;
Lisa.Connection.logging.log("INFO: Found my userid: "
+ Lisa.Connection.myUserId);
usersDone = true;
// Now we have an User ID, Setup REST
setupRest();
}
});
isModelComplete();
}
function processQueue(xml) {
$(xml).find('result').children().each(function(idx, queue) {
var queueModel = xmlToQueue($(queue))
Lisa.Connection.model.addQueue(queueModel);
});
queuesDone = true;
isModelComplete();
}
function processCall(xml) {
$(xml).find('result').children().each(function(idx, call) {
var callModel = xmlToCall($(call), true)
if (callModel)
Lisa.Connection.model.addCall(callModel);
});
callsDone = true;
isModelComplete();
}
function processTime(xml) {
var timeStr = $(xml).find("time").find("utc").text();
//var timeZoneStr = $(xml).find("time").find("tzo").text();
Lisa.Connection.logging.log("Time on server: " + timeStr);
var serverDateObj = new Date(timeStr);
var serverTimeOffset = serverDateObj - new Date();
Lisa.Connection.logging.log("Server time-offet is " + serverTimeOffset + "ms"); // How many ms seems the server ahead?
if (Math.abs(serverTimeOffset) > 3000) {
Lisa.Connection.logging.log("Client time is off by more than 3 seconds... compensating.");
Lisa.Connection.serverTimeOffset = serverTimeOffset;
}
}
function isModelComplete() {
if (usersDone && queuesDone && callsDone && restDone) {
if (Lisa.Connection.myUserId == 0) {
Lisa.Connection.logging
.log("ERROR: Model complete, but couldn't find own user.");
modelCompleteDeferred.fail();
connectionStatusChanged(Strophe.Status.CONNFAIL);
return false;
}
Lisa.Connection.model.pingSuccessObservable.notify(Lisa.Connection.model.pingSuccess = true);
Lisa.Connection.logging.log("INFO: Initial model complete!")
modelCompleteDeferred.resolve();
return true;
}
return false;
}
function onMessageReceived(stanza) {
Lisa.Connection.logging.log("INFO: Message Received");
}
function onPubsubReceived(stanza) {
Lisa.Connection.logging.log("INFO: Pubsub Received.")
var state = $(stanza).find('subscription').attr('subscription');
if (state) {
Lisa.Connection.logging.log("INFO: Subscription state: " + state)
onSubscribe(stanza);
}
$('item', stanza).each(
function(idx, item) {
item = $(item).children(":first");
if (item.prop('nodeName').toLowerCase() == 'notification') {
onReceiveNotification(item);
} else {
Lisa.Connection.logging.log('Unknown message: '
+ item.prop('nodeName'));
}
});
}
function onGetCompany(stanza) {
Lisa.Connection.logging.log("INFO: Get Company Received;")
Lisa.Connection.logging.status("Subscribing...")
// Retrieve company id and name
companyId = $(stanza).find('result').find('id').text();
companyName = $(stanza).find('result').find('name').text();
// Subscribe to company pubsub stream
// NOTE: not using strophe's subscribe here; we want to subscribe using
// our full jid, not bare jid
var node = companyId;
var iq = $iq({
to : connection.PubSub.service,
type : 'set',
id : connection.getUniqueId('pubsub')
}).c('pubsub', {
xmlns : Strophe.NS.PUBSUB
}).c('subscribe', {
node : node,
jid : connection.jid
});
connection.sendIQ(iq, onSubscribe, callback(function() {
initDeferred.reject("Sending Subscription request failed.");
}));
}
// Called through onGetCompany, or through onPubsubReceived.
function onSubscribe(stanza) {
var state = $(stanza).find('subscription').attr('subscription');
if (state == 'pending') {
// Waiting for 'subscribed'.
} else if (state == 'subscribed') {
onSubscribed();
} else {
initDeferred.reject("Subscription failed");
}
}
function onSubscribed() {
Lisa.Connection.logging.log("INFO: Subscribed Received;")
Lisa.Connection.logging.status("Subscribed.")
initDeferred.resolve(); // Mark initialisation as completed.
}
function onReceiveNotification(msg) {
Lisa.Connection.logging.log("INFO: Notification Received;");
var type = msg.attr('type');
if (type.indexOf('notification.call') == 0) {
if (type == 'notification.call.end') {
var callId = msg.find("callId").text();
if (Lisa.Connection.model.removedCalls.callIsRemoved(callId)) {
Lisa.Connection.logging.log("WARNING: Call has already been removed, not removing again: " + callId);
return;
}
var call = findOrCreateCall(callId);
Lisa.Connection.model.removeCall(call);
} else if (type == 'notification.call.start') {
var call = msg.find('call');
var callModel = xmlToCall(call);
if (Lisa.Connection.model.removedCalls.callIsRemoved(callModel.id)) {
Lisa.Connection.logging.log("WARNING: Call has already been removed, not adding: " + callModel.id);
return;
}
Lisa.Connection.model.addCall(callModel);
} else if (type == 'notification.call.update') {
var call = msg.find('call');
var callModel = xmlToCall(call);
if (Lisa.Connection.model.removedCalls.callIsRemoved(callModel.id)) {
Lisa.Connection.logging.log("WARNING: Call has already been removed, not updating: " + callModel.id);
return;
}
Lisa.Connection.model.addCall(callModel);
}
} else if (type.indexOf('notification.queueMember') == 0) {
xmlToQueueMember(type, msg);
} else if (type.indexOf('notification.queue.call') == 0) {
xmlToQueueCall(type, msg);
} else if (type.indexOf('notification.queue.update') == 0) {
xmlQueueUpdate(msg);
} else if (type.indexOf('notification.user.update') == 0) {
xmlUserUpdate(msg);
} else if (type.indexOf('notification.user.status') == 0) {
var userXml = msg.find('user');
var user = xmlToUser(userXml);
Lisa.Connection.model.addUser(user);
}
}
function callback(cb) {
// Callback wrapper with
// (1) proper error reporting (Strophe swallows errors)
// (2) always returns true to keep the handler installed
return function() {
try {
cb.apply(this, arguments);
} catch (e) {
Lisa.Connection.logging
.log('ERROR: ' + (e.stack ? e.stack : e));
}
// Return true to keep calling the callback.
return true;
};
}
// Set ourselves to invisible mode.
this.setInvisible = function() {
var iq1 = $iq({
type : 'set',
id : connection.getUniqueId('lisa')
}).c('query', {
xmlns : 'jabber:iq:privacy'
}).c('list', {
name : 'invisible'
}).c('item', {
action : 'deny',
order : '1'
}).c('presence-out', {});
var iq2 = $iq({
type : 'set',
id : connection.getUniqueId('lisa')
}).c('query', {
xmlns : 'jabber:iq:privacy'
}).c('active', {
name : 'invisible'
});
this.sendIQ(iq1);
return this.sendIQ(iq2);
};
this.sendInitialPresence = function() {
var pres = $pres().c('priority').t('1');
connection.send(pres);
}
function xmlToUser(user) {
var id = user.attr('id');
var userModel = findOrCreateUser(id);
userModel.name = user.find('name').text();
userModel.loggedIn = (user.find('loggedIn').text() == "true");
// assume just 1 extension, for now
userModel.extension = user.find('extensions').text();
userModel.jid = user.find('identifiers').find('xmppJid').text();
userModel.username = user.find('identifiers').find('compassId').text();
return userModel;
}
function findOrCreateUser(id) {
var userModel = Lisa.Connection.model.users[id];
if (userModel == null) {
userModel = new Lisa.User();
}
userModel.id = id;
return userModel;
}
function xmlToQueueMember(type, msg) {
var member = msg.find('member');
var queueId = member.find('queueId').text();
var queue = Lisa.Connection.model.getQueue(queueId);
var userId = member.find('userId').text();
var user = Lisa.Connection.model.getUser(userId);
if (user && queue) {
if (type.indexOf('notification.queueMember.enter') == 0) {
queue.addUser(user);
user.addQueue(queue);
Lisa.Connection.logging.log("Added user " + user +
" to queue " + queue);
} else if (type.indexOf('notification.queueMember.leave') == 0) {
queue.removeUser(user);
user.removeQueue(queue);
Lisa.Connection.logging.log("Removed user " + user +
" from queue " + queue);
} else if (type.indexOf('notification.queueMember.update') == 0) {
var pausedSinceText = member.find('pausedSince').text();
if ((pausedSinceText != undefined) && (pausedSinceText != "")) {
if (parseInt(pausedSinceText) > 0) {
Lisa.Connection.logging.log("Setting user to paused for queue " + queue);
user.pausedForQueue[queue.id] = true;
user.observable.notify(user, Lisa.User.EventTypes.PropertyChanged, "pausedForQueue", user.pausedForQueue);
if (user.id == Lisa.Connection.myUserId) {
queue.paused = true;
queue.observable.notify(queue, Lisa.Queue.EventTypes.PropertyChanged, "paused", queue.paused);
}
}
} else {
Lisa.Connection.logging.log("Setting user to unpaused for queue " + queue);
user.pausedForQueue[queue.id] = false;
user.observable.notify(user, Lisa.User.EventTypes.PropertyChanged, "pausedForQueue", user.pausedForQueue);
if (user.id == Lisa.Connection.myUserId) {
queue.paused = false;
queue.observable.notify(queue, Lisa.Queue.EventTypes.PropertyChanged, "unpaused", queue.paused);
}
}
}
}
}
function xmlToQueueCall(type, notification) {
var queueId = notification.find('queueId').text();
var queue = Lisa.Connection.model.getQueue(queueId);
var queueCall = notification.find('queueCall');
var callId = queueCall.find('callId').text();
var call = Lisa.Connection.model.getCall(callId);
if (queue && call) {
if (type.indexOf('notification.queue.call.enter') == 0) {
queue.addCall(call);
} else if (type.indexOf('notification.queue.call.leave') == 0) {
queue.removeCall(call);
}
} else {
Lisa.Connection.logging.warn("Couldn't find queue " + queue +
" or call " + call + " " + callId);
}
}
function xmlToQueue(queue) {
var id = queue.attr('id');
var queueModel = findOrCreateQueue(id);
queueModel.name = queue.find('name').text();
// User-entries
queueModel.users = {};
queue.find('userEntries').children().each(
function(queueModel) {
return function(idx, child) {
var userId = $(child).find('userId').text();
var user = Lisa.Connection.model.users[userId];
var amPaused = $(child).find('pausedSince').text() != 0;
if (user) {
queueModel.addUser(user);
user.addQueue(queueModel);
Lisa.Connection.logging.log("Added user " + user
+ " to queue " + queueModel);
user.pausedForQueue[queueModel.id] = amPaused;
if (user.id == Lisa.Connection.myUserId) {
// We are this user.
queueModel.paused = amPaused;
}
}
}
}(queueModel));
// call-entries
queueModel.calls = {};
queue.find('callIds').children().each(function(queueModel) {
return function(idx, child) {
var callId = $(child).text();
var call = findOrCreateCall(callId);
call.id = callId;
queueModel.addCall(call);
}
}(queueModel));
// Statistics
queueModel.maxWait = getStatistic(queue, "maxWait");
queueModel.averageWait = getStatistic(queue, "averageWait");
queueModel.totalWait = getStatistic(queue, "totalWait");
queueModel.handledCalls = getStatistic(queue, "handledCalls");
queueModel.totalCalls = getStatistic(queue, "totalCalls");
return queueModel;
}
function xmlQueueUpdate(notification) {
var queueId = notification.find('queueId').text();
var queue = Lisa.Connection.model.getQueue(queueId);
if (queue == null) {
Lisa.Connection.logging.warn("Received notification for unknown queue id " + queueId);
return;
}
statisticsUpdate(notification, queue);
}
function xmlUserUpdate(notification) {
var userId = notification.find('userId').text();
var user = Lisa.Connection.model.getUser(userId);
if (user == null) {
Lisa.Connection.logging.warn("Received notification for unknown user id " + userId);
return;
}
notification.children().each(function(user) {
return function (idx, child) {
if (child.nodeName == "propertyChange") {
var name = $(child).find("name").text();
var value = $(child).find("newValue").text();
if (name == "loggedIn") {
user.loggedIn = (value == "true");
user.observable.notify(user, Lisa.User.EventTypes.PropertyChanged, "loggedIn", user.loggedIn);
}
}
}
}(user));
}
function statisticsUpdate(notificationXml, queueModel) {
notificationXml.children().each(function(queueModel) {
return function (idx, child) {
if (child.nodeName == "propertyChange") {
var statistic = $(child).find("name").text();
var statValue = $(child).find("newValue").text();
var statNumber = parseInt(statValue);
if (isNaN(statNumber)) statNumber = 0;
queueModel[statistic] = statNumber;
queueModel.observable.notify(queueModel, Lisa.Queue.EventTypes.PropertyChanged, statistic, statNumber);
}
}
}(queueModel));
}
function getStatistic(queueXml, elementName) {
var statistic = queueXml.find(elementName).text();
return (statistic == '') ? 0 : parseInt(statistic);
}
function findOrCreateCall(id) {
var callModel = Lisa.Connection.model.calls[id];
if (callModel == null) {
callModel = new Lisa.Call();
}
callModel.id = id;
return callModel;
}
function findOrCreateQueue(id) {
var queueModel = Lisa.Connection.model.queues[id];
if (queueModel == null) {
queueModel = new Lisa.Queue();
}
queueModel.id = id;
return queueModel;
}
function xmlToCall(call, initialData) {
initialData = initialData || false;
var id = call.attr('id');
var callModel = findOrCreateCall(id);
// Source
var src = call.find('source');
if (initialData && src.find('timeEnd').length != 0) {
Lisa.Connection.logging.log("Call " + callModel.id +
" already ended source. Not adding to model.");
return null;
}
var srcDesc = src.find('description').text();
//console.log("Source callpoint description: " + srcDesc);
var sourceUser = null
if (src.attr('type') == 'User') {
var userId = src.find('userId').text();
var sourceUser = findOrCreateUser(userId);
callModel.sourceObj = sourceUser;
} else if (src.attr('type') == 'Queue') {
var queueId = src.find('queueId').text();
if (queueId) {
callModel.sourceObj = Lisa.Connection.model.queues[queueId];
}
} else if (src.attr('type') == 'External') {
callModel.sourceObj = src.find('number').text();
}
// Remove the call from the original source user;
// After changing the sourceUser, we won't remember the original sourceuser anymore.
if (callModel.sourceUser && (callModel.sourceUser != sourceUser)) {
callModel.removeUser(callModel.sourceUser);
}
callModel.sourceUser = sourceUser;
callModel.source = src;
// Destination
var dst = call.find('destination');
if (initialData && dst.find('timeEnd').length != 0) {
Lisa.Connection.logging.log("Call " + callModel.id +
" already ended destination. Not adding to model.");
return null;
}
var dstDesc = dst.find('description').text();
//console.log("Destination callpoint description: " + dstDesc);
var destinationUser = null;
if (dst.attr('type') == 'User') {
var userId = dst.find('userId').text();
var destinationUser = findOrCreateUser(userId);
callModel.destinationObj = destinationUser;
} else if (dst.attr('type') == 'Queue') {
var queueId = dst.find('queueId').text();
if (queueId) {
callModel.destinationObj = Lisa.Connection.model.queues[queueId];
}
} else if (dst.attr('type') == 'External') {
callModel.destinationObj = dst.find('number').text();
} else if (dst.attr('type') == 'Dialplan') {
callModel.destinationObj = dst.find('exten').text();
}
// Remove the call from the original destination user;
// After changing the destinationUser, we won't remember the original destinationUser anymore.
if (callModel.destinationUser && (callModel.destinationUser != destinationUser)) {
callModel.removeUser(callModel.destinationUser);
if (dstDesc != "dialler") callModel.userHasChanged = true;
}
callModel.destinationUser = destinationUser;
callModel.destination = dst;
callModel.state = call.find('>state').text();
var queueCallElement = call.find('properties').find('QueueCallForCall');
if (queueCallElement) {
callModel.queueCallForCall = queueCallElement.text();
}
return callModel;
}
}
/* Logging functionality */
Lisa.Connection.logging = new function() {
this.cb = undefined;
this.statusCb = undefined;
this.loggingObservable = new Lisa.Observable();
this.statusObservable = new Lisa.Observable();
this.setCallback = function(callback) {
this.loggingObservable.addObserver(