Source: wtapi.presence.js

/**
 * A plugin that provides Presence functionality
 *
 * @version 1.0.0
 */
(function(WTAPI) {
    var NS_WILDIX_PRESENCE = "urn:wildix:presence";
    var NS_WILDIX_SUBSCRIPTIONS = "urn:wildix:subscriptions";

    /**
     * Constructor for Presence plugin.
     * Instance will be created each time when new WTAPI instance is created. <br />
     * Plugin could be accessible thought WTAPI with <b>presence</b> property.
     *
     * @tutorial presence-plugin
     * @memberof WTAPI
     * @alias WTAPI.PresencePlugin
     * @augments Observer
     * @public
     * @constructor
     */
    function PresencePlugin(wtapi) {
        this._presences = {}; // A list of available presences
        this._presences = {}; // A list of available presences
        this._subscriptions = []; // A list of manual subscriptions
        this._wtapi = wtapi;
        this._wtapi.registerHandler("iq", this._handleIQ, this, 10);
        this._wtapi.registerHandler("presence", this._handlePresence, this, 10);
        this._wtapi.addListener("connected", this._onConnected, this);
        this._wtapi.addListener("disconnected", this._onDisconnected, this);
        this._roster = this._wtapi.roster;
        this._empty_presence = new WTAPI.Presence();
        this._personal_presence = this._empty_presence;

        // Initialize supper
        WTAPI.Observable.call(this);
    }

    PresencePlugin.prototype = Object.create(WTAPI.Observable.prototype);

    // ============================================================
    // Public events
    // ============================================================

    /**
     * Indicates that user has changed his presence.
     *
     * @event WTAPI.PresencePlugin#presence_changed
     * @property {WTAPI.User} user
     * @property {WTAPI.Presence} presence
     */

    /**
     * Indicates that personal presence has been changed.
     *
     * @event WTAPI.PresencePlugin#personal_presence_changed
     * @property {WTAPI.Presence} presence
     */

    // ============================================================
    // Public functions
    // ============================================================

    /**
     * @callback SubscriptionsCallback
     * @param {WTAPI.User[]} users
     */

    /**
     * Returns a presence of current
     *
     * @returns {WTAPI.Presence}
     */
    PresencePlugin.prototype.getPersonalPresence = function() {
        return this._personal_presence;
    }

    /**
     *
     *
     * @param {WTAPI.Presence} presence
     * @param {function} callback
     */
    PresencePlugin.prototype.changePersonalPresence = function(presence, callback) {
        if (!(presence instanceof WTAPI.Presence)) {
            throw new Error("Presence should be an instance of WTAPI.Presence");
        }

        var scope = arguments[2] || this;
        var packet = this._createSetPresencePacket(presence);
        var handler = this._createSetPresenceHandler(callback, scope);
        this._wtapi.getConnection().sendIQ(packet, handler);
    }

    /**
     * Returns the presence info for a particular user.
     *
     * @param {WTAPI.User} user
     * @returns {WTAPI.Presence}
     */
    PresencePlugin.prototype.get = function(user) {
        var jid = user.getJid()
        return jid in this._presences ? this._presences[jid] : this._empty_presence;
    }

    /**
     * Retrieves a list of subscribed users (a list of users that send they presence).
     * All subscriptions (roster subscriptions and manual subscriptions) are returned to callback.
     *
     * @param {SubscriptionsCallback} callback
     */
    PresencePlugin.prototype.getSubscriptions = function(callback) {
        var scope = arguments[1] || this;
        var packet = this._createGetSubscriptionsPacket();
        var handler = this._createGetSubscriptionsHandler(callback, scope);
        this._wtapi.getConnection().sendIQ(packet, handler);
    }

    /**
     * Subscribe a specified extension for presence events.
     * Subscription will be removed when session is closed.
     *
     * It is not necessary to subscribe to users that already in roster, because they already subscribed.
     *
     * @param {String[]} extensions
     * @param {function} callback
     */
    PresencePlugin.prototype.subscribe = function(extensions, callback) {
        var subs = this._subscriptions.slice(0);;
        for (var i=0; i < extensions.length; i++) {
            var ext = extensions[i];
            var exist = false;
            for (var s=0; s < subs.length; s++) {
                if (subs[s] == ext) {
                    exist = true;
                    break;
                }
            }

            if (!exist) {
                subs.push(ext);
            }
        }

        var scope = arguments[1] || this;
        var packet = this._createSetSubscriptionsPacket(subs);
        var handler = this._createSetSubscriptionsHandler(callback, scope);

        this._subscriptions = subs;
        this._wtapi.getConnection().sendIQ(packet, handler);
    }

    /**
     * Remove previously subscribed user for current session.
     * When specified user already in roster, it is not possible to remove subscription from him.
     *
     * @param {String[]} extensions A list of extensions (user extensions)
     * @param {function} callback
     */
    PresencePlugin.prototype.unsubscribe = function(extensions, callback) {
        var subs = [];
        for (var s=0; s < this._subscriptions.length; s++) {
            var ext = this._subscriptions[s];
            var exist = false;
            for (var i=0; i < extensions.length; i++) {
                if (extensions[i] == ext) {
                    exist = true;
                    break;
                }
            }

            if (!exist) {
                subs.push(ext);
            }
        }

        var scope = arguments[1] || this;
        var packet = this._createSetSubscriptionsPacket(subs);
        var handler = this._createSetSubscriptionsHandler(callback, scope);

        this._subscriptions = subs;
        this._wtapi.getConnection().sendIQ(packet, handler);
    }

    // ============================================================
    // PresencePlugin callbacks
    // ============================================================

    /**
     * @private
     */
    PresencePlugin.prototype._onConnected = function() {
        // Format manual subscriptions.
        // Manual subscriptions are automatically removes when disconnected from server and we should restore them.
        if (this._subscriptions.length > 0) {
            this._wtapi.getConnection().sendIQ(
                this._createSetSubscriptionsPacket(this._subscriptions),
                this._createSetSubscriptionsHandler(function(){}, this)
            );
        }

        // Format packet to receive personal presence.
        this._wtapi.getConnection().sendIQ(
            this._createGetPersonalPresencePacket(),
            this._createGetPersonalPresenceHandler()
        );
    }

    /**
     * @private
     * @fires WTAPI.PresencePlugin#presence_changed
     * @fires WTAPI.PresencePlugin#personal_presence_changed
     */
    PresencePlugin.prototype._onDisconnected = function() {
        // Set all available presences to unavailable.
        for(var jid in this._presences) {
            var presence = this._presences[jid];
            if (presence instanceof WTAPI.Presence) {
                var user = this._roster.getUser(jid);

                if (user) {
                    this._presences[jid] = this._empty_presence;
                    this._fire("presence_changed", user, this._empty_presence);
                }
            }
        }

        this._presences = {};
        this._personal_presence = this._empty_presence;
        this._fire("personal_presence_changed", this._personal_presence);
    }

    /**
     *
     * @param iq
     * @returns {boolean}
     * @private
     */
    PresencePlugin.prototype._handleIQ = function(iq) {
        if (iq.getQueryXMLNS() == NS_WILDIX_PRESENCE) {
            this._personal_presence = this._parsePersonalPresence(iq.getNode()).build();
            this._fire("personal_presence_changed", this._personal_presence);
            return true;
        }

        return false;
    }

    /**
     *
     * @param {JSJaCPresence} packet
     * @private
     */
    PresencePlugin.prototype._handlePresence = function(packet) {
        var from = packet.getFromJID();
        if (from.getResource() != "") {
            //return;
        }

        var node = packet.getNode();
        var nick = packet.getChildVal('nick', 'http://jabber.org/protocol/nick')
        var jid = from.removeResource().toString();
        var presence = this._parsePresence(node).build();
        var user = this._roster.getUser(jid);

        if (user === null) {
        	if(nick == ''){
        		nick = null;
        	}
            user = this._roster.createUser(jid, nick);
        }

        this._presences[user.getJid()] = presence;
        this._fire("presence_changed", user, presence);
    }

    // ============================================================
    // Private functions
    // ============================================================

    /**
     *
     * @returns {*}
     * @private
     */
    PresencePlugin.prototype._createGetSubscriptionsPacket = function() {
        var packet = new JSJaCIQ().setType('get');
        packet.setQuery(NS_WILDIX_SUBSCRIPTIONS);
        return packet;
    }

    PresencePlugin.prototype._createGetPersonalPresencePacket = function() {
        var packet = new JSJaCIQ().setType('get');
        packet.setQuery(NS_WILDIX_PRESENCE);
        return packet;
    }

    /**
     *
     * @param callback
     * @param scope
     * @returns {{error_handler: Function, result_handler: Function}}
     * @private
     */
    PresencePlugin.prototype._createGetSubscriptionsHandler = function(callback, scope) {
        var self = this;
        return {
            result_handler: function(iq) {
                if (typeof callback == 'function') {
                    callback.call(scope, self._parseGetSubscriptionsResponse(iq));
                }
            },
            error_handler: function() {
                if (typeof callback == 'function') {
                    callback.call(scope, []);
                }
            }
        }
    }

    PresencePlugin.prototype._createGetPersonalPresenceHandler = function() {
        var self = this;
        return {
            result_handler: function(iq) {
                self._personal_presence = self._parsePersonalPresence(iq.getNode()).build();
                self._fire("personal_presence_changed", self._personal_presence);
            },
            error_handler: function() {
                // Notify about it
            }
        }
    }

    /**
     *
     * @param {String[]} users
     * @private
     */
    PresencePlugin.prototype._createSetSubscriptionsPacket = function(extensions) {
        var packet = new JSJaCIQ().setType('set');
        var query = packet.setQuery(NS_WILDIX_SUBSCRIPTIONS);
        var subscriptions = packet.getDoc().createElement('subscriptions');
        for (var i=0; i < extensions.length; i++) {
            var item = packet.getDoc().createElement("item");
            item.setAttribute("user", extensions[i]);
            subscriptions.appendChild(item);
        }
        query.appendChild(subscriptions);
        return packet;
    }

    PresencePlugin.prototype._createSetSubscriptionsHandler = function(callback, scope) {
        var self = this;
        return {
            result_handler: function(iq) {
                if (typeof callback == 'function') {
                    callback.call(scope, self._parseGetSubscriptionsResponse(iq));
                }
            },
            error_handler: function() {
                if (typeof callback == 'function') {
                    callback.call(scope, []);
                }
            }
        }
    }

    /**
     *
     *
     * @param presence
     * @returns {JSJaCPacket}
     * @private
     */
    PresencePlugin.prototype._createSetPresencePacket = function(presence) {
        var packet = new JSJaCIQ().setType('set');
        var query = packet.setQuery(NS_WILDIX_PRESENCE);
        var extra = packet.getDoc().createElement('extra');

        // Create show element
        if (presence.isAway() || presence.isDND()) {
            query.appendChild(packet.buildNode('show', {}, presence.isAway() ? "away" : "dnd"));
        }else if(presence.isMUR()){
        	query.appendChild(packet.buildNode('show', {}, "mur"));
        }

        // Create status message element
        if (presence.isStatusMessageAvailable()) {
            query.appendChild(packet.buildNode('status', {}, presence.getStatusMessage()));
        }

        // Create status until element
        if (presence.isStatusUntilAvailable() && (presence.isAway() || presence.isDND())) {
            extra.appendChild(packet.buildNode('until', {}, this._formatUntilDate(presence.getStatusUntil())));
        }

        // Create location element
        if (presence.isLocationAvailable()) {
            var location = packet.getDoc().createElement('location');
            location.appendChild(packet.buildNode('lat', {}, presence.getLocation().getLat()));
            location.appendChild(packet.buildNode('lng', {}, presence.getLocation().getLng()));
            location.appendChild(packet.buildNode('address', {}, presence.getLocation().getAddress()));
            extra.appendChild(location);
        }

        if (extra.childNodes.length > 0) {
            query.appendChild(extra);
        }
        return packet;
    }

    /**
     *
     * @param callback
     * @param scope
     * @returns {{result_handler: Function, error_handler: Function}}
     * @private
     */
    PresencePlugin.prototype._createSetPresenceHandler = function(callback, scope) {
        var self = this;
        return {
            result_handler: function(iq) {
                self._personal_presence = self._parsePersonalPresence(iq.getNode()).build();
                self._fire("personal_presence_changed", self._personal_presence);
                if (typeof callback == 'function') {
                    callback.call(scope, self._personal_presence);
                }
            },
            error_handler: function() {
                if (typeof callback == 'function') {
                    callback.call(scope, self._personal_presence);
                }
            }
        }
    }

    /**
     *
     * @param {Node} node
     * @returns {WTAPI.Presence.Builder}
     * @private
     */
    PresencePlugin.prototype._parsePresence = function(node) {
        var builder = new WTAPI.Presence.Builder();

        // Determine online / offline status
        if (!node.hasAttribute("type") || node.getAttribute("type") != "unavailable") {
            builder.setOnline();
        }

        // Determine user status (away / dnd / mur)
		var show = node.getElementsByTagName('show');
		if (show.length > 0 && show.item(0).firstChild) {
			switch (show.item(0).firstChild.nodeValue) {
                case "away":
                    builder.setAway();
                    break;
                case "dnd":
                    builder.setDND();
                    break;
                case "mur":
                    builder.setMUR();
                    break;
            }
		}

        // Determine status message
		var status_msg = node.getElementsByTagName('status');
		if (status_msg.length > 0 && status_msg.item(0).firstChild) {
            builder.setStatusMessage(status_msg.item(0).firstChild.nodeValue);
		}

		var extra = node.getElementsByTagName('extra');
		if (extra.length > 0) {
			extra = extra.item(0);

            // Determine device status (ringing / talking / rt)
			var device_show = extra.getElementsByTagName('device-show');
    		if (device_show.length > 0 && device_show.item(0).firstChild) {
    			switch (device_show.item(0).firstChild.nodeValue) {
                    case "ringing":
                        builder.setRinging();
                        break;
                    case "talking":
                        builder.setTalking();
                        break;
                    case "rt":
                        builder.setRingingAndTalking();
                        break;
                    case "registered":
                    	builder.setRegisteredDevice();
                        break;
                }
    		}

            // Determine presence expire attribute
    		var until = extra.getElementsByTagName('until');
    		if (until.length > 0 && until.item(0).firstChild) {
                var until = this._parseUntilDate(until.item(0).firstChild.nodeValue);
                if (until !== null) {
                    builder.setStatusUntil(until);
                }
    		}

            // Determine user location
			var location = extra.getElementsByTagName('location');
			if (location.length > 0) {
				location = location.item(0);
                var lat, lng, address;
				var latEl = location.getElementsByTagName('lat');
                var lngEl = location.getElementsByTagName('lng');
                var addressEl = location.getElementsByTagName('address');

                if (latEl.length > 0 && latEl.item(0).firstChild) {
        			lat = latEl.item(0).firstChild.nodeValue;
        		}
        		if (lngEl.length > 0 && lngEl.item(0).firstChild) {
        			lng = lngEl.item(0).firstChild.nodeValue;
        		}
        		if (addressEl.length > 0 && addressEl.item(0).firstChild) {
        			address = addressEl.item(0).firstChild.nodeValue;
        		}
                if (lat && lng && address) {
                    builder.setLocation(new WTAPI.Location(address, lat, lng));
                }
			}

			// Determine user info about connected call
			var connectedInfo = extra.getElementsByTagName('connected-info');
            if(connectedInfo.length > 0){
            	connectedInfo = connectedInfo.item(0);
            	var name, number;

            	var call_name = connectedInfo.getElementsByTagName('connected-name');
                if(call_name.length > 0 && call_name.item(0).firstChild){
                	name = call_name.item(0).firstChild.nodeValue;
                }

                var connectedNumber = connectedInfo.getElementsByTagName('connected-id');
                if(connectedNumber.length > 0 && connectedNumber.item(0).firstChild){
                	number = connectedNumber.item(0).firstChild.nodeValue;
                }

                if (name || number) {
                    builder.setConnectedCall(new WTAPI.ConnectedCall(name, number));
                }
            }

			var profileData = extra.getElementsByTagName('profile_data');
			if (profileData.length > 0) {
				profileData = profileData.item(0);

                var mail, authProvider, url, gender, language, parentHostUrl, photo;

                mail = authProvider = url = gender = language = parentHostUrl = photo = '';

				var mailEl = profileData.getElementsByTagName('mail');
                var authProviderEl = profileData.getElementsByTagName('auth_provider');
                var urlEl = profileData.getElementsByTagName('profile_url');
                var genderEl = profileData.getElementsByTagName('gender');
                var languageEl = profileData.getElementsByTagName('language');
                var parentHostUrlEl = profileData.getElementsByTagName('parentHostUrl');
                var photoEl = profileData.getElementsByTagName('photo_url');

                if (mailEl.length > 0 && mailEl.item(0).firstChild) {
                	mail = mailEl.item(0).firstChild.nodeValue;
        		}
        		if (authProviderEl.length > 0 && authProviderEl.item(0).firstChild) {
        			authProvider = authProviderEl.item(0).firstChild.nodeValue;
        		}
        		if (urlEl.length > 0 && urlEl.item(0).firstChild) {
        			url = urlEl.item(0).firstChild.nodeValue;
        		}
        		if (genderEl.length > 0 && genderEl.item(0).firstChild) {
        			gender = genderEl.item(0).firstChild.nodeValue;
        		}
        		if (languageEl.length > 0 && languageEl.item(0).firstChild) {
        			language = languageEl.item(0).firstChild.nodeValue;
        		}
        		if (parentHostUrlEl.length > 0 && parentHostUrlEl.item(0).firstChild) {
        			parentHostUrl = parentHostUrlEl.item(0).firstChild.nodeValue;
        		}
        		if (photoEl.length > 0 && photoEl.item(0).firstChild) {
        			photo = photoEl.item(0).firstChild.nodeValue;
        		}

                builder.setProfileData(new WTAPI.ProfileData(mail, authProvider, url, gender, language, parentHostUrl, photo));

			}
		}

		return builder;
    };

    /**
     *
     * @param node
     * @returns {WTAPI.Presence.Builder}
     * @private
     */
    PresencePlugin.prototype._parsePersonalPresence = function(node) {
        return this._parsePresence(node).setOnline();
    };

    /**
     *
     * @param packet
     * @returns {WTAPI.User[]}
     * @private
     */
    PresencePlugin.prototype._parseGetSubscriptionsResponse = function(packet) {
        var result = [];
        var node = packet.getNode();
        var subscriptions = node.getElementsByTagName("subscriptions");
        if (subscriptions.length != 1) {
            return result;
        }

        var items = subscriptions.item(0).getElementsByTagName("item");
        for (var i=0; i < items.length; i++) {
            var item = items.item(i);
            if (item.hasAttribute("user")) {
                var extension = item.getAttribute("user");
                var jid = extension;
                if(this._wtapi.isNewServer()){
                	var s = extension.split("@");
                    if (s.length <= 1){
                    	jid = extension + '@' + this._wtapi.server;
                    }
                }else{
                	jid = extension + "@wildix";
                }
                var user = this._roster.getUser(jid);
                if (user === null) {
                    user = this._roster.createUser(jid);
                }

                result.push(user);
            }
        }

        return result;
    };

    /**
     * Parse a status until and returns a proper date object.
     * Date format should be in ISO 8601 format (2013-06-27T07:40Z)
     *
     * @param date {String}
     * @returns {Date|null} Null when date is in incompatible format.
     * @private
     */
    PresencePlugin.prototype._parseUntilISODate = function(date) {
        var result = new Date(date);
        if (result) {
            return result;
        }

        return null;
    };

    /**
     * Parse a status until and returns a proper date object.
     * Date format should be: dd/mm/yyyy hh:mm (19/06/2013 13:50)
     *
     * @param dateString
     * @returns {Date|null} Null when date is in incompatible format.
     * @private
     */
    PresencePlugin.prototype._parseUntilDate = function(dateString) {
        var regex = /(\d{1,2})\/(\d{1,2})\/(\d{4})\s(\d{1,2}):(\d{1,2})/;
        var matches = regex.exec(dateString);
        if (matches === null) {
            return this._parseUntilISODate(dateString);
        }

        var year = parseInt(matches[3]);
        var month = parseInt(matches[2]) - 1; // Careful, month starts at 0!
        var day = parseInt(matches[1]);
        var hours = parseInt(matches[4]);
        var minutes = parseInt(matches[5]);
        var seconds = 0;

        return new Date(Date.UTC(year, month, day, hours, minutes, seconds));
    };

    /**
     * Return a Date object as a String, using the ISO 8601 format
     *
     * @param {Date} date
     * @returns {string}
     * @private
     */
    PresencePlugin.prototype._formatUntilDate = function(date) {
        function pad(n) { return n < 10 ? '0' + n : n }
        return date.getUTCFullYear() + '-'
            + pad(date.getUTCMonth() + 1) + '-'
            + pad(date.getUTCDate()) + 'T'
            + pad(date.getUTCHours()) + ':'
            + pad(date.getUTCMinutes()) + ':'
            + pad(date.getUTCSeconds()) + 'Z';
    };

    WTAPI.addPlugin("presence", PresencePlugin);
}(WTAPI));