Source: plugins/wtapi.presence.js

import WTAPI from '../wtapi'
import {JSJaCPresence, JSJaCIQ} from '../jsjac';

/**
 * A plugin that provides Presence functionality
 *
 * @version 1.0.0
 */

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';
};

export default PresencePlugin;