strophe.roster.js 13 KB

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