strophe.roster.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. /*
  2. Copyright 2010, François de Metz <francois@2metz.fr>
  3. */
  4. /**
  5. * Roster Plugin
  6. * Allow easily roster management
  7. *
  8. * Features
  9. * * Get roster from server
  10. * * handle presence
  11. * * handle roster iq
  12. * * subscribe/unsubscribe
  13. * * authorize/unauthorize
  14. * * roster versioning (xep 237)
  15. */
  16. // AMD/global registrations
  17. (function (root, factory) {
  18. if (typeof define === 'function' && define.amd) {
  19. define([
  20. "Libraries/strophe"
  21. ], function () {
  22. return factory(jQuery, console);
  23. }
  24. );
  25. }
  26. }(this, function ($, console) {
  27. Strophe.addConnectionPlugin('roster',
  28. {
  29. _connection: null,
  30. _callbacks : [],
  31. /** Property: items
  32. * Roster items
  33. * [
  34. * {
  35. * name : "",
  36. * jid : "",
  37. * subscription : "",
  38. * ask : "",
  39. * groups : ["", ""],
  40. * resources : {
  41. * myresource : {
  42. * show : "",
  43. * status : "",
  44. * priority : ""
  45. * }
  46. * }
  47. * }
  48. * ]
  49. */
  50. items : [],
  51. /** Property: ver
  52. * current roster revision
  53. * always null if server doesn't support xep 237
  54. */
  55. ver : null,
  56. /** Function: init
  57. * Plugin init
  58. *
  59. * Parameters:
  60. * (Strophe.Connection) conn - Strophe connection
  61. */
  62. init: function(conn)
  63. {
  64. this._connection = conn;
  65. this.items = [];
  66. // Override the connect and attach methods to always add presence and roster handlers.
  67. // They are removed when the connection disconnects, so must be added on connection.
  68. var oldCallback, roster = this, _connect = conn.connect, _attach = conn.attach;
  69. var newCallback = function(status)
  70. {
  71. if (status == Strophe.Status.ATTACHED || status == Strophe.Status.CONNECTED)
  72. {
  73. try
  74. {
  75. // Presence subscription
  76. conn.addHandler(roster._onReceivePresence.bind(roster), null, 'presence', null, null, null);
  77. conn.addHandler(roster._onReceiveIQ.bind(roster), Strophe.NS.ROSTER, 'iq', "set", null, null);
  78. }
  79. catch (e)
  80. {
  81. Strophe.error(e);
  82. }
  83. }
  84. if (oldCallback !== null)
  85. oldCallback.apply(this, arguments);
  86. };
  87. conn.connect = function(jid, pass, callback, wait, hold)
  88. {
  89. oldCallback = callback;
  90. if (typeof arguments[0] == "undefined")
  91. arguments[0] = null;
  92. if (typeof arguments[1] == "undefined")
  93. arguments[1] = null;
  94. arguments[2] = newCallback;
  95. _connect.apply(conn, arguments);
  96. };
  97. conn.attach = function(jid, sid, rid, callback, wait, hold, wind)
  98. {
  99. oldCallback = callback;
  100. if (typeof arguments[0] == "undefined")
  101. arguments[0] = null;
  102. if (typeof arguments[1] == "undefined")
  103. arguments[1] = null;
  104. if (typeof arguments[2] == "undefined")
  105. arguments[2] = null;
  106. arguments[3] = newCallback;
  107. _attach.apply(conn, arguments);
  108. };
  109. Strophe.addNamespace('ROSTER_VER', 'urn:xmpp:features:rosterver');
  110. },
  111. /** Function: supportVersioning
  112. * return true if roster versioning is enabled on server
  113. */
  114. supportVersioning: function()
  115. {
  116. return (this._connection.features && this._connection.features.getElementsByTagName('ver').length > 0);
  117. },
  118. /** Function: get
  119. * Get Roster on server
  120. *
  121. * Parameters:
  122. * (Function) userCallback - callback on roster result
  123. * (String) ver - current rev of roster
  124. * (only used if roster versioning is enabled)
  125. * (Array) items - initial items of ver
  126. * (only used if roster versioning is enabled)
  127. * In browser context you can use sessionStorage
  128. * to store your roster in json (JSON.stringify())
  129. */
  130. get: function(userCallback, ver, items)
  131. {
  132. var attrs = {xmlns: Strophe.NS.ROSTER};
  133. this.items = [];
  134. if (this.supportVersioning())
  135. {
  136. // empty rev because i want an rev attribute in the result
  137. attrs.ver = ver || '';
  138. this.items = items || [];
  139. }
  140. var iq = $iq({type: 'get', 'id' : this._connection.getUniqueId('roster')}).c('query', attrs);
  141. this._connection.sendIQ(iq,
  142. this._onReceiveRosterSuccess.bind(this, userCallback),
  143. this._onReceiveRosterError.bind(this, userCallback));
  144. },
  145. /** Function: registerCallback
  146. * register callback on roster (presence and iq)
  147. *
  148. * Parameters:
  149. * (Function) call_back
  150. */
  151. registerCallback: function(call_back)
  152. {
  153. this._callbacks.push(call_back);
  154. },
  155. /** Function: findItem
  156. * Find item by JID
  157. *
  158. * Parameters:
  159. * (String) jid
  160. */
  161. findItem : function(jid)
  162. {
  163. for (var i = 0; i < this.items.length; i++)
  164. {
  165. if (this.items[i] && this.items[i].jid == jid)
  166. {
  167. return this.items[i];
  168. }
  169. }
  170. return false;
  171. },
  172. /** Function: removeItem
  173. * Remove item by JID
  174. *
  175. * Parameters:
  176. * (String) jid
  177. */
  178. removeItem : function(jid)
  179. {
  180. for (var i = 0; i < this.items.length; i++)
  181. {
  182. if (this.items[i] && this.items[i].jid == jid)
  183. {
  184. this.items.splice(i, 1);
  185. return true;
  186. }
  187. }
  188. return false;
  189. },
  190. /** Function: subscribe
  191. * Subscribe presence
  192. *
  193. * Parameters:
  194. * (String) jid
  195. * (String) message
  196. */
  197. subscribe: function(jid, message)
  198. {
  199. var pres = $pres({to: jid, type: "subscribe"});
  200. if (message && message != "")
  201. pres.c("status").t(message);
  202. this._connection.send(pres);
  203. },
  204. /** Function: unsubscribe
  205. * Unsubscribe presence
  206. *
  207. * Parameters:
  208. * (String) jid
  209. * (String) message
  210. */
  211. unsubscribe: function(jid, message)
  212. {
  213. var pres = $pres({to: jid, type: "unsubscribe"});
  214. if (message && message != "")
  215. pres.c("status").t(message);
  216. this._connection.send(pres);
  217. },
  218. /** Function: authorize
  219. * Authorize presence subscription
  220. *
  221. * Parameters:
  222. * (String) jid
  223. * (String) message
  224. */
  225. authorize: function(jid, message)
  226. {
  227. var pres = $pres({to: jid, type: "subscribed"});
  228. if (message && message != "")
  229. pres.c("status").t(message);
  230. this._connection.send(pres);
  231. },
  232. /** Function: unauthorize
  233. * Unauthorize presence subscription
  234. *
  235. * Parameters:
  236. * (String) jid
  237. * (String) message
  238. */
  239. unauthorize: function(jid, message)
  240. {
  241. var pres = $pres({to: jid, type: "unsubscribed"});
  242. if (message && message != "")
  243. pres.c("status").t(message);
  244. this._connection.send(pres);
  245. },
  246. /** Function: add
  247. * Add roster item
  248. *
  249. * Parameters:
  250. * (String) jid - item jid
  251. * (String) name - name
  252. * (Array) groups
  253. * (Function) call_back
  254. */
  255. add: function(jid, name, groups, call_back)
  256. {
  257. var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: jid,
  258. name: name});
  259. for (var i = 0; i < groups.length; i++)
  260. {
  261. iq.c('group').t(groups[i]).up();
  262. }
  263. this._connection.sendIQ(iq, call_back, call_back);
  264. },
  265. /** Function: update
  266. * Update roster item
  267. *
  268. * Parameters:
  269. * (String) jid - item jid
  270. * (String) name - name
  271. * (Array) groups
  272. * (Function) call_back
  273. */
  274. update: function(jid, name, groups, call_back)
  275. {
  276. var item = this.findItem(jid);
  277. if (!item)
  278. {
  279. throw "item not found";
  280. }
  281. var newName = name || item.name;
  282. var newGroups = groups || item.groups;
  283. var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: item.jid,
  284. name: newName});
  285. for (var i = 0; i < newGroups.length; i++)
  286. {
  287. iq.c('group').t(newGroups[i]).up();
  288. }
  289. this._connection.sendIQ(iq, call_back, call_back);
  290. },
  291. /** Function: remove
  292. * Remove roster item
  293. *
  294. * Parameters:
  295. * (String) jid - item jid
  296. * (Function) call_back
  297. */
  298. remove: function(jid, call_back)
  299. {
  300. var item = this.findItem(jid);
  301. if (!item)
  302. {
  303. throw "item not found";
  304. }
  305. var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: item.jid,
  306. subscription: "remove"});
  307. this._connection.sendIQ(iq, call_back, call_back);
  308. },
  309. /** PrivateFunction: _onReceiveRosterSuccess
  310. *
  311. */
  312. _onReceiveRosterSuccess: function(userCallback, stanza)
  313. {
  314. this._updateItems(stanza);
  315. userCallback(this.items);
  316. },
  317. /** PrivateFunction: _onReceiveRosterError
  318. *
  319. */
  320. _onReceiveRosterError: function(userCallback, stanza)
  321. {
  322. userCallback(this.items);
  323. },
  324. /** PrivateFunction: _onReceivePresence
  325. * Handle presence
  326. */
  327. _onReceivePresence : function(presence)
  328. {
  329. // TODO: from is optional
  330. var jid = presence.getAttribute('from');
  331. var from = Strophe.getBareJidFromJid(jid);
  332. var item = this.findItem(from);
  333. // not in roster
  334. if (!item)
  335. {
  336. return true;
  337. }
  338. var type = presence.getAttribute('type');
  339. if (type == 'unavailable')
  340. {
  341. delete item.resources[Strophe.getResourceFromJid(jid)];
  342. }
  343. else if (!type)
  344. {
  345. // TODO: add timestamp
  346. item.resources[Strophe.getResourceFromJid(jid)] = {
  347. show : (presence.getElementsByTagName('show').length != 0) ? Strophe.getText(presence.getElementsByTagName('show')[0]) : "",
  348. status : (presence.getElementsByTagName('status').length != 0) ? Strophe.getText(presence.getElementsByTagName('status')[0]) : "",
  349. priority : (presence.getElementsByTagName('priority').length != 0) ? Strophe.getText(presence.getElementsByTagName('priority')[0]) : ""
  350. };
  351. }
  352. else
  353. {
  354. // Stanza is not a presence notification. (It's probably a subscription type stanza.)
  355. return true;
  356. }
  357. this._call_backs(this.items, item);
  358. return true;
  359. },
  360. /** PrivateFunction: _call_backs
  361. *
  362. */
  363. _call_backs : function(items, item)
  364. {
  365. for (var i = 0; i < this._callbacks.length; i++) // [].forEach my love ...
  366. {
  367. this._callbacks[i](items, item);
  368. }
  369. },
  370. /** PrivateFunction: _onReceiveIQ
  371. * Handle roster push.
  372. */
  373. _onReceiveIQ : function(iq)
  374. {
  375. var id = iq.getAttribute('id');
  376. var from = iq.getAttribute('from');
  377. // Receiving client MUST ignore stanza unless it has no from or from = user's JID.
  378. if (from && from != "" && from != this._connection.jid && from != Strophe.getBareJidFromJid(this._connection.jid))
  379. return true;
  380. var iqresult = $iq({type: 'result', id: id, from: this._connection.jid});
  381. this._connection.send(iqresult);
  382. this._updateItems(iq);
  383. return true;
  384. },
  385. /** PrivateFunction: _updateItems
  386. * Update items from iq
  387. */
  388. _updateItems : function(iq)
  389. {
  390. var query = iq.getElementsByTagName('query');
  391. if (query.length != 0)
  392. {
  393. this.ver = query.item(0).getAttribute('ver');
  394. var self = this;
  395. Strophe.forEachChild(query.item(0), 'item',
  396. function (item)
  397. {
  398. self._updateItem(item);
  399. }
  400. );
  401. }
  402. this._call_backs(this.items);
  403. },
  404. /** PrivateFunction: _updateItem
  405. * Update internal representation of roster item
  406. */
  407. _updateItem : function(item)
  408. {
  409. var jid = item.getAttribute("jid");
  410. var name = item.getAttribute("name");
  411. var subscription = item.getAttribute("subscription");
  412. var ask = item.getAttribute("ask");
  413. var groups = [];
  414. Strophe.forEachChild(item, 'group',
  415. function(group)
  416. {
  417. groups.push(Strophe.getText(group));
  418. }
  419. );
  420. if (subscription == "remove")
  421. {
  422. this.removeItem(jid);
  423. return;
  424. }
  425. var item = this.findItem(jid);
  426. if (!item)
  427. {
  428. this.items.push({
  429. name : name,
  430. jid : jid,
  431. subscription : subscription,
  432. ask : ask,
  433. groups : groups,
  434. resources : {}
  435. });
  436. }
  437. else
  438. {
  439. item.name = name;
  440. item.subscription = subscription;
  441. item.ask = ask;
  442. item.groups = groups;
  443. }
  444. }
  445. });
  446. }));