mam.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. (function (root, factory) {
  2. define(["jasmine", "mock", "test-utils"], factory);
  3. } (this, function (jasmine, mock, test_utils) {
  4. "use strict";
  5. const _ = converse.env._;
  6. const Backbone = converse.env.Backbone;
  7. const Strophe = converse.env.Strophe;
  8. const $iq = converse.env.$iq;
  9. const $msg = converse.env.$msg;
  10. const moment = converse.env.moment;
  11. const u = converse.env.utils;
  12. // See: https://xmpp.org/rfcs/rfc3921.html
  13. describe("Message Archive Management", function () {
  14. // Implement the protocol defined in https://xmpp.org/extensions/xep-0313.html#config
  15. describe("Archived Messages", function () {
  16. it("aren't shown as duplicates by comparing their stanza id and archive id",
  17. mock.initConverse(
  18. null, ['discoInitialized'], {},
  19. async function (done, _converse) {
  20. await test_utils.openAndEnterChatRoom(_converse, 'trek-radio', 'conference.lightwitch.org', 'jcbrand');
  21. const view = _converse.chatboxviews.get('trek-radio@conference.lightwitch.org');
  22. let stanza = u.toStanza(
  23. `<message xmlns="jabber:client" to="jcbrand@lightwitch.org/converse.js-73057452" type="groupchat" from="trek-radio@conference.lightwitch.org/comndrdukath#0805 (STO)">
  24. <body>negan</body>
  25. <stanza-id xmlns="urn:xmpp:sid:0" id="45fbbf2a-1059-479d-9283-c8effaf05621" by="trek-radio@conference.lightwitch.org"/>
  26. </message>`);
  27. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  28. await test_utils.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
  29. // Not sure whether such a race-condition might pose a problem
  30. // in "real-world" situations.
  31. stanza = u.toStanza(
  32. `<message xmlns="jabber:client"
  33. to="jcbrand@lightwitch.org/converse.js-73057452"
  34. from="trek-radio@conference.lightwitch.org">
  35. <result xmlns="urn:xmpp:mam:2" queryid="82d9db27-6cf8-4787-8c2c-5a560263d823" id="45fbbf2a-1059-479d-9283-c8effaf05621">
  36. <forwarded xmlns="urn:xmpp:forward:0">
  37. <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:17:23Z"/>
  38. <message from="trek-radio@conference.lightwitch.org/comndrdukath#0805 (STO)" type="groupchat">
  39. <body>negan</body>
  40. </message>
  41. </forwarded>
  42. </result>
  43. </message>`);
  44. spyOn(view.model, 'hasDuplicateArchiveID').and.callThrough();
  45. view.model.onMessage(stanza);
  46. await test_utils.waitUntil(() => view.model.hasDuplicateArchiveID.calls.count());
  47. expect(view.model.hasDuplicateArchiveID.calls.count()).toBe(1);
  48. const result = await view.model.hasDuplicateArchiveID.calls.all()[0].returnValue
  49. expect(result).toBe(true);
  50. expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
  51. done();
  52. }));
  53. it("aren't shown as duplicates by comparing only their archive id",
  54. mock.initConverse(
  55. null, ['discoInitialized'], {},
  56. async function (done, _converse) {
  57. await test_utils.openAndEnterChatRoom(_converse, 'discuss', 'conference.conversejs.org', 'dummy');
  58. const view = _converse.chatboxviews.get('discuss@conference.conversejs.org');
  59. let stanza = u.toStanza(
  60. `<message xmlns="jabber:client" to="dummy@localhost/resource" from="discuss@conference.conversejs.org">
  61. <result xmlns="urn:xmpp:mam:2" queryid="06fea9ca-97c9-48c4-8583-009ff54ea2e8" id="7a9fde91-4387-4bf8-b5d3-978dab8f6bf3">
  62. <forwarded xmlns="urn:xmpp:forward:0">
  63. <delay xmlns="urn:xmpp:delay" stamp="2018-12-05T04:53:12Z"/>
  64. <message xmlns="jabber:client" to="discuss@conference.conversejs.org" type="groupchat" xml:lang="en" from="discuss@conference.conversejs.org/prezel">
  65. <body>looks like omemo fails completely with "bundle is undefined" when there is a device in the devicelist that has no keys published</body>
  66. <x xmlns="http://jabber.org/protocol/muc#user">
  67. <item affiliation="none" jid="prezel@blubber.im" role="participant"/>
  68. </x>
  69. </message>
  70. </forwarded>
  71. </result>
  72. </message>`);
  73. view.model.onMessage(stanza);
  74. await test_utils.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
  75. expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
  76. stanza = u.toStanza(
  77. `<message xmlns="jabber:client" to="dummy@localhost/resource" from="discuss@conference.conversejs.org">
  78. <result xmlns="urn:xmpp:mam:2" queryid="06fea9ca-97c9-48c4-8583-009ff54ea2e8" id="7a9fde91-4387-4bf8-b5d3-978dab8f6bf3">
  79. <forwarded xmlns="urn:xmpp:forward:0">
  80. <delay xmlns="urn:xmpp:delay" stamp="2018-12-05T04:53:12Z"/>
  81. <message xmlns="jabber:client" to="discuss@conference.conversejs.org" type="groupchat" xml:lang="en" from="discuss@conference.conversejs.org/prezel">
  82. <body>looks like omemo fails completely with "bundle is undefined" when there is a device in the devicelist that has no keys published</body>
  83. <x xmlns="http://jabber.org/protocol/muc#user">
  84. <item affiliation="none" jid="prezel@blubber.im" role="participant"/>
  85. </x>
  86. </message>
  87. </forwarded>
  88. </result>
  89. </message>`);
  90. spyOn(view.model, 'hasDuplicateArchiveID').and.callThrough();
  91. view.model.onMessage(stanza);
  92. await test_utils.waitUntil(() => view.model.hasDuplicateArchiveID.calls.count());
  93. expect(view.model.hasDuplicateArchiveID.calls.count()).toBe(1);
  94. const result = await view.model.hasDuplicateArchiveID.calls.all()[0].returnValue
  95. expect(result).toBe(true);
  96. expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
  97. done();
  98. }))
  99. });
  100. describe("The archive.query API", function () {
  101. it("can be used to query for all archived messages",
  102. mock.initConverse(
  103. null, ['discoInitialized'], {},
  104. function (done, _converse) {
  105. let sent_stanza, IQ_id;
  106. const sendIQ = _converse.connection.sendIQ;
  107. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  108. sent_stanza = iq;
  109. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  110. });
  111. if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
  112. _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
  113. }
  114. _converse.api.archive.query();
  115. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  116. expect(sent_stanza.toString()).toBe(
  117. `<iq id="${IQ_id}" type="set" xmlns="jabber:client"><query queryid="${queryid}" xmlns="urn:xmpp:mam:2"/></iq>`);
  118. done();
  119. }));
  120. it("can be used to query for all messages to/from a particular JID",
  121. mock.initConverse(
  122. null, [], {},
  123. async function (done, _converse) {
  124. const entity = await _converse.api.disco.entities.get(_converse.domain);
  125. if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
  126. _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
  127. }
  128. let sent_stanza, IQ_id;
  129. const sendIQ = _converse.connection.sendIQ;
  130. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  131. sent_stanza = iq;
  132. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  133. });
  134. _converse.api.archive.query({'with':'juliet@capulet.lit'});
  135. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  136. expect(sent_stanza.toString()).toBe(
  137. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  138. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  139. `<x type="submit" xmlns="jabber:x:data">`+
  140. `<field type="hidden" var="FORM_TYPE">`+
  141. `<value>urn:xmpp:mam:2</value>`+
  142. `</field>`+
  143. `<field var="with">`+
  144. `<value>juliet@capulet.lit</value>`+
  145. `</field>`+
  146. `</x>`+
  147. `</query>`+
  148. `</iq>`);
  149. done();
  150. }));
  151. it("can be used to query for archived messages from a chat room",
  152. mock.initConverse(
  153. null, [], {},
  154. async function (done, _converse) {
  155. const entity = await _converse.api.disco.entities.get(_converse.domain);
  156. if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
  157. _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
  158. }
  159. let sent_stanza, IQ_id;
  160. const sendIQ = _converse.connection.sendIQ;
  161. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  162. sent_stanza = iq;
  163. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  164. });
  165. const callback = jasmine.createSpy('callback');
  166. _converse.api.archive.query({'with': 'coven@chat.shakespeare.lit', 'groupchat': true}, callback);
  167. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  168. expect(sent_stanza.toString()).toBe(
  169. `<iq id="${IQ_id}" to="coven@chat.shakespeare.lit" type="set" xmlns="jabber:client">`+
  170. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  171. `<x type="submit" xmlns="jabber:x:data">`+
  172. `<field type="hidden" var="FORM_TYPE">`+
  173. `<value>urn:xmpp:mam:2</value>`+
  174. `</field>`+
  175. `</x>`+
  176. `</query>`+
  177. `</iq>`);
  178. done();
  179. }));
  180. it("checks whether returned MAM messages from a MUC room are from the right JID",
  181. mock.initConverse(
  182. null, [], {},
  183. async function (done, _converse) {
  184. const entity = await _converse.api.disco.entities.get(_converse.domain);
  185. if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
  186. _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
  187. }
  188. let sent_stanza, IQ_id;
  189. const sendIQ = _converse.connection.sendIQ;
  190. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  191. sent_stanza = iq;
  192. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  193. });
  194. const callback = jasmine.createSpy('callback');
  195. _converse.api.archive.query({'with': 'coven@chat.shakespear.lit', 'groupchat': true, 'max':'10'}, callback);
  196. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  197. /* <message id='iasd207' from='coven@chat.shakespeare.lit' to='hag66@shakespeare.lit/pda'>
  198. * <result xmlns='urn:xmpp:mam:2' queryid='g27' id='34482-21985-73620'>
  199. * <forwarded xmlns='urn:xmpp:forward:0'>
  200. * <delay xmlns='urn:xmpp:delay' stamp='2002-10-13T23:58:37Z'/>
  201. * <message xmlns="jabber:client"
  202. * from='coven@chat.shakespeare.lit/firstwitch'
  203. * id='162BEBB1-F6DB-4D9A-9BD8-CFDCC801A0B2'
  204. * type='groupchat'>
  205. * <body>Thrice the brinded cat hath mew'd.</body>
  206. * <x xmlns='http://jabber.org/protocol/muc#user'>
  207. * <item affiliation='none'
  208. * jid='witch1@shakespeare.lit'
  209. * role='participant' />
  210. * </x>
  211. * </message>
  212. * </forwarded>
  213. * </result>
  214. * </message>
  215. */
  216. const msg1 = $msg({'id':'iasd207', 'from': 'other@chat.shakespear.lit', 'to': 'dummy@localhost'})
  217. .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'34482-21985-73620'})
  218. .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
  219. .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
  220. .c('message', {
  221. 'xmlns':'jabber:client',
  222. 'to':'dummy@localhost',
  223. 'id':'162BEBB1-F6DB-4D9A-9BD8-CFDCC801A0B2',
  224. 'from':'coven@chat.shakespeare.lit/firstwitch',
  225. 'type':'groupchat' })
  226. .c('body').t("Thrice the brinded cat hath mew'd.");
  227. _converse.connection._dataRecv(test_utils.createRequest(msg1));
  228. /* Send an <iq> stanza to indicate the end of the result set.
  229. *
  230. * <iq type='result' id='juliet1'>
  231. * <fin xmlns='urn:xmpp:mam:2'>
  232. * <set xmlns='http://jabber.org/protocol/rsm'>
  233. * <first index='0'>28482-98726-73623</first>
  234. * <last>09af3-cc343-b409f</last>
  235. * <count>20</count>
  236. * </set>
  237. * </iq>
  238. */
  239. const stanza = $iq({'type': 'result', 'id': IQ_id})
  240. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  241. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  242. .c('first', {'index': '0'}).t('23452-4534-1').up()
  243. .c('last').t('09af3-cc343-b409f').up()
  244. .c('count').t('16');
  245. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  246. await test_utils.waitUntil(() => callback.calls.count());
  247. expect(callback).toHaveBeenCalled();
  248. const args = callback.calls.argsFor(0);
  249. expect(args[0].length).toBe(0);
  250. done();
  251. }));
  252. it("can be used to query for all messages in a certain timespan",
  253. mock.initConverse(
  254. null, [], {},
  255. async function (done, _converse) {
  256. let sent_stanza, IQ_id;
  257. const sendIQ = _converse.connection.sendIQ;
  258. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  259. sent_stanza = iq;
  260. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  261. });
  262. const entities = await _converse.api.disco.entities.get();
  263. if (!entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
  264. _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
  265. }
  266. const start = '2010-06-07T00:00:00Z';
  267. const end = '2010-07-07T13:23:54Z';
  268. _converse.api.archive.query({
  269. 'start': start,
  270. 'end': end
  271. });
  272. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  273. expect(sent_stanza.toString()).toBe(
  274. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  275. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  276. `<x type="submit" xmlns="jabber:x:data">`+
  277. `<field type="hidden" var="FORM_TYPE">`+
  278. `<value>urn:xmpp:mam:2</value>`+
  279. `</field>`+
  280. `<field var="start">`+
  281. `<value>${moment(start).format()}</value>`+
  282. `</field>`+
  283. `<field var="end">`+
  284. `<value>${moment(end).format()}</value>`+
  285. `</field>`+
  286. `</x>`+
  287. `</query>`+
  288. `</iq>`
  289. );
  290. done();
  291. }));
  292. it("throws a TypeError if an invalid date is provided",
  293. mock.initConverse(
  294. null, [], {},
  295. async function (done, _converse) {
  296. const entity = await _converse.api.disco.entities.get(_converse.domain);
  297. if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
  298. _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
  299. }
  300. expect(_.partial(_converse.api.archive.query, {'start': 'not a real date'})).toThrow(
  301. new TypeError('archive.query: invalid date provided for: start')
  302. );
  303. done();
  304. }));
  305. it("can be used to query for all messages after a certain time",
  306. mock.initConverse(
  307. null, [], {},
  308. async function (done, _converse) {
  309. const entity = await _converse.api.disco.entities.get(_converse.domain);
  310. if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
  311. _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
  312. }
  313. let sent_stanza, IQ_id;
  314. const sendIQ = _converse.connection.sendIQ;
  315. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  316. sent_stanza = iq;
  317. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  318. });
  319. if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
  320. _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
  321. }
  322. const start = '2010-06-07T00:00:00Z';
  323. _converse.api.archive.query({'start': start});
  324. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  325. expect(sent_stanza.toString()).toBe(
  326. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  327. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  328. `<x type="submit" xmlns="jabber:x:data">`+
  329. `<field type="hidden" var="FORM_TYPE">`+
  330. `<value>urn:xmpp:mam:2</value>`+
  331. `</field>`+
  332. `<field var="start">`+
  333. `<value>${moment(start).format()}</value>`+
  334. `</field>`+
  335. `</x>`+
  336. `</query>`+
  337. `</iq>`
  338. );
  339. done();
  340. }));
  341. it("can be used to query for a limited set of results",
  342. mock.initConverse(
  343. null, [], {},
  344. async function (done, _converse) {
  345. const entity = await _converse.api.disco.entities.get(_converse.domain);
  346. if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
  347. _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
  348. }
  349. let sent_stanza, IQ_id;
  350. const sendIQ = _converse.connection.sendIQ;
  351. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  352. sent_stanza = iq;
  353. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  354. });
  355. const start = '2010-06-07T00:00:00Z';
  356. _converse.api.archive.query({'start': start, 'max':10});
  357. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  358. expect(sent_stanza.toString()).toBe(
  359. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  360. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  361. `<x type="submit" xmlns="jabber:x:data">`+
  362. `<field type="hidden" var="FORM_TYPE">`+
  363. `<value>urn:xmpp:mam:2</value>`+
  364. `</field>`+
  365. `<field var="start">`+
  366. `<value>${moment(start).format()}</value>`+
  367. `</field>`+
  368. `</x>`+
  369. `<set xmlns="http://jabber.org/protocol/rsm">`+
  370. `<max>10</max>`+
  371. `</set>`+
  372. `</query>`+
  373. `</iq>`
  374. );
  375. done();
  376. }));
  377. it("can be used to page through results",
  378. mock.initConverse(
  379. null, [], {},
  380. async function (done, _converse) {
  381. const entity = await _converse.api.disco.entities.get(_converse.domain);
  382. if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
  383. _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
  384. }
  385. let sent_stanza, IQ_id;
  386. const sendIQ = _converse.connection.sendIQ;
  387. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  388. sent_stanza = iq;
  389. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  390. });
  391. const start = '2010-06-07T00:00:00Z';
  392. _converse.api.archive.query({
  393. 'start': start,
  394. 'after': '09af3-cc343-b409f',
  395. 'max':10
  396. });
  397. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  398. expect(sent_stanza.toString()).toBe(
  399. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  400. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  401. `<x type="submit" xmlns="jabber:x:data">`+
  402. `<field type="hidden" var="FORM_TYPE">`+
  403. `<value>urn:xmpp:mam:2</value>`+
  404. `</field>`+
  405. `<field var="start">`+
  406. `<value>${moment(start).format()}</value>`+
  407. `</field>`+
  408. `</x>`+
  409. `<set xmlns="http://jabber.org/protocol/rsm">`+
  410. `<max>10</max>`+
  411. `<after>09af3-cc343-b409f</after>`+
  412. `</set>`+
  413. `</query>`+
  414. `</iq>`);
  415. done();
  416. }));
  417. it("accepts \"before\" with an empty string as value to reverse the order",
  418. mock.initConverse(
  419. null, [], {},
  420. async function (done, _converse) {
  421. const entity = await _converse.api.disco.entities.get(_converse.domain);
  422. if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
  423. _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
  424. }
  425. let sent_stanza, IQ_id;
  426. const sendIQ = _converse.connection.sendIQ;
  427. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  428. sent_stanza = iq;
  429. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  430. });
  431. _converse.api.archive.query({'before': '', 'max':10});
  432. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  433. expect(sent_stanza.toString()).toBe(
  434. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  435. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  436. `<x type="submit" xmlns="jabber:x:data">`+
  437. `<field type="hidden" var="FORM_TYPE">`+
  438. `<value>urn:xmpp:mam:2</value>`+
  439. `</field>`+
  440. `</x>`+
  441. `<set xmlns="http://jabber.org/protocol/rsm">`+
  442. `<max>10</max>`+
  443. `<before></before>`+
  444. `</set>`+
  445. `</query>`+
  446. `</iq>`);
  447. done();
  448. }));
  449. it("accepts a Strophe.RSM object for the query options",
  450. mock.initConverse(
  451. null, [], {},
  452. async function (done, _converse) {
  453. const entity = await _converse.api.disco.entities.get(_converse.domain);
  454. if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
  455. _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
  456. }
  457. let sent_stanza, IQ_id;
  458. const sendIQ = _converse.connection.sendIQ;
  459. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  460. sent_stanza = iq;
  461. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  462. });
  463. // Normally the user wouldn't manually make a Strophe.RSM object
  464. // and pass it in. However, in the callback method an RSM object is
  465. // returned which can be reused for easy paging. This test is
  466. // more for that usecase.
  467. const rsm = new Strophe.RSM({'max': '10'});
  468. rsm['with'] = 'romeo@montague.lit'; // eslint-disable-line dot-notation
  469. rsm.start = '2010-06-07T00:00:00Z';
  470. _converse.api.archive.query(rsm);
  471. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  472. expect(sent_stanza.toString()).toBe(
  473. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  474. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  475. `<x type="submit" xmlns="jabber:x:data">`+
  476. `<field type="hidden" var="FORM_TYPE">`+
  477. `<value>urn:xmpp:mam:2</value>`+
  478. `</field>`+
  479. `<field var="with">`+
  480. `<value>romeo@montague.lit</value>`+
  481. `</field>`+
  482. `<field var="start">`+
  483. `<value>${moment(rsm.start).format()}</value>`+
  484. `</field>`+
  485. `</x>`+
  486. `<set xmlns="http://jabber.org/protocol/rsm">`+
  487. `<max>10</max>`+
  488. `</set>`+
  489. `</query>`+
  490. `</iq>`);
  491. done();
  492. }));
  493. it("accepts a callback function, which it passes the messages and a Strophe.RSM object",
  494. mock.initConverse(
  495. null, [], {},
  496. async function (done, _converse) {
  497. const entity = await _converse.api.disco.entities.get(_converse.domain);
  498. if (!entity.features.findWhere({'var': Strophe.NS.MAM})) {
  499. _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
  500. }
  501. let sent_stanza, IQ_id;
  502. const sendIQ = _converse.connection.sendIQ;
  503. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  504. sent_stanza = iq;
  505. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  506. });
  507. const callback = jasmine.createSpy('callback');
  508. _converse.api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'}, callback);
  509. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  510. /* <message id='aeb213' to='juliet@capulet.lit/chamber'>
  511. * <result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>
  512. * <forwarded xmlns='urn:xmpp:forward:0'>
  513. * <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
  514. * <message xmlns='jabber:client'
  515. * to='juliet@capulet.lit/balcony'
  516. * from='romeo@montague.lit/orchard'
  517. * type='chat'>
  518. * <body>Call me but love, and I'll be new baptized; Henceforth I never will be Romeo.</body>
  519. * </message>
  520. * </forwarded>
  521. * </result>
  522. * </message>
  523. */
  524. const msg1 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'})
  525. .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73623'})
  526. .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
  527. .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
  528. .c('message', {
  529. 'xmlns':'jabber:client',
  530. 'to':'juliet@capulet.lit/balcony',
  531. 'from':'romeo@montague.lit/orchard',
  532. 'type':'chat' })
  533. .c('body').t("Call me but love, and I'll be new baptized;");
  534. _converse.connection._dataRecv(test_utils.createRequest(msg1));
  535. const msg2 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'})
  536. .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73624'})
  537. .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
  538. .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
  539. .c('message', {
  540. 'xmlns':'jabber:client',
  541. 'to':'juliet@capulet.lit/balcony',
  542. 'from':'romeo@montague.lit/orchard',
  543. 'type':'chat' })
  544. .c('body').t("Henceforth I never will be Romeo.");
  545. _converse.connection._dataRecv(test_utils.createRequest(msg2));
  546. /* Send an <iq> stanza to indicate the end of the result set.
  547. *
  548. * <iq type='result' id='juliet1'>
  549. * <fin xmlns='urn:xmpp:mam:2'>
  550. * <set xmlns='http://jabber.org/protocol/rsm'>
  551. * <first index='0'>28482-98726-73623</first>
  552. * <last>09af3-cc343-b409f</last>
  553. * <count>20</count>
  554. * </set>
  555. * </iq>
  556. */
  557. const stanza = $iq({'type': 'result', 'id': IQ_id})
  558. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  559. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  560. .c('first', {'index': '0'}).t('23452-4534-1').up()
  561. .c('last').t('09af3-cc343-b409f').up()
  562. .c('count').t('16');
  563. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  564. await test_utils.waitUntil(() => callback.calls.count());
  565. expect(callback).toHaveBeenCalled();
  566. const args = callback.calls.argsFor(0);
  567. expect(args[0].length).toBe(2);
  568. expect(args[0][0].outerHTML).toBe(msg1.nodeTree.outerHTML);
  569. expect(args[0][1].outerHTML).toBe(msg2.nodeTree.outerHTML);
  570. expect(args[1]['with']).toBe('romeo@capulet.lit'); // eslint-disable-line dot-notation
  571. expect(args[1].max).toBe('10');
  572. expect(args[1].count).toBe('16');
  573. expect(args[1].first).toBe('23452-4534-1');
  574. expect(args[1].last).toBe('09af3-cc343-b409f');
  575. done()
  576. }));
  577. });
  578. describe("The default preference", function () {
  579. it("is set once server support for MAM has been confirmed",
  580. mock.initConverse(
  581. null, [], {},
  582. async function (done, _converse) {
  583. const entity = await _converse.api.disco.entities.get(_converse.domain);
  584. let sent_stanza, IQ_id;
  585. const sendIQ = _converse.connection.sendIQ;
  586. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  587. sent_stanza = iq;
  588. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  589. });
  590. spyOn(_converse, 'onMAMPreferences').and.callThrough();
  591. _converse.message_archiving = 'never';
  592. const feature = new Backbone.Model({
  593. 'var': Strophe.NS.MAM
  594. });
  595. spyOn(feature, 'save').and.callFake(feature.set); // Save will complain about a url not being set
  596. entity.onFeatureAdded(feature);
  597. expect(_converse.connection.sendIQ).toHaveBeenCalled();
  598. expect(sent_stanza.toLocaleString()).toBe(
  599. `<iq id="${IQ_id}" type="get" xmlns="jabber:client">`+
  600. `<prefs xmlns="urn:xmpp:mam:2"/>`+
  601. `</iq>`);
  602. /* Example 20. Server responds with current preferences
  603. *
  604. * <iq type='result' id='juliet2'>
  605. * <prefs xmlns='urn:xmpp:mam:0' default='roster'>
  606. * <always/>
  607. * <never/>
  608. * </prefs>
  609. * </iq>
  610. */
  611. let stanza = $iq({'type': 'result', 'id': IQ_id})
  612. .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'roster'})
  613. .c('always').c('jid').t('romeo@montague.lit').up().up()
  614. .c('never').c('jid').t('montague@montague.lit');
  615. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  616. await test_utils.waitUntil(() => _converse.onMAMPreferences.calls.count());
  617. expect(_converse.onMAMPreferences).toHaveBeenCalled();
  618. expect(_converse.connection.sendIQ.calls.count()).toBe(3);
  619. expect(sent_stanza.toString()).toBe(
  620. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  621. `<prefs default="never" xmlns="urn:xmpp:mam:2">`+
  622. `<always><jid>romeo@montague.lit</jid></always>`+
  623. `<never><jid>montague@montague.lit</jid></never>`+
  624. `</prefs>`+
  625. `</iq>`
  626. );
  627. expect(feature.get('preference')).toBe(undefined);
  628. /* <iq type='result' id='juliet3'>
  629. * <prefs xmlns='urn:xmpp:mam:0' default='always'>
  630. * <always>
  631. * <jid>romeo@montague.lit</jid>
  632. * </always>
  633. * <never>
  634. * <jid>montague@montague.lit</jid>
  635. * </never>
  636. * </prefs>
  637. * </iq>
  638. */
  639. stanza = $iq({'type': 'result', 'id': IQ_id})
  640. .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'always'})
  641. .c('always').up()
  642. .c('never');
  643. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  644. await test_utils.waitUntil(() => feature.save.calls.count());
  645. expect(feature.save).toHaveBeenCalled();
  646. expect(feature.get('preferences')['default']).toBe('never'); // eslint-disable-line dot-notation
  647. done();
  648. }));
  649. });
  650. });
  651. }));