diff --git a/bower.json b/bower.json index 594e1c7bf..efac268a7 100644 --- a/bower.json +++ b/bower.json @@ -16,7 +16,6 @@ "backbone.browserStorage": "*", "backbone.overview": "*", "strophe": "~1.1.3", - "strophe.roster": "https://raw.github.com/strophe/strophejs-plugins/b1f364eb6e854ffe844c57add38e885cfeb9b498/roster/strophe.roster.js", "strophe.muc": "https://raw.githubusercontent.com/strophe/strophejs-plugins/master/muc/strophe.muc.js", "otr": "0.2.12", "crypto-js-evanvosberg": "~3.1.2", @@ -30,7 +29,7 @@ "bootstrapJS": "https://raw.githubusercontent.com/jcbrand/bootstrap/7d96a5f60d26c67b5348b270a775518b96a702c8/dist/js/bootstrap.js", "fontawesome": "~4.1.0", "typeahead.js": "https://raw.githubusercontent.com/jcbrand/typeahead.js/eedfb10505dd3a20123d1fafc07c1352d83f0ab3/dist/typeahead.jquery.js", - "strophejs-plugins": "~0.0.4" + "strophejs-plugins": "git@github.com:strophe/strophejs-plugins.git#a56421ff4ecf0807113ab48c46728715597df599" }, "exportsOverride": {} } diff --git a/main.js b/main.js index c0e54fe66..0946c251f 100644 --- a/main.js +++ b/main.js @@ -17,7 +17,7 @@ config = { "strophe": "components/strophe/strophe", "strophe.disco": "components/strophejs-plugins/disco/strophe.disco", "strophe.muc": "components/strophe.muc/index", - "strophe.roster": "components/strophe.roster/index", + "strophe.roster": "src/strophe.roster", "strophe.vcard": "components/strophejs-plugins/vcard/strophe.vcard", "text": 'components/requirejs-text/text', "tpl": 'components/requirejs-tpl-jcbrand/tpl', diff --git a/src/strophe.roster.js b/src/strophe.roster.js new file mode 100644 index 000000000..79182d118 --- /dev/null +++ b/src/strophe.roster.js @@ -0,0 +1,447 @@ +/* + Copyright 2010, François de Metz +*/ +/** + * Roster Plugin + * Allow easily roster management + * + * Features + * * Get roster from server + * * handle presence + * * handle roster iq + * * subscribe/unsubscribe + * * authorize/unauthorize + * * roster versioning (xep 237) + */ +Strophe.addConnectionPlugin('roster', +{ + /** Function: init + * Plugin init + * + * Parameters: + * (Strophe.Connection) conn - Strophe connection + */ + init: function(conn) + { + this._connection = conn; + this._callbacks = []; + /** Property: items + * Roster items + * [ + * { + * name : "", + * jid : "", + * subscription : "", + * ask : "", + * groups : ["", ""], + * resources : { + * myresource : { + * show : "", + * status : "", + * priority : "" + * } + * } + * } + * ] + */ + this.items = []; + /** Property: ver + * current roster revision + * always null if server doesn't support xep 237 + */ + this.ver = null; + // Override the connect and attach methods to always add presence and roster handlers. + // They are removed when the connection disconnects, so must be added on connection. + var oldCallback, roster = this, _connect = conn.connect, _attach = conn.attach; + var newCallback = function(status) + { + if (status == Strophe.Status.ATTACHED || status == Strophe.Status.CONNECTED) + { + try + { + // Presence subscription + conn.addHandler(roster._onReceivePresence.bind(roster), null, 'presence', null, null, null); + conn.addHandler(roster._onReceiveIQ.bind(roster), Strophe.NS.ROSTER, 'iq', "set", null, null); + } + catch (e) + { + Strophe.error(e); + } + } + if (typeof oldCallback === "function") { + oldCallback.apply(this, arguments); + } + }; + conn.connect = function(jid, pass, callback, wait, hold) + { + oldCallback = callback; + if (typeof jid == "undefined") + jid = null; + if (typeof pass == "undefined") + pass = null; + callback = newCallback; + _connect.apply(conn, [jid, pass, callback, wait, hold]); + }; + conn.attach = function(jid, sid, rid, callback, wait, hold, wind) + { + oldCallback = callback; + if (typeof jid == "undefined") + jid = null; + if (typeof sid == "undefined") + sid = null; + if (typeof rid == "undefined") + rid = null; + callback = newCallback; + _attach.apply(conn, [jid, sid, rid, callback, wait, hold, wind]); + }; + + Strophe.addNamespace('ROSTER_VER', 'urn:xmpp:features:rosterver'); + Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick'); + }, + /** Function: supportVersioning + * return true if roster versioning is enabled on server + */ + supportVersioning: function() + { + return (this._connection.features && this._connection.features.getElementsByTagName('ver').length > 0); + }, + /** Function: get + * Get Roster on server + * + * Parameters: + * (Function) userCallback - callback on roster result + * (String) ver - current rev of roster + * (only used if roster versioning is enabled) + * (Array) items - initial items of ver + * (only used if roster versioning is enabled) + * In browser context you can use sessionStorage + * to store your roster in json (JSON.stringify()) + */ + get: function(userCallback, ver, items) + { + var attrs = {xmlns: Strophe.NS.ROSTER}; + if (this.supportVersioning()) + { + // empty rev because i want an rev attribute in the result + attrs.ver = ver || ''; + this.items = items || []; + } + var iq = $iq({type: 'get', 'id' : this._connection.getUniqueId('roster')}).c('query', attrs); + return this._connection.sendIQ(iq, + this._onReceiveRosterSuccess.bind(this, userCallback), + this._onReceiveRosterError.bind(this, userCallback)); + }, + /** Function: registerCallback + * register callback on roster (presence and iq) + * + * Parameters: + * (Function) call_back + */ + registerCallback: function(call_back) + { + this._callbacks.push(call_back); + }, + /** Function: findItem + * Find item by JID + * + * Parameters: + * (String) jid + */ + findItem : function(jid) + { + try { + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] && this.items[i].jid == jid) + { + return this.items[i]; + } + } + } catch (e) + { + Strophe.error(e); + } + return false; + }, + /** Function: removeItem + * Remove item by JID + * + * Parameters: + * (String) jid + */ + removeItem : function(jid) + { + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] && this.items[i].jid == jid) + { + this.items.splice(i, 1); + return true; + } + } + return false; + }, + /** Function: subscribe + * Subscribe presence + * + * Parameters: + * (String) jid + * (String) message (optional) + * (String) nick (optional) + */ + subscribe: function(jid, message, nick) { + var pres = $pres({to: jid, type: "subscribe"}); + if (message && message !== "") { + pres.c("status").t(message).up(); + } + if (nick && nick !== "") { + pres.c('nick', {'xmlns': Strophe.NS.NICK}).t(nick).up(); + } + this._connection.send(pres); + }, + /** Function: unsubscribe + * Unsubscribe presence + * + * Parameters: + * (String) jid + * (String) message + */ + unsubscribe: function(jid, message) + { + var pres = $pres({to: jid, type: "unsubscribe"}); + if (message && message !== "") + pres.c("status").t(message); + this._connection.send(pres); + }, + /** Function: authorize + * Authorize presence subscription + * + * Parameters: + * (String) jid + * (String) message + */ + authorize: function(jid, message) + { + var pres = $pres({to: jid, type: "subscribed"}); + if (message && message !== "") + pres.c("status").t(message); + this._connection.send(pres); + }, + /** Function: unauthorize + * Unauthorize presence subscription + * + * Parameters: + * (String) jid + * (String) message + */ + unauthorize: function(jid, message) + { + var pres = $pres({to: jid, type: "unsubscribed"}); + if (message && message !== "") + pres.c("status").t(message); + this._connection.send(pres); + }, + /** Function: add + * Add roster item + * + * Parameters: + * (String) jid - item jid + * (String) name - name + * (Array) groups + * (Function) call_back + */ + add: function(jid, name, groups, call_back) + { + var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: jid, + name: name}); + for (var i = 0; i < groups.length; i++) + { + iq.c('group').t(groups[i]).up(); + } + this._connection.sendIQ(iq, call_back, call_back); + }, + /** Function: update + * Update roster item + * + * Parameters: + * (String) jid - item jid + * (String) name - name + * (Array) groups + * (Function) call_back + */ + update: function(jid, name, groups, call_back) + { + var item = this.findItem(jid); + if (!item) + { + throw "item not found"; + } + var newName = name || item.name; + var newGroups = groups || item.groups; + var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: item.jid, + name: newName}); + for (var i = 0; i < newGroups.length; i++) + { + iq.c('group').t(newGroups[i]).up(); + } + return this._connection.sendIQ(iq, call_back, call_back); + }, + /** Function: remove + * Remove roster item + * + * Parameters: + * (String) jid - item jid + * (Function) call_back + */ + remove: function(jid, call_back) + { + var item = this.findItem(jid); + if (!item) + { + throw "item not found"; + } + var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: item.jid, + subscription: "remove"}); + this._connection.sendIQ(iq, call_back, call_back); + }, + /** PrivateFunction: _onReceiveRosterSuccess + * + */ + _onReceiveRosterSuccess: function(userCallback, stanza) + { + this._updateItems(stanza); + if (typeof userCallback === "function") { + userCallback(this.items); + } + }, + /** PrivateFunction: _onReceiveRosterError + * + */ + _onReceiveRosterError: function(userCallback, stanza) + { + userCallback(this.items); + }, + /** PrivateFunction: _onReceivePresence + * Handle presence + */ + _onReceivePresence : function(presence) + { + // TODO: from is optional + var jid = presence.getAttribute('from'); + var from = Strophe.getBareJidFromJid(jid); + var item = this.findItem(from); + // not in roster + if (!item) + { + return true; + } + var type = presence.getAttribute('type'); + if (type == 'unavailable') + { + delete item.resources[Strophe.getResourceFromJid(jid)]; + } + else if (!type) + { + // TODO: add timestamp + item.resources[Strophe.getResourceFromJid(jid)] = { + show : (presence.getElementsByTagName('show').length !== 0) ? Strophe.getText(presence.getElementsByTagName('show')[0]) : "", + status : (presence.getElementsByTagName('status').length !== 0) ? Strophe.getText(presence.getElementsByTagName('status')[0]) : "", + priority : (presence.getElementsByTagName('priority').length !== 0) ? Strophe.getText(presence.getElementsByTagName('priority')[0]) : "" + }; + } + else + { + // Stanza is not a presence notification. (It's probably a subscription type stanza.) + return true; + } + this._call_backs(this.items, item); + return true; + }, + /** PrivateFunction: _call_backs + * + */ + _call_backs : function(items, item) + { + for (var i = 0; i < this._callbacks.length; i++) // [].forEach my love ... + { + this._callbacks[i](items, item); + } + }, + /** PrivateFunction: _onReceiveIQ + * Handle roster push. + */ + _onReceiveIQ : function(iq) + { + var id = iq.getAttribute('id'); + var from = iq.getAttribute('from'); + // Receiving client MUST ignore stanza unless it has no from or from = user's JID. + if (from && from !== "" && from != this._connection.jid && from != Strophe.getBareJidFromJid(this._connection.jid)) + return true; + var iqresult = $iq({type: 'result', id: id, from: this._connection.jid}); + this._connection.send(iqresult); + this._updateItems(iq); + return true; + }, + /** PrivateFunction: _updateItems + * Update items from iq + */ + _updateItems : function(iq) + { + var query = iq.getElementsByTagName('query'); + if (query.length !== 0) + { + this.ver = query.item(0).getAttribute('ver'); + var self = this; + Strophe.forEachChild(query.item(0), 'item', + function (item) + { + self._updateItem(item); + } + ); + } + this._call_backs(this.items); + }, + /** PrivateFunction: _updateItem + * Update internal representation of roster item + */ + _updateItem : function(item) + { + var jid = item.getAttribute("jid"); + var name = item.getAttribute("name"); + var subscription = item.getAttribute("subscription"); + var ask = item.getAttribute("ask"); + var groups = []; + Strophe.forEachChild(item, 'group', + function(group) + { + groups.push(Strophe.getText(group)); + } + ); + + if (subscription == "remove") + { + this.removeItem(jid); + return; + } + + item = this.findItem(jid); + if (!item) + { + this.items.push({ + name : name, + jid : jid, + subscription : subscription, + ask : ask, + groups : groups, + resources : {} + }); + } + else + { + item.name = name; + item.subscription = subscription; + item.ask = ask; + item.groups = groups; + } + } +});