UNPKG

lisa-api

Version:

Lisa Javascript XMPP Library =============

1,618 lines (1,406 loc) 50.8 kB
// 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(