mam.js 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098
  1. /*global mock */
  2. const Model = converse.env.Model;
  3. const Strophe = converse.env.Strophe;
  4. const $iq = converse.env.$iq;
  5. const $msg = converse.env.$msg;
  6. const dayjs = converse.env.dayjs;
  7. const u = converse.env.utils;
  8. const sizzle = converse.env.sizzle;
  9. // See: https://xmpp.org/rfcs/rfc3921.html
  10. // Implements the protocol defined in https://xmpp.org/extensions/xep-0313.html#config
  11. describe("Message Archive Management", function () {
  12. describe("The XEP-0313 Archive", function () {
  13. it("is queried when the user enters a new MUC",
  14. mock.initConverse(['discoInitialized'], {'archived_messages_page_size': 2}, async function (done, _converse) {
  15. const sent_IQs = _converse.connection.IQ_stanzas;
  16. const muc_jid = 'orchard@chat.shakespeare.lit';
  17. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  18. let view = _converse.chatboxviews.get(muc_jid);
  19. let iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop());
  20. expect(Strophe.serialize(iq_get)).toBe(
  21. `<iq id="${iq_get.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
  22. `<query queryid="${iq_get.querySelector('query').getAttribute('queryid')}" xmlns="${Strophe.NS.MAM}">`+
  23. `<x type="submit" xmlns="jabber:x:data">`+
  24. `<field type="hidden" var="FORM_TYPE"><value>urn:xmpp:mam:2</value></field>`+
  25. `</x>`+
  26. `<set xmlns="http://jabber.org/protocol/rsm"><max>2</max><before></before></set>`+
  27. `</query>`+
  28. `</iq>`);
  29. let first_msg_id = _converse.connection.getUniqueId();
  30. let last_msg_id = _converse.connection.getUniqueId();
  31. let message = u.toStanza(
  32. `<message xmlns="jabber:client"
  33. to="romeo@montague.lit/orchard"
  34. from="${muc_jid}">
  35. <result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${first_msg_id}">
  36. <forwarded xmlns="urn:xmpp:forward:0">
  37. <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:15:23Z"/>
  38. <message from="${muc_jid}/some1" type="groupchat">
  39. <body>2nd Message</body>
  40. </message>
  41. </forwarded>
  42. </result>
  43. </message>`);
  44. _converse.connection._dataRecv(mock.createRequest(message));
  45. message = u.toStanza(
  46. `<message xmlns="jabber:client"
  47. to="romeo@montague.lit/orchard"
  48. from="${muc_jid}">
  49. <result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${last_msg_id}">
  50. <forwarded xmlns="urn:xmpp:forward:0">
  51. <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:16:23Z"/>
  52. <message from="${muc_jid}/some1" type="groupchat">
  53. <body>3rd Message</body>
  54. </message>
  55. </forwarded>
  56. </result>
  57. </message>`);
  58. _converse.connection._dataRecv(mock.createRequest(message));
  59. // Clear so that we don't match the older query
  60. while (sent_IQs.length) { sent_IQs.pop(); }
  61. // XXX: Even though the count is 3, when fetching messages for
  62. // the first time, we don't paginate, so that message
  63. // is not fetched. The user needs to manually load older
  64. // messages for it to be fetched.
  65. // TODO: we need to add a clickable link to load older messages
  66. let result = u.toStanza(
  67. `<iq type='result' id='${iq_get.getAttribute('id')}'>
  68. <fin xmlns='urn:xmpp:mam:2'>
  69. <set xmlns='http://jabber.org/protocol/rsm'>
  70. <first index='0'>${first_msg_id}</first>
  71. <last>${last_msg_id}</last>
  72. <count>3</count>
  73. </set>
  74. </fin>
  75. </iq>`);
  76. _converse.connection._dataRecv(mock.createRequest(result));
  77. await u.waitUntil(() => view.model.messages.length === 2);
  78. view.close();
  79. // Clear so that we don't match the older query
  80. while (sent_IQs.length) { sent_IQs.pop(); }
  81. await u.waitUntil(() => _converse.chatboxes.length === 1);
  82. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  83. view = _converse.chatboxviews.get(muc_jid);
  84. await u.waitUntil(() => view.model.messages.length);
  85. iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop());
  86. expect(Strophe.serialize(iq_get)).toBe(
  87. `<iq id="${iq_get.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
  88. `<query queryid="${iq_get.querySelector('query').getAttribute('queryid')}" xmlns="${Strophe.NS.MAM}">`+
  89. `<x type="submit" xmlns="jabber:x:data">`+
  90. `<field type="hidden" var="FORM_TYPE"><value>urn:xmpp:mam:2</value></field>`+
  91. `</x>`+
  92. `<set xmlns="http://jabber.org/protocol/rsm"><max>2</max><after>${message.querySelector('result').getAttribute('id')}</after></set>`+
  93. `</query>`+
  94. `</iq>`);
  95. first_msg_id = _converse.connection.getUniqueId();
  96. last_msg_id = _converse.connection.getUniqueId();
  97. message = u.toStanza(
  98. `<message xmlns="jabber:client"
  99. to="romeo@montague.lit/orchard"
  100. from="${muc_jid}">
  101. <result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${first_msg_id}">
  102. <forwarded xmlns="urn:xmpp:forward:0">
  103. <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:17:23Z"/>
  104. <message from="${muc_jid}/some1" type="groupchat">
  105. <body>4th Message</body>
  106. </message>
  107. </forwarded>
  108. </result>
  109. </message>`);
  110. _converse.connection._dataRecv(mock.createRequest(message));
  111. message = u.toStanza(
  112. `<message xmlns="jabber:client"
  113. to="romeo@montague.lit/orchard"
  114. from="${muc_jid}">
  115. <result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${last_msg_id}">
  116. <forwarded xmlns="urn:xmpp:forward:0">
  117. <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:18:23Z"/>
  118. <message from="${muc_jid}/some1" type="groupchat">
  119. <body>5th Message</body>
  120. </message>
  121. </forwarded>
  122. </result>
  123. </message>`);
  124. _converse.connection._dataRecv(mock.createRequest(message));
  125. // Clear so that we don't match the older query
  126. while (sent_IQs.length) { sent_IQs.pop(); }
  127. result = u.toStanza(
  128. `<iq type='result' id='${iq_get.getAttribute('id')}'>
  129. <fin xmlns='urn:xmpp:mam:2'>
  130. <set xmlns='http://jabber.org/protocol/rsm'>
  131. <first index='0'>${first_msg_id}</first>
  132. <last>${last_msg_id}</last>
  133. <count>5</count>
  134. </set>
  135. </fin>
  136. </iq>`);
  137. _converse.connection._dataRecv(mock.createRequest(result));
  138. await u.waitUntil(() => view.model.messages.length === 4);
  139. iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop());
  140. expect(Strophe.serialize(iq_get)).toBe(
  141. `<iq id="${iq_get.getAttribute('id')}" to="orchard@chat.shakespeare.lit" type="set" xmlns="jabber:client">`+
  142. `<query queryid="${iq_get.querySelector('query').getAttribute('queryid')}" xmlns="urn:xmpp:mam:2">`+
  143. `<x type="submit" xmlns="jabber:x:data">`+
  144. `<field type="hidden" var="FORM_TYPE"><value>urn:xmpp:mam:2</value></field>`+
  145. `</x>`+
  146. `<set xmlns="http://jabber.org/protocol/rsm">`+
  147. `<max>2</max><after>${last_msg_id}</after>`+
  148. `</set>`+
  149. `</query>`+
  150. `</iq>`);
  151. const msg_id = _converse.connection.getUniqueId();
  152. message = u.toStanza(
  153. `<message xmlns="jabber:client"
  154. to="romeo@montague.lit/orchard"
  155. from="${muc_jid}">
  156. <result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${msg_id}">
  157. <forwarded xmlns="urn:xmpp:forward:0">
  158. <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:19:23Z"/>
  159. <message from="${muc_jid}/some1" type="groupchat">
  160. <body>6th Message</body>
  161. </message>
  162. </forwarded>
  163. </result>
  164. </message>`);
  165. _converse.connection._dataRecv(mock.createRequest(message));
  166. result = u.toStanza(
  167. `<iq type='result' id='${iq_get.getAttribute('id')}'>
  168. <fin xmlns="urn:xmpp:mam:2" complete="true">
  169. <set xmlns="http://jabber.org/protocol/rsm">
  170. <first index="0">${msg_id}</first>
  171. <last>${msg_id}</last>
  172. <count>6</count>
  173. </set>
  174. </fin>
  175. </iq>`);
  176. _converse.connection._dataRecv(mock.createRequest(result));
  177. await u.waitUntil(() => view.model.messages.length === 5);
  178. const msg_els = view.content.querySelectorAll('.chat-msg__text');
  179. expect(Array.from(msg_els).map(e => e.textContent).join(' ')).toBe("2nd Message 3rd Message 4th Message 5th Message 6th Message");
  180. done();
  181. }));
  182. });
  183. describe("An archived message", function () {
  184. describe("when received", function () {
  185. it("is discarded if it doesn't come from the right sender",
  186. mock.initConverse(
  187. ['discoInitialized'], {},
  188. async function (done, _converse) {
  189. await mock.waitForRoster(_converse, 'current', 1);
  190. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  191. await mock.openChatBoxFor(_converse, contact_jid);
  192. const view = _converse.chatboxviews.get(contact_jid);
  193. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  194. const sent_IQs = _converse.connection.IQ_stanzas;
  195. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
  196. const queryid = stanza.querySelector('query').getAttribute('queryid');
  197. let msg = $msg({'id': _converse.connection.getUniqueId(), 'from': 'impersonator@capulet.lit', 'to': _converse.bare_jid})
  198. .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()})
  199. .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
  200. .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
  201. .c('message', {
  202. 'xmlns':'jabber:client',
  203. 'to': _converse.bare_jid,
  204. 'id': _converse.connection.getUniqueId(),
  205. 'from': contact_jid,
  206. 'type':'chat'
  207. }).c('body').t("Meet me at the dance");
  208. spyOn(converse.env.log, 'warn');
  209. _converse.connection._dataRecv(mock.createRequest(msg));
  210. expect(converse.env.log.warn).toHaveBeenCalledWith(`Ignoring alleged MAM message from ${msg.nodeTree.getAttribute('from')}`);
  211. msg = $msg({'id': _converse.connection.getUniqueId(), 'to': _converse.bare_jid})
  212. .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()})
  213. .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
  214. .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
  215. .c('message', {
  216. 'xmlns':'jabber:client',
  217. 'to': _converse.bare_jid,
  218. 'id': _converse.connection.getUniqueId(),
  219. 'from': contact_jid,
  220. 'type':'chat'
  221. }).c('body').t("Thrice the brinded cat hath mew'd.");
  222. _converse.connection._dataRecv(mock.createRequest(msg));
  223. const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
  224. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  225. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  226. .c('first', {'index': '0'}).t('23452-4534-1').up()
  227. .c('last').t('09af3-cc343-b409f').up()
  228. .c('count').t('16');
  229. _converse.connection._dataRecv(mock.createRequest(iq_result));
  230. await new Promise(resolve => view.once('messageInserted', resolve));
  231. expect(view.model.messages.length).toBe(1);
  232. expect(view.model.messages.at(0).get('message')).toBe("Thrice the brinded cat hath mew'd.");
  233. done();
  234. }));
  235. it("updates the is_archived value of an already cached version",
  236. mock.initConverse(
  237. ['discoInitialized'], {},
  238. async function (done, _converse) {
  239. await mock.openAndEnterChatRoom(_converse, 'trek-radio@conference.lightwitch.org', 'romeo');
  240. const view = _converse.chatboxviews.get('trek-radio@conference.lightwitch.org');
  241. let stanza = u.toStanza(
  242. `<message xmlns="jabber:client" to="romeo@montague.lit/orchard" type="groupchat" from="trek-radio@conference.lightwitch.org/some1">
  243. <body>Hello</body>
  244. <stanza-id xmlns="urn:xmpp:sid:0" id="45fbbf2a-1059-479d-9283-c8effaf05621" by="trek-radio@conference.lightwitch.org"/>
  245. </message>`);
  246. _converse.connection._dataRecv(mock.createRequest(stanza));
  247. await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
  248. expect(view.model.messages.length).toBe(1);
  249. expect(view.model.messages.at(0).get('is_archived')).toBe(false);
  250. expect(view.model.messages.at(0).get('stanza_id trek-radio@conference.lightwitch.org')).toBe('45fbbf2a-1059-479d-9283-c8effaf05621');
  251. stanza = u.toStanza(
  252. `<message xmlns="jabber:client"
  253. to="romeo@montague.lit/orchard"
  254. from="trek-radio@conference.lightwitch.org">
  255. <result xmlns="urn:xmpp:mam:2" queryid="82d9db27-6cf8-4787-8c2c-5a560263d823" id="45fbbf2a-1059-479d-9283-c8effaf05621">
  256. <forwarded xmlns="urn:xmpp:forward:0">
  257. <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:17:23Z"/>
  258. <message from="trek-radio@conference.lightwitch.org/some1" type="groupchat">
  259. <body>Hello</body>
  260. </message>
  261. </forwarded>
  262. </result>
  263. </message>`);
  264. spyOn(view.model, 'getDuplicateMessage').and.callThrough();
  265. spyOn(view.model, 'updateMessage').and.callThrough();
  266. view.model.handleMAMResult({ 'messages': [stanza] });
  267. await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
  268. expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
  269. const result = view.model.getDuplicateMessage.calls.all()[0].returnValue
  270. expect(result instanceof _converse.Message).toBe(true);
  271. expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
  272. await u.waitUntil(() => view.model.updateMessage.calls.count());
  273. expect(view.model.messages.length).toBe(1);
  274. expect(view.model.messages.at(0).get('is_archived')).toBe(true);
  275. expect(view.model.messages.at(0).get('stanza_id trek-radio@conference.lightwitch.org')).toBe('45fbbf2a-1059-479d-9283-c8effaf05621');
  276. done();
  277. }));
  278. it("isn't shown as duplicate by comparing its stanza id or archive id",
  279. mock.initConverse(
  280. ['discoInitialized'], {},
  281. async function (done, _converse) {
  282. await mock.openAndEnterChatRoom(_converse, 'trek-radio@conference.lightwitch.org', 'jcbrand');
  283. const view = _converse.chatboxviews.get('trek-radio@conference.lightwitch.org');
  284. let stanza = u.toStanza(
  285. `<message xmlns="jabber:client" to="jcbrand@lightwitch.org/converse.js-73057452" type="groupchat" from="trek-radio@conference.lightwitch.org/comndrdukath#0805 (STO)">
  286. <body>negan</body>
  287. <stanza-id xmlns="urn:xmpp:sid:0" id="45fbbf2a-1059-479d-9283-c8effaf05621" by="trek-radio@conference.lightwitch.org"/>
  288. </message>`);
  289. _converse.connection._dataRecv(mock.createRequest(stanza));
  290. await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
  291. // Not sure whether such a race-condition might pose a problem
  292. // in "real-world" situations.
  293. stanza = u.toStanza(
  294. `<message xmlns="jabber:client"
  295. to="jcbrand@lightwitch.org/converse.js-73057452"
  296. from="trek-radio@conference.lightwitch.org">
  297. <result xmlns="urn:xmpp:mam:2" queryid="82d9db27-6cf8-4787-8c2c-5a560263d823" id="45fbbf2a-1059-479d-9283-c8effaf05621">
  298. <forwarded xmlns="urn:xmpp:forward:0">
  299. <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:17:23Z"/>
  300. <message from="trek-radio@conference.lightwitch.org/comndrdukath#0805 (STO)" type="groupchat">
  301. <body>negan</body>
  302. </message>
  303. </forwarded>
  304. </result>
  305. </message>`);
  306. spyOn(view.model, 'getDuplicateMessage').and.callThrough();
  307. view.model.handleMAMResult({ 'messages': [stanza] });
  308. await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
  309. expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
  310. const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue
  311. expect(result instanceof _converse.Message).toBe(true);
  312. expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
  313. done();
  314. }));
  315. it("isn't shown as duplicate by comparing only the archive id",
  316. mock.initConverse(
  317. ['discoInitialized'], {},
  318. async function (done, _converse) {
  319. await mock.openAndEnterChatRoom(_converse, 'discuss@conference.conversejs.org', 'romeo');
  320. const view = _converse.chatboxviews.get('discuss@conference.conversejs.org');
  321. let stanza = u.toStanza(
  322. `<message xmlns="jabber:client" to="romeo@montague.lit/orchard" from="discuss@conference.conversejs.org">
  323. <result xmlns="urn:xmpp:mam:2" queryid="06fea9ca-97c9-48c4-8583-009ff54ea2e8" id="7a9fde91-4387-4bf8-b5d3-978dab8f6bf3">
  324. <forwarded xmlns="urn:xmpp:forward:0">
  325. <delay xmlns="urn:xmpp:delay" stamp="2018-12-05T04:53:12Z"/>
  326. <message xmlns="jabber:client" to="discuss@conference.conversejs.org" type="groupchat" xml:lang="en" from="discuss@conference.conversejs.org/prezel">
  327. <body>looks like omemo fails completely with "bundle is undefined" when there is a device in the devicelist that has no keys published</body>
  328. <x xmlns="http://jabber.org/protocol/muc#user">
  329. <item affiliation="none" jid="prezel@blubber.im" role="participant"/>
  330. </x>
  331. </message>
  332. </forwarded>
  333. </result>
  334. </message>`);
  335. view.model.handleMAMResult({ 'messages': [stanza] });
  336. await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
  337. expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
  338. stanza = u.toStanza(
  339. `<message xmlns="jabber:client" to="romeo@montague.lit/orchard" from="discuss@conference.conversejs.org">
  340. <result xmlns="urn:xmpp:mam:2" queryid="06fea9ca-97c9-48c4-8583-009ff54ea2e8" id="7a9fde91-4387-4bf8-b5d3-978dab8f6bf3">
  341. <forwarded xmlns="urn:xmpp:forward:0">
  342. <delay xmlns="urn:xmpp:delay" stamp="2018-12-05T04:53:12Z"/>
  343. <message xmlns="jabber:client" to="discuss@conference.conversejs.org" type="groupchat" xml:lang="en" from="discuss@conference.conversejs.org/prezel">
  344. <body>looks like omemo fails completely with "bundle is undefined" when there is a device in the devicelist that has no keys published</body>
  345. <x xmlns="http://jabber.org/protocol/muc#user">
  346. <item affiliation="none" jid="prezel@blubber.im" role="participant"/>
  347. </x>
  348. </message>
  349. </forwarded>
  350. </result>
  351. </message>`);
  352. spyOn(view.model, 'getDuplicateMessage').and.callThrough();
  353. view.model.handleMAMResult({ 'messages': [stanza] });
  354. await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
  355. expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
  356. const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue
  357. expect(result instanceof _converse.Message).toBe(true);
  358. expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
  359. done();
  360. }))
  361. });
  362. });
  363. describe("The archive.query API", function () {
  364. it("can be used to query for all archived messages",
  365. mock.initConverse(['discoInitialized'], {}, async function (done, _converse) {
  366. const sendIQ = _converse.connection.sendIQ;
  367. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  368. let sent_stanza, IQ_id;
  369. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  370. sent_stanza = iq;
  371. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  372. });
  373. _converse.api.archive.query();
  374. await u.waitUntil(() => sent_stanza);
  375. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  376. expect(sent_stanza.toString()).toBe(
  377. `<iq id="${IQ_id}" type="set" xmlns="jabber:client"><query queryid="${queryid}" xmlns="urn:xmpp:mam:2"/></iq>`);
  378. done();
  379. }));
  380. it("can be used to query for all messages to/from a particular JID",
  381. mock.initConverse([], {}, async function (done, _converse) {
  382. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  383. let sent_stanza, IQ_id;
  384. const sendIQ = _converse.connection.sendIQ;
  385. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  386. sent_stanza = iq;
  387. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  388. });
  389. _converse.api.archive.query({'with':'juliet@capulet.lit'});
  390. await u.waitUntil(() => sent_stanza);
  391. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  392. expect(sent_stanza.toString()).toBe(
  393. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  394. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  395. `<x type="submit" xmlns="jabber:x:data">`+
  396. `<field type="hidden" var="FORM_TYPE">`+
  397. `<value>urn:xmpp:mam:2</value>`+
  398. `</field>`+
  399. `<field var="with">`+
  400. `<value>juliet@capulet.lit</value>`+
  401. `</field>`+
  402. `</x>`+
  403. `</query>`+
  404. `</iq>`);
  405. done();
  406. }));
  407. it("can be used to query for archived messages from a chat room",
  408. mock.initConverse([], {}, async function (done, _converse) {
  409. const room_jid = 'coven@chat.shakespeare.lit';
  410. _converse.api.archive.query({'with': room_jid, 'groupchat': true});
  411. await mock.waitUntilDiscoConfirmed(_converse, room_jid, null, [Strophe.NS.MAM]);
  412. const sent_stanzas = _converse.connection.sent_stanzas;
  413. const stanza = await u.waitUntil(
  414. () => sent_stanzas.filter(s => sizzle(`[xmlns="${Strophe.NS.MAM}"]`, s).length).pop());
  415. const queryid = stanza.querySelector('query').getAttribute('queryid');
  416. expect(Strophe.serialize(stanza)).toBe(
  417. `<iq id="${stanza.getAttribute('id')}" to="coven@chat.shakespeare.lit" type="set" xmlns="jabber:client">`+
  418. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  419. `<x type="submit" xmlns="jabber:x:data">`+
  420. `<field type="hidden" var="FORM_TYPE">`+
  421. `<value>urn:xmpp:mam:2</value>`+
  422. `</field>`+
  423. `</x>`+
  424. `</query>`+
  425. `</iq>`);
  426. done();
  427. }));
  428. it("checks whether returned MAM messages from a MUC room are from the right JID",
  429. mock.initConverse([], {}, async function (done, _converse) {
  430. const room_jid = 'coven@chat.shakespeare.lit';
  431. const promise = _converse.api.archive.query({'with': room_jid, 'groupchat': true, 'max':'10'});
  432. await mock.waitUntilDiscoConfirmed(_converse, room_jid, null, [Strophe.NS.MAM]);
  433. const sent_stanzas = _converse.connection.sent_stanzas;
  434. const sent_stanza = await u.waitUntil(
  435. () => sent_stanzas.filter(s => sizzle(`[xmlns="${Strophe.NS.MAM}"]`, s).length).pop());
  436. const queryid = sent_stanza.querySelector('query').getAttribute('queryid');
  437. /* <message id='iasd207' from='coven@chat.shakespeare.lit' to='hag66@shakespeare.lit/pda'>
  438. * <result xmlns='urn:xmpp:mam:2' queryid='g27' id='34482-21985-73620'>
  439. * <forwarded xmlns='urn:xmpp:forward:0'>
  440. * <delay xmlns='urn:xmpp:delay' stamp='2002-10-13T23:58:37Z'/>
  441. * <message xmlns="jabber:client"
  442. * from='coven@chat.shakespeare.lit/firstwitch'
  443. * id='162BEBB1-F6DB-4D9A-9BD8-CFDCC801A0B2'
  444. * type='groupchat'>
  445. * <body>Thrice the brinded cat hath mew'd.</body>
  446. * <x xmlns='http://jabber.org/protocol/muc#user'>
  447. * <item affiliation='none'
  448. * jid='witch1@shakespeare.lit'
  449. * role='participant' />
  450. * </x>
  451. * </message>
  452. * </forwarded>
  453. * </result>
  454. * </message>
  455. */
  456. const msg1 = $msg({'id':'iasd207', 'from': 'other@chat.shakespear.lit', 'to': 'romeo@montague.lit'})
  457. .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'34482-21985-73620'})
  458. .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
  459. .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
  460. .c('message', {
  461. 'xmlns':'jabber:client',
  462. 'to':'romeo@montague.lit',
  463. 'id':'162BEBB1-F6DB-4D9A-9BD8-CFDCC801A0B2',
  464. 'from':'coven@chat.shakespeare.lit/firstwitch',
  465. 'type':'groupchat' })
  466. .c('body').t("Thrice the brinded cat hath mew'd.");
  467. _converse.connection._dataRecv(mock.createRequest(msg1));
  468. /* Send an <iq> stanza to indicate the end of the result set.
  469. *
  470. * <iq type='result' id='juliet1'>
  471. * <fin xmlns='urn:xmpp:mam:2'>
  472. * <set xmlns='http://jabber.org/protocol/rsm'>
  473. * <first index='0'>28482-98726-73623</first>
  474. * <last>09af3-cc343-b409f</last>
  475. * <count>20</count>
  476. * </set>
  477. * </iq>
  478. */
  479. const stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')})
  480. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  481. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  482. .c('first', {'index': '0'}).t('23452-4534-1').up()
  483. .c('last').t('09af3-cc343-b409f').up()
  484. .c('count').t('16');
  485. _converse.connection._dataRecv(mock.createRequest(stanza));
  486. const result = await promise;
  487. expect(result.messages.length).toBe(0);
  488. done();
  489. }));
  490. it("can be used to query for all messages in a certain timespan",
  491. mock.initConverse([], {}, async function (done, _converse) {
  492. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  493. let sent_stanza, IQ_id;
  494. const sendIQ = _converse.connection.sendIQ;
  495. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  496. sent_stanza = iq;
  497. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  498. });
  499. const start = '2010-06-07T00:00:00Z';
  500. const end = '2010-07-07T13:23:54Z';
  501. _converse.api.archive.query({
  502. 'start': start,
  503. 'end': end
  504. });
  505. await u.waitUntil(() => sent_stanza);
  506. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  507. expect(sent_stanza.toString()).toBe(
  508. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  509. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  510. `<x type="submit" xmlns="jabber:x:data">`+
  511. `<field type="hidden" var="FORM_TYPE">`+
  512. `<value>urn:xmpp:mam:2</value>`+
  513. `</field>`+
  514. `<field var="start">`+
  515. `<value>${dayjs(start).toISOString()}</value>`+
  516. `</field>`+
  517. `<field var="end">`+
  518. `<value>${dayjs(end).toISOString()}</value>`+
  519. `</field>`+
  520. `</x>`+
  521. `</query>`+
  522. `</iq>`
  523. );
  524. done();
  525. }));
  526. it("throws a TypeError if an invalid date is provided",
  527. mock.initConverse([], {}, async function (done, _converse) {
  528. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  529. try {
  530. await _converse.api.archive.query({'start': 'not a real date'});
  531. } catch (e) {
  532. expect(() => {throw e}).toThrow(new TypeError('archive.query: invalid date provided for: start'));
  533. }
  534. done();
  535. }));
  536. it("can be used to query for all messages after a certain time",
  537. mock.initConverse([], {}, async function (done, _converse) {
  538. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  539. let sent_stanza, IQ_id;
  540. const sendIQ = _converse.connection.sendIQ;
  541. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  542. sent_stanza = iq;
  543. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  544. });
  545. if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) {
  546. _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
  547. }
  548. const start = '2010-06-07T00:00:00Z';
  549. _converse.api.archive.query({'start': start});
  550. await u.waitUntil(() => sent_stanza);
  551. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  552. expect(sent_stanza.toString()).toBe(
  553. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  554. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  555. `<x type="submit" xmlns="jabber:x:data">`+
  556. `<field type="hidden" var="FORM_TYPE">`+
  557. `<value>urn:xmpp:mam:2</value>`+
  558. `</field>`+
  559. `<field var="start">`+
  560. `<value>${dayjs(start).toISOString()}</value>`+
  561. `</field>`+
  562. `</x>`+
  563. `</query>`+
  564. `</iq>`
  565. );
  566. done();
  567. }));
  568. it("can be used to query for a limited set of results",
  569. mock.initConverse([], {}, async function (done, _converse) {
  570. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  571. let sent_stanza, IQ_id;
  572. const sendIQ = _converse.connection.sendIQ;
  573. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  574. sent_stanza = iq;
  575. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  576. });
  577. const start = '2010-06-07T00:00:00Z';
  578. _converse.api.archive.query({'start': start, 'max':10});
  579. await u.waitUntil(() => sent_stanza);
  580. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  581. expect(sent_stanza.toString()).toBe(
  582. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  583. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  584. `<x type="submit" xmlns="jabber:x:data">`+
  585. `<field type="hidden" var="FORM_TYPE">`+
  586. `<value>urn:xmpp:mam:2</value>`+
  587. `</field>`+
  588. `<field var="start">`+
  589. `<value>${dayjs(start).toISOString()}</value>`+
  590. `</field>`+
  591. `</x>`+
  592. `<set xmlns="http://jabber.org/protocol/rsm">`+
  593. `<max>10</max>`+
  594. `</set>`+
  595. `</query>`+
  596. `</iq>`
  597. );
  598. done();
  599. }));
  600. it("can be used to page through results",
  601. mock.initConverse([], {}, async function (done, _converse) {
  602. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  603. let sent_stanza, IQ_id;
  604. const sendIQ = _converse.connection.sendIQ;
  605. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  606. sent_stanza = iq;
  607. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  608. });
  609. const start = '2010-06-07T00:00:00Z';
  610. _converse.api.archive.query({
  611. 'start': start,
  612. 'after': '09af3-cc343-b409f',
  613. 'max':10
  614. });
  615. await u.waitUntil(() => sent_stanza);
  616. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  617. expect(sent_stanza.toString()).toBe(
  618. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  619. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  620. `<x type="submit" xmlns="jabber:x:data">`+
  621. `<field type="hidden" var="FORM_TYPE">`+
  622. `<value>urn:xmpp:mam:2</value>`+
  623. `</field>`+
  624. `<field var="start">`+
  625. `<value>${dayjs(start).toISOString()}</value>`+
  626. `</field>`+
  627. `</x>`+
  628. `<set xmlns="http://jabber.org/protocol/rsm">`+
  629. `<max>10</max>`+
  630. `<after>09af3-cc343-b409f</after>`+
  631. `</set>`+
  632. `</query>`+
  633. `</iq>`);
  634. done();
  635. }));
  636. it("accepts \"before\" with an empty string as value to reverse the order",
  637. mock.initConverse([], {}, async function (done, _converse) {
  638. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  639. let sent_stanza, IQ_id;
  640. const sendIQ = _converse.connection.sendIQ;
  641. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  642. sent_stanza = iq;
  643. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  644. });
  645. _converse.api.archive.query({'before': '', 'max':10});
  646. await u.waitUntil(() => sent_stanza);
  647. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  648. expect(sent_stanza.toString()).toBe(
  649. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  650. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  651. `<x type="submit" xmlns="jabber:x:data">`+
  652. `<field type="hidden" var="FORM_TYPE">`+
  653. `<value>urn:xmpp:mam:2</value>`+
  654. `</field>`+
  655. `</x>`+
  656. `<set xmlns="http://jabber.org/protocol/rsm">`+
  657. `<max>10</max>`+
  658. `<before></before>`+
  659. `</set>`+
  660. `</query>`+
  661. `</iq>`);
  662. done();
  663. }));
  664. it("accepts a _converse.RSM object for the query options",
  665. mock.initConverse([], {}, async function (done, _converse) {
  666. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  667. let sent_stanza, IQ_id;
  668. const sendIQ = _converse.connection.sendIQ;
  669. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  670. sent_stanza = iq;
  671. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  672. });
  673. // Normally the user wouldn't manually make a _converse.RSM object
  674. // and pass it in. However, in the callback method an RSM object is
  675. // returned which can be reused for easy paging. This test is
  676. // more for that usecase.
  677. const rsm = new _converse.RSM({'max': '10'});
  678. rsm['with'] = 'romeo@montague.lit'; // eslint-disable-line dot-notation
  679. rsm.start = '2010-06-07T00:00:00Z';
  680. _converse.api.archive.query(rsm);
  681. await u.waitUntil(() => sent_stanza);
  682. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  683. expect(sent_stanza.toString()).toBe(
  684. `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
  685. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  686. `<x type="submit" xmlns="jabber:x:data">`+
  687. `<field type="hidden" var="FORM_TYPE">`+
  688. `<value>urn:xmpp:mam:2</value>`+
  689. `</field>`+
  690. `<field var="with">`+
  691. `<value>romeo@montague.lit</value>`+
  692. `</field>`+
  693. `<field var="start">`+
  694. `<value>${dayjs(rsm.start).toISOString()}</value>`+
  695. `</field>`+
  696. `</x>`+
  697. `<set xmlns="http://jabber.org/protocol/rsm">`+
  698. `<max>10</max>`+
  699. `</set>`+
  700. `</query>`+
  701. `</iq>`);
  702. done();
  703. }));
  704. it("returns an object which includes the messages and a _converse.RSM object",
  705. mock.initConverse([], {}, async function (done, _converse) {
  706. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  707. let sent_stanza, IQ_id;
  708. const sendIQ = _converse.connection.sendIQ;
  709. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  710. sent_stanza = iq;
  711. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  712. });
  713. const promise = _converse.api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'});
  714. await u.waitUntil(() => sent_stanza);
  715. const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid');
  716. /* <message id='aeb213' to='juliet@capulet.lit/chamber'>
  717. * <result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>
  718. * <forwarded xmlns='urn:xmpp:forward:0'>
  719. * <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
  720. * <message xmlns='jabber:client'
  721. * to='juliet@capulet.lit/balcony'
  722. * from='romeo@montague.lit/orchard'
  723. * type='chat'>
  724. * <body>Call me but love, and I'll be new baptized; Henceforth I never will be Romeo.</body>
  725. * </message>
  726. * </forwarded>
  727. * </result>
  728. * </message>
  729. */
  730. const msg1 = $msg({'id':'aeb212', 'to':'juliet@capulet.lit/chamber'})
  731. .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73623'})
  732. .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
  733. .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
  734. .c('message', {
  735. 'xmlns':'jabber:client',
  736. 'to':'juliet@capulet.lit/balcony',
  737. 'from':'romeo@montague.lit/orchard',
  738. 'type':'chat' })
  739. .c('body').t("Call me but love, and I'll be new baptized;");
  740. _converse.connection._dataRecv(mock.createRequest(msg1));
  741. const msg2 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'})
  742. .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73624'})
  743. .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
  744. .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
  745. .c('message', {
  746. 'xmlns':'jabber:client',
  747. 'to':'juliet@capulet.lit/balcony',
  748. 'from':'romeo@montague.lit/orchard',
  749. 'type':'chat' })
  750. .c('body').t("Henceforth I never will be Romeo.");
  751. _converse.connection._dataRecv(mock.createRequest(msg2));
  752. /* Send an <iq> stanza to indicate the end of the result set.
  753. *
  754. * <iq type='result' id='juliet1'>
  755. * <fin xmlns='urn:xmpp:mam:2'>
  756. * <set xmlns='http://jabber.org/protocol/rsm'>
  757. * <first index='0'>28482-98726-73623</first>
  758. * <last>09af3-cc343-b409f</last>
  759. * <count>20</count>
  760. * </set>
  761. * </iq>
  762. */
  763. const stanza = $iq({'type': 'result', 'id': IQ_id})
  764. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  765. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  766. .c('first', {'index': '0'}).t('23452-4534-1').up()
  767. .c('last').t('09af3-cc343-b409f').up()
  768. .c('count').t('16');
  769. _converse.connection._dataRecv(mock.createRequest(stanza));
  770. const result = await promise;
  771. expect(result.messages.length).toBe(2);
  772. expect(result.messages[0].outerHTML).toBe(msg1.nodeTree.outerHTML);
  773. expect(result.messages[1].outerHTML).toBe(msg2.nodeTree.outerHTML);
  774. expect(result.rsm['with']).toBe('romeo@capulet.lit'); // eslint-disable-line dot-notation
  775. expect(result.rsm.max).toBe('10');
  776. expect(result.rsm.count).toBe('16');
  777. expect(result.rsm.first).toBe('23452-4534-1');
  778. expect(result.rsm.last).toBe('09af3-cc343-b409f');
  779. done()
  780. }));
  781. });
  782. describe("The default preference", function () {
  783. it("is set once server support for MAM has been confirmed",
  784. mock.initConverse([], {}, async function (done, _converse) {
  785. const entity = await _converse.api.disco.entities.get(_converse.domain);
  786. spyOn(_converse, 'onMAMPreferences').and.callThrough();
  787. _converse.message_archiving = 'never';
  788. const feature = new Model({
  789. 'var': Strophe.NS.MAM
  790. });
  791. spyOn(feature, 'save').and.callFake(feature.set); // Save will complain about a url not being set
  792. entity.onFeatureAdded(feature);
  793. const IQ_stanzas = _converse.connection.IQ_stanzas;
  794. let sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(s => sizzle('iq[type="get"] prefs[xmlns="urn:xmpp:mam:2"]', s).length).pop());
  795. expect(Strophe.serialize(sent_stanza)).toBe(
  796. `<iq id="${sent_stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
  797. `<prefs xmlns="urn:xmpp:mam:2"/>`+
  798. `</iq>`);
  799. /* Example 20. Server responds with current preferences
  800. *
  801. * <iq type='result' id='juliet2'>
  802. * <prefs xmlns='urn:xmpp:mam:0' default='roster'>
  803. * <always/>
  804. * <never/>
  805. * </prefs>
  806. * </iq>
  807. */
  808. let stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')})
  809. .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'roster'})
  810. .c('always').c('jid').t('romeo@montague.lit').up().up()
  811. .c('never').c('jid').t('montague@montague.lit');
  812. _converse.connection._dataRecv(mock.createRequest(stanza));
  813. await u.waitUntil(() => _converse.onMAMPreferences.calls.count());
  814. expect(_converse.onMAMPreferences).toHaveBeenCalled();
  815. sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(s => sizzle('iq[type="set"] prefs[xmlns="urn:xmpp:mam:2"]', s).length).pop());
  816. expect(Strophe.serialize(sent_stanza)).toBe(
  817. `<iq id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
  818. `<prefs default="never" xmlns="urn:xmpp:mam:2">`+
  819. `<always><jid>romeo@montague.lit</jid></always>`+
  820. `<never><jid>montague@montague.lit</jid></never>`+
  821. `</prefs>`+
  822. `</iq>`
  823. );
  824. expect(feature.get('preference')).toBe(undefined);
  825. /* <iq type='result' id='juliet3'>
  826. * <prefs xmlns='urn:xmpp:mam:0' default='always'>
  827. * <always>
  828. * <jid>romeo@montague.lit</jid>
  829. * </always>
  830. * <never>
  831. * <jid>montague@montague.lit</jid>
  832. * </never>
  833. * </prefs>
  834. * </iq>
  835. */
  836. stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')})
  837. .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'always'})
  838. .c('always').up()
  839. .c('never');
  840. _converse.connection._dataRecv(mock.createRequest(stanza));
  841. await u.waitUntil(() => feature.save.calls.count());
  842. expect(feature.save).toHaveBeenCalled();
  843. expect(feature.get('preferences')['default']).toBe('never'); // eslint-disable-line dot-notation
  844. done();
  845. }));
  846. });
  847. });
  848. describe("Chatboxes", function () {
  849. describe("A Chatbox", function () {
  850. it("will fetch archived messages once it's opened",
  851. mock.initConverse(['discoInitialized'], {}, async function (done, _converse) {
  852. await mock.waitForRoster(_converse, 'current', 1);
  853. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  854. await mock.openChatBoxFor(_converse, contact_jid);
  855. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  856. let sent_stanza, IQ_id;
  857. const sendIQ = _converse.connection.sendIQ;
  858. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  859. sent_stanza = iq;
  860. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  861. });
  862. await u.waitUntil(() => sent_stanza);
  863. const stanza_el = sent_stanza.root().nodeTree;
  864. const queryid = stanza_el.querySelector('query').getAttribute('queryid');
  865. expect(sent_stanza.toString()).toBe(
  866. `<iq id="${stanza_el.getAttribute('id')}" type="set" xmlns="jabber:client">`+
  867. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  868. `<x type="submit" xmlns="jabber:x:data">`+
  869. `<field type="hidden" var="FORM_TYPE"><value>urn:xmpp:mam:2</value></field>`+
  870. `<field var="with"><value>mercutio@montague.lit</value></field>`+
  871. `</x>`+
  872. `<set xmlns="http://jabber.org/protocol/rsm"><max>50</max><before></before></set>`+
  873. `</query>`+
  874. `</iq>`
  875. );
  876. const msg1 = $msg({'id':'aeb212', 'to': contact_jid})
  877. .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73623'})
  878. .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
  879. .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
  880. .c('message', {
  881. 'xmlns':'jabber:client',
  882. 'to': contact_jid,
  883. 'from': _converse.bare_jid,
  884. 'type':'chat' })
  885. .c('body').t("Call me but love, and I'll be new baptized;");
  886. _converse.connection._dataRecv(mock.createRequest(msg1));
  887. const msg2 = $msg({'id':'aeb213', 'to': contact_jid})
  888. .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73624'})
  889. .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
  890. .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
  891. .c('message', {
  892. 'xmlns':'jabber:client',
  893. 'to': contact_jid,
  894. 'from': _converse.bare_jid,
  895. 'type':'chat' })
  896. .c('body').t("Henceforth I never will be Romeo.");
  897. _converse.connection._dataRecv(mock.createRequest(msg2));
  898. const stanza = $iq({'type': 'result', 'id': IQ_id})
  899. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  900. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  901. .c('first', {'index': '0'}).t('23452-4534-1').up()
  902. .c('last').t('09af3-cc343-b409f').up()
  903. .c('count').t('16');
  904. _converse.connection._dataRecv(mock.createRequest(stanza));
  905. done();
  906. }));
  907. it("will show an error message if the MAM query times out",
  908. mock.initConverse(['discoInitialized'], {}, async function (done, _converse) {
  909. const sendIQ = _converse.connection.sendIQ;
  910. let timeout_happened = false;
  911. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  912. sendIQ.bind(this)(iq, callback, errback);
  913. if (!timeout_happened) {
  914. if (typeof(iq.tree) === "function") {
  915. iq = iq.tree();
  916. }
  917. if (sizzle('query[xmlns="urn:xmpp:mam:2"]', iq).length) {
  918. // We emulate a timeout event
  919. callback(null);
  920. timeout_happened = true;
  921. }
  922. }
  923. });
  924. await mock.waitForRoster(_converse, 'current', 1);
  925. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  926. await mock.openChatBoxFor(_converse, contact_jid);
  927. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  928. const IQ_stanzas = _converse.connection.IQ_stanzas;
  929. let sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle('query[xmlns="urn:xmpp:mam:2"]', iq).length).pop());
  930. let queryid = sent_stanza.querySelector('query').getAttribute('queryid');
  931. expect(Strophe.serialize(sent_stanza)).toBe(
  932. `<iq id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
  933. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  934. `<x type="submit" xmlns="jabber:x:data">`+
  935. `<field type="hidden" var="FORM_TYPE"><value>urn:xmpp:mam:2</value></field>`+
  936. `<field var="with"><value>mercutio@montague.lit</value></field>`+
  937. `</x>`+
  938. `<set xmlns="http://jabber.org/protocol/rsm"><max>50</max><before></before></set>`+
  939. `</query>`+
  940. `</iq>`);
  941. const view = _converse.chatboxviews.get(contact_jid);
  942. expect(view.model.messages.length).toBe(1);
  943. expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
  944. expect(view.model.messages.at(0).get('type')).toBe('error');
  945. expect(view.model.messages.at(0).get('message')).toBe('Timeout while trying to fetch archived messages.');
  946. let err_message = view.el.querySelector('.message.chat-error');
  947. err_message.querySelector('.retry').click();
  948. expect(err_message.querySelector('.spinner')).not.toBe(null);
  949. while (_converse.connection.IQ_stanzas.length) {
  950. _converse.connection.IQ_stanzas.pop();
  951. }
  952. sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle('query[xmlns="urn:xmpp:mam:2"]', iq).length).pop());
  953. queryid = sent_stanza.querySelector('query').getAttribute('queryid');
  954. expect(Strophe.serialize(sent_stanza)).toBe(
  955. `<iq id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
  956. `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
  957. `<x type="submit" xmlns="jabber:x:data">`+
  958. `<field type="hidden" var="FORM_TYPE"><value>urn:xmpp:mam:2</value></field>`+
  959. `<field var="with"><value>mercutio@montague.lit</value></field>`+
  960. `</x>`+
  961. `<set xmlns="http://jabber.org/protocol/rsm"><max>50</max><before></before></set>`+
  962. `</query>`+
  963. `</iq>`);
  964. const msg1 = $msg({'id':'aeb212', 'to': contact_jid})
  965. .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid': queryid, 'id':'28482-98726-73623'})
  966. .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
  967. .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
  968. .c('message', {
  969. 'xmlns':'jabber:client',
  970. 'to': contact_jid,
  971. 'from': _converse.bare_jid,
  972. 'type':'chat' })
  973. .c('body').t("Call me but love, and I'll be new baptized;");
  974. _converse.connection._dataRecv(mock.createRequest(msg1));
  975. const msg2 = $msg({'id':'aeb213', 'to': contact_jid})
  976. .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid': queryid, 'id':'28482-98726-73624'})
  977. .c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
  978. .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:18:25Z'}).up()
  979. .c('message', {
  980. 'xmlns':'jabber:client',
  981. 'to': contact_jid,
  982. 'from': _converse.bare_jid,
  983. 'type':'chat' })
  984. .c('body').t("Henceforth I never will be Romeo.");
  985. _converse.connection._dataRecv(mock.createRequest(msg2));
  986. const stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')})
  987. .c('fin', {'xmlns': 'urn:xmpp:mam:2', 'complete': true})
  988. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  989. .c('first', {'index': '0'}).t('28482-98726-73623').up()
  990. .c('last').t('28482-98726-73624').up()
  991. .c('count').t('2');
  992. _converse.connection._dataRecv(mock.createRequest(stanza));
  993. await u.waitUntil(() => view.model.messages.length === 2, 500);
  994. err_message = view.el.querySelector('.message.chat-error');
  995. expect(err_message).toBe(null);
  996. done();
  997. }));
  998. });
  999. });