/**
* 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));