retractions.js 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936
  1. (function (root, factory) {
  2. define([
  3. "jasmine",
  4. "mock",
  5. "test-utils"
  6. ], factory);
  7. } (this, function (jasmine, mock, test_utils) {
  8. "use strict";
  9. const { Strophe, $iq } = converse.env;
  10. const u = converse.env.utils;
  11. async function sendAndThenRetractMessage (_converse, view) {
  12. view.model.sendMessage('hello world');
  13. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  14. const msg_obj = view.model.messages.at(0);
  15. const reflection_stanza = u.toStanza(`
  16. <message xmlns="jabber:client"
  17. from="${msg_obj.get('from')}"
  18. to="${_converse.connection.jid}"
  19. type="groupchat">
  20. <msg_body>${msg_obj.get('message')}</msg_body>
  21. <stanza-id xmlns="urn:xmpp:sid:0"
  22. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  23. by="lounge@montague.lit"/>
  24. <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
  25. </message>`);
  26. await view.model.onMessage(reflection_stanza);
  27. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
  28. const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
  29. retract_button.click();
  30. await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
  31. const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
  32. submit_button.click();
  33. const sent_stanzas = _converse.connection.sent_stanzas;
  34. return u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('message apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
  35. }
  36. describe("Message Retractions", function () {
  37. describe("A groupchat message retraction", function () {
  38. it("is not applied if it's not from the right author",
  39. mock.initConverse(
  40. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  41. async function (done, _converse) {
  42. const muc_jid = 'lounge@montague.lit';
  43. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  44. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  45. const received_stanza = u.toStanza(`
  46. <message to='${_converse.jid}' from='${muc_jid}/eve' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  47. <body>Hello world</body>
  48. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  49. </message>
  50. `);
  51. const view = _converse.api.chatviews.get(muc_jid);
  52. await view.model.onMessage(received_stanza);
  53. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  54. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  55. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  56. const retraction_stanza = u.toStanza(`
  57. <message type="groupchat" id='retraction-id-1' from="${muc_jid}/mallory" to="${muc_jid}/romeo">
  58. <apply-to id="stanza-id-1" xmlns="urn:xmpp:fasten:0">
  59. <retract xmlns="urn:xmpp:message-retract:0" />
  60. </apply-to>
  61. </message>
  62. `);
  63. spyOn(view.model, 'handleRetraction').and.callThrough();
  64. _converse.connection._dataRecv(test_utils.createRequest(retraction_stanza));
  65. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
  66. expect(view.model.handleRetraction.calls.first().returnValue).toBe(true);
  67. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  68. expect(view.model.messages.length).toBe(2);
  69. expect(view.model.messages.at(1).get('retracted')).toBeTruthy();
  70. expect(view.model.messages.at(1).get('is_ephemeral')).toBeFalsy();
  71. expect(view.model.messages.at(1).get('dangling_retraction')).toBe(true);
  72. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  73. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  74. done();
  75. }));
  76. it("can be received before the message it pertains to",
  77. mock.initConverse(
  78. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  79. async function (done, _converse) {
  80. const date = (new Date()).toISOString();
  81. const muc_jid = 'lounge@montague.lit';
  82. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  83. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  84. const retraction_stanza = u.toStanza(`
  85. <message type="groupchat" id='retraction-id-1' from="${muc_jid}/eve" to="${muc_jid}/romeo">
  86. <apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
  87. <retract by="${muc_jid}/eve" xmlns="urn:xmpp:message-retract:0" />
  88. </apply-to>
  89. </message>
  90. `);
  91. const view = _converse.api.chatviews.get(muc_jid);
  92. spyOn(converse.env.log, 'warn');
  93. spyOn(view.model, 'handleRetraction').and.callThrough();
  94. _converse.connection._dataRecv(test_utils.createRequest(retraction_stanza));
  95. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
  96. await u.waitUntil(() => view.model.messages.length === 1);
  97. expect(view.model.handleRetraction.calls.first().returnValue).toBe(true);
  98. expect(view.model.messages.length).toBe(1);
  99. expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
  100. expect(view.model.messages.at(0).get('dangling_retraction')).toBe(true);
  101. const received_stanza = u.toStanza(`
  102. <message to='${_converse.jid}' from='${muc_jid}/eve' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  103. <body>Hello world</body>
  104. <delay xmlns='urn:xmpp:delay' stamp='${date}'/>
  105. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  106. <origin-id xmlns="urn:xmpp:sid:0" id="origin-id-1"/>
  107. </message>
  108. `);
  109. _converse.connection._dataRecv(test_utils.createRequest(received_stanza));
  110. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
  111. expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
  112. expect(view.model.messages.length).toBe(1);
  113. const message = view.model.messages.at(0)
  114. expect(message.get('retracted')).toBeTruthy();
  115. expect(message.get('dangling_retraction')).toBe(false);
  116. expect(message.get('origin_id')).toBe('origin-id-1');
  117. expect(message.get(`stanza_id ${muc_jid}`)).toBe('stanza-id-1');
  118. expect(message.get('time')).toBe(date);
  119. expect(message.get('type')).toBe('groupchat');
  120. expect(view.model.handleRetraction.calls.all().pop().returnValue).toBe(true);
  121. done();
  122. }));
  123. });
  124. describe("A message retraction", function () {
  125. it("can be received before the message it pertains to",
  126. mock.initConverse(
  127. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  128. async function (done, _converse) {
  129. const date = (new Date()).toISOString();
  130. await test_utils.waitForRoster(_converse, 'current', 1);
  131. await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
  132. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  133. const view = await test_utils.openChatBoxFor(_converse, contact_jid);
  134. spyOn(view.model, 'handleRetraction').and.callThrough();
  135. const retraction_stanza = u.toStanza(`
  136. <message id="${u.getUniqueId()}"
  137. to="${_converse.bare_jid}"
  138. from="${contact_jid}"
  139. type="chat"
  140. xmlns="jabber:client">
  141. <apply-to id="2e972ea0-0050-44b7-a830-f6638a2595b3" xmlns="urn:xmpp:fasten:0">
  142. <retract xmlns="urn:xmpp:message-retract:0"/>
  143. </apply-to>
  144. </message>
  145. `);
  146. const promise = new Promise(resolve => _converse.api.listen.on('messageAdded', resolve));
  147. _converse.connection._dataRecv(test_utils.createRequest(retraction_stanza));
  148. await u.waitUntil(() => view.model.messages.length === 1);
  149. await promise;
  150. const message = view.model.messages.at(0);
  151. expect(message.get('dangling_retraction')).toBe(true);
  152. expect(message.get('is_ephemeral')).toBe(false);
  153. expect(message.get('retracted')).toBeTruthy();
  154. expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
  155. const stanza = u.toStanza(`
  156. <message xmlns="jabber:client"
  157. to="${_converse.bare_jid}"
  158. type="chat"
  159. id="2e972ea0-0050-44b7-a830-f6638a2595b3"
  160. from="${contact_jid}">
  161. <body>Hello world</body>
  162. <delay xmlns='urn:xmpp:delay' stamp='${date}'/>
  163. <markable xmlns="urn:xmpp:chat-markers:0"/>
  164. <origin-id xmlns="urn:xmpp:sid:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
  165. <stanza-id xmlns="urn:xmpp:sid:0" id="IxVDLJ0RYbWcWvqC" by="${_converse.bare_jid}"/>
  166. </message>`);
  167. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  168. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
  169. expect(view.model.messages.length).toBe(1);
  170. expect(message.get('retracted')).toBeTruthy();
  171. expect(message.get('dangling_retraction')).toBe(false);
  172. expect(message.get('origin_id')).toBe('2e972ea0-0050-44b7-a830-f6638a2595b3');
  173. expect(message.get('time')).toBe(date);
  174. expect(message.get('type')).toBe('chat');
  175. done();
  176. }));
  177. });
  178. describe("A Received Chat Message", function () {
  179. it("can be followed up by a retraction",
  180. mock.initConverse(
  181. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  182. async function (done, _converse) {
  183. await test_utils.waitForRoster(_converse, 'current', 1);
  184. await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
  185. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  186. const view = await test_utils.openChatBoxFor(_converse, contact_jid);
  187. let stanza = u.toStanza(`
  188. <message xmlns="jabber:client"
  189. to="${_converse.bare_jid}"
  190. type="chat"
  191. id="29132ea0-0121-2897-b121-36638c259554"
  192. from="${contact_jid}">
  193. <body>😊</body>
  194. <markable xmlns="urn:xmpp:chat-markers:0"/>
  195. <origin-id xmlns="urn:xmpp:sid:0" id="29132ea0-0121-2897-b121-36638c259554"/>
  196. <stanza-id xmlns="urn:xmpp:sid:0" id="kxViLhgbnNMcWv10" by="${_converse.bare_jid}"/>
  197. </message>`);
  198. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  199. await u.waitUntil(() => view.model.messages.length === 1);
  200. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  201. stanza = u.toStanza(`
  202. <message xmlns="jabber:client"
  203. to="${_converse.bare_jid}"
  204. type="chat"
  205. id="2e972ea0-0050-44b7-a830-f6638a2595b3"
  206. from="${contact_jid}">
  207. <body>This message will be retracted</body>
  208. <markable xmlns="urn:xmpp:chat-markers:0"/>
  209. <origin-id xmlns="urn:xmpp:sid:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
  210. <stanza-id xmlns="urn:xmpp:sid:0" id="IxVDLJ0RYbWcWvqC" by="${_converse.bare_jid}"/>
  211. </message>`);
  212. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  213. await u.waitUntil(() => view.model.messages.length === 2);
  214. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
  215. const retraction_stanza = u.toStanza(`
  216. <message id="${u.getUniqueId()}"
  217. to="${_converse.bare_jid}"
  218. from="${contact_jid}"
  219. type="chat"
  220. xmlns="jabber:client">
  221. <apply-to id="2e972ea0-0050-44b7-a830-f6638a2595b3" xmlns="urn:xmpp:fasten:0">
  222. <retract xmlns="urn:xmpp:message-retract:0"/>
  223. </apply-to>
  224. </message>
  225. `);
  226. _converse.connection._dataRecv(test_utils.createRequest(retraction_stanza));
  227. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  228. expect(view.model.messages.length).toBe(2);
  229. const message = view.model.messages.at(1);
  230. expect(message.get('retracted')).toBeTruthy();
  231. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  232. const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
  233. expect(msg_el.textContent.trim()).toBe('Mercutio has retracted this message');
  234. expect(u.hasClass('chat-msg--followup', view.el.querySelector('.chat-msg--retracted'))).toBe(true);
  235. done();
  236. }));
  237. });
  238. describe("A Sent Chat Message", function () {
  239. it("can be retracted by its author",
  240. mock.initConverse(
  241. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  242. async function (done, _converse) {
  243. await test_utils.waitForRoster(_converse, 'current', 1);
  244. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  245. const view = await test_utils.openChatBoxFor(_converse, contact_jid);
  246. view.model.sendMessage('hello world');
  247. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  248. const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
  249. retract_button.click();
  250. await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
  251. const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
  252. submit_button.click();
  253. const sent_stanzas = _converse.connection.sent_stanzas;
  254. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  255. const msg_obj = view.model.messages.at(0);
  256. const retraction_stanza = await u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('message apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
  257. expect(Strophe.serialize(retraction_stanza)).toBe(
  258. `<message id="${retraction_stanza.getAttribute('id')}" to="${contact_jid}" type="chat" xmlns="jabber:client">`+
  259. `<store xmlns="urn:xmpp:hints"/>`+
  260. `<apply-to id="${msg_obj.get('origin_id')}" xmlns="urn:xmpp:fasten:0">`+
  261. `<retract xmlns="urn:xmpp:message-retract:0"/>`+
  262. `</apply-to>`+
  263. `</message>`);
  264. const message = view.model.messages.at(0);
  265. expect(view.model.messages.length).toBe(1);
  266. expect(message.get('retracted')).toBeTruthy();
  267. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  268. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
  269. expect(el.textContent.trim()).toBe('Romeo Montague has retracted this message');
  270. done();
  271. }));
  272. });
  273. describe("A Received Groupchat Message", function () {
  274. it("can be followed up by a retraction by the author",
  275. mock.initConverse(
  276. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  277. async function (done, _converse) {
  278. const muc_jid = 'lounge@montague.lit';
  279. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  280. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  281. const received_stanza = u.toStanza(`
  282. <message to='${_converse.jid}' from='${muc_jid}/eve' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  283. <body>Hello world</body>
  284. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  285. <origin-id xmlns='urn:xmpp:sid:0' id='origin-id-1' by='${muc_jid}'/>
  286. </message>
  287. `);
  288. const view = _converse.api.chatviews.get(muc_jid);
  289. await view.model.onMessage(received_stanza);
  290. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  291. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  292. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  293. const retraction_stanza = u.toStanza(`
  294. <message type="groupchat" id='retraction-id-1' from="${muc_jid}/eve" to="${muc_jid}/romeo">
  295. <apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
  296. <retract by="${muc_jid}/eve" xmlns="urn:xmpp:message-retract:0" />
  297. </apply-to>
  298. </message>
  299. `);
  300. _converse.connection._dataRecv(test_utils.createRequest(retraction_stanza));
  301. // We opportunistically save the message as retracted, even before receiving the retraction message
  302. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  303. expect(view.model.messages.length).toBe(1);
  304. expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
  305. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  306. const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
  307. expect(msg_el.textContent.trim()).toBe('eve has retracted this message');
  308. expect(msg_el.querySelector('.chat-msg--retracted q')).toBe(null);
  309. done();
  310. }));
  311. it("can be retracted by a moderator, with the IQ response received before the retraction message",
  312. mock.initConverse(
  313. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  314. async function (done, _converse) {
  315. const muc_jid = 'lounge@montague.lit';
  316. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  317. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  318. const view = _converse.api.chatviews.get(muc_jid);
  319. const occupant = view.model.getOwnOccupant();
  320. expect(occupant.get('role')).toBe('moderator');
  321. const received_stanza = u.toStanza(`
  322. <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  323. <body>Visit this site to get free Bitcoin!</body>
  324. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  325. </message>
  326. `);
  327. await view.model.onMessage(received_stanza);
  328. await u.waitUntil(() => view.model.messages.length === 1);
  329. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  330. const reason = "This content is inappropriate for this forum!"
  331. const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
  332. retract_button.click();
  333. await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
  334. const reason_input = document.querySelector('#converse-modals .modal input[name="reason"]');
  335. reason_input.value = 'This content is inappropriate for this forum!';
  336. const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
  337. submit_button.click();
  338. const sent_IQs = _converse.connection.IQ_stanzas;
  339. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
  340. const message = view.model.messages.at(0);
  341. const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
  342. expect(Strophe.serialize(stanza)).toBe(
  343. `<iq id="${stanza.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
  344. `<apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">`+
  345. `<moderate xmlns="urn:xmpp:message-moderate:0">`+
  346. `<retract xmlns="urn:xmpp:message-retract:0"/>`+
  347. `<reason>This content is inappropriate for this forum!</reason>`+
  348. `</moderate>`+
  349. `</apply-to>`+
  350. `</iq>`);
  351. const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'});
  352. _converse.connection._dataRecv(test_utils.createRequest(result_iq));
  353. // We opportunistically save the message as retracted, even before receiving the retraction message
  354. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  355. expect(view.model.messages.length).toBe(1);
  356. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  357. expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
  358. expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
  359. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  360. const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
  361. expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has retracted this message from mallory');
  362. const qel = msg_el.querySelector('q');
  363. expect(qel.textContent.trim()).toBe('This content is inappropriate for this forum!');
  364. // The server responds with a retraction message
  365. const retraction = u.toStanza(`
  366. <message type="groupchat" id='retraction-id-1' from="${muc_jid}" to="${muc_jid}/romeo">
  367. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  368. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  369. <retract xmlns='urn:xmpp:message-retract:0' />
  370. <reason>${reason}</reason>
  371. </moderated>
  372. </apply-to>
  373. </message>`);
  374. await view.model.onMessage(retraction);
  375. expect(view.model.messages.length).toBe(1);
  376. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  377. expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
  378. expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
  379. done();
  380. }));
  381. it("can not be retracted if the MUC doesn't support message moderation",
  382. mock.initConverse(
  383. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  384. async function (done, _converse) {
  385. const muc_jid = 'lounge@montague.lit';
  386. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  387. const view = _converse.api.chatviews.get(muc_jid);
  388. const occupant = view.model.getOwnOccupant();
  389. expect(occupant.get('role')).toBe('moderator');
  390. const received_stanza = u.toStanza(`
  391. <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  392. <body>Visit this site to get free Bitcoin!</body>
  393. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  394. </message>
  395. `);
  396. await view.model.onMessage(received_stanza);
  397. await u.waitUntil(() => view.el.querySelector('.chat-msg__content'));
  398. expect(view.el.querySelector('.chat-msg__content .chat-msg__action-retract')).toBe(null);
  399. const result = await view.model.canRetractMessages();
  400. expect(result).toBe(false);
  401. done();
  402. }));
  403. it("can be retracted by a moderator, with the retraction message received before the IQ response",
  404. mock.initConverse(
  405. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  406. async function (done, _converse) {
  407. const muc_jid = 'lounge@montague.lit';
  408. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  409. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  410. const view = _converse.api.chatviews.get(muc_jid);
  411. const occupant = view.model.getOwnOccupant();
  412. expect(occupant.get('role')).toBe('moderator');
  413. const received_stanza = u.toStanza(`
  414. <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  415. <body>Visit this site to get free Bitcoin!</body>
  416. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  417. </message>
  418. `);
  419. await view.model.onMessage(received_stanza);
  420. await u.waitUntil(() => view.model.messages.length === 1);
  421. const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
  422. retract_button.click();
  423. await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
  424. const reason_input = document.querySelector('#converse-modals .modal input[name="reason"]');
  425. const reason = "This content is inappropriate for this forum!"
  426. reason_input.value = reason;
  427. const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
  428. submit_button.click();
  429. const sent_IQs = _converse.connection.IQ_stanzas;
  430. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
  431. const message = view.model.messages.at(0);
  432. const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
  433. // The server responds with a retraction message
  434. const retraction = u.toStanza(`
  435. <message type="groupchat" id='retraction-id-1' from="${muc_jid}" to="${muc_jid}/romeo">
  436. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  437. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  438. <retract xmlns='urn:xmpp:message-retract:0' />
  439. <reason>${reason}</reason>
  440. </moderated>
  441. </apply-to>
  442. </message>`);
  443. await view.model.onMessage(retraction);
  444. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  445. expect(view.model.messages.length).toBe(1);
  446. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  447. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  448. const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  449. expect(msg_el.textContent).toBe('romeo has retracted this message from mallory');
  450. const qel = view.el.querySelector('.chat-msg--retracted .chat-msg__message q');
  451. expect(qel.textContent).toBe('This content is inappropriate for this forum!');
  452. const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'});
  453. _converse.connection._dataRecv(test_utils.createRequest(result_iq));
  454. expect(view.model.messages.length).toBe(1);
  455. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  456. expect(view.model.messages.at(0).get('moderated_by')).toBe(_converse.bare_jid);
  457. expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
  458. done();
  459. }));
  460. });
  461. describe("A Sent Groupchat Message", function () {
  462. it("can be retracted by its author",
  463. mock.initConverse(
  464. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  465. async function (done, _converse) {
  466. const muc_jid = 'lounge@montague.lit';
  467. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  468. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  469. const view = _converse.api.chatviews.get(muc_jid);
  470. const occupant = view.model.getOwnOccupant();
  471. expect(occupant.get('role')).toBe('moderator');
  472. occupant.save('role', 'member');
  473. const retraction_stanza = await sendAndThenRetractMessage(_converse, view);
  474. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  475. const msg_obj = view.model.messages.at(0);
  476. expect(Strophe.serialize(retraction_stanza)).toBe(
  477. `<message id="${retraction_stanza.getAttribute('id')}" to="${muc_jid}" type="groupchat" xmlns="jabber:client">`+
  478. `<store xmlns="urn:xmpp:hints"/>`+
  479. `<apply-to id="${msg_obj.get('origin_id')}" xmlns="urn:xmpp:fasten:0">`+
  480. `<retract xmlns="urn:xmpp:message-retract:0"/>`+
  481. `</apply-to>`+
  482. `</message>`);
  483. const message = view.model.messages.at(0);
  484. expect(message.get('retracted')).toBeTruthy();
  485. expect(message.get('is_ephemeral')).toBe(false);
  486. const stanza_id = message.get(`stanza_id ${muc_jid}`);
  487. // The server responds with a retraction message
  488. const reflection = u.toStanza(`
  489. <message type="groupchat" id="${retraction_stanza.getAttribute('id')}" from="${muc_jid}" to="${muc_jid}/romeo">
  490. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  491. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  492. <retract xmlns='urn:xmpp:message-retract:0' />
  493. </moderated>
  494. </apply-to>
  495. </message>`);
  496. spyOn(view.model, 'handleRetraction').and.callThrough();
  497. _converse.connection._dataRecv(test_utils.createRequest(reflection));
  498. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
  499. expect(view.model.messages.length).toBe(1);
  500. expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
  501. expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
  502. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  503. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  504. expect(el.textContent).toBe('romeo has retracted this message');
  505. done();
  506. }));
  507. it("can be retracted by its author, causing an error message in response",
  508. mock.initConverse(
  509. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  510. async function (done, _converse) {
  511. const muc_jid = 'lounge@montague.lit';
  512. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  513. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  514. const view = _converse.api.chatviews.get(muc_jid);
  515. const occupant = view.model.getOwnOccupant();
  516. expect(occupant.get('role')).toBe('moderator');
  517. occupant.save('role', 'member');
  518. const retraction_stanza = await sendAndThenRetractMessage(_converse, view);
  519. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  520. expect(view.model.messages.length).toBe(1);
  521. expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
  522. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  523. expect(el.textContent.trim()).toBe('romeo has retracted this message');
  524. const message = view.model.messages.at(0);
  525. const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
  526. // The server responds with an error message
  527. const error = u.toStanza(`
  528. <message type="error" id="${retraction_stanza.getAttribute('id')}" from="${muc_jid}" to="${view.model.get('jid')}/romeo">
  529. <error by='${muc_jid}' type='auth'>
  530. <forbidden xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
  531. </error>
  532. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  533. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  534. <retract xmlns='urn:xmpp:message-retract:0' />
  535. </moderated>
  536. </apply-to>
  537. </message>`);
  538. _converse.connection._dataRecv(test_utils.createRequest(error));
  539. await u.waitUntil(() => view.el.querySelectorAll('.chat-error').length === 1);
  540. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
  541. expect(view.model.messages.length).toBe(1);
  542. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  543. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  544. expect(view.el.querySelectorAll('.chat-error').length).toBe(1);
  545. const errmsg = view.el.querySelector('.chat-error');
  546. expect(errmsg.textContent).toBe("Sorry, something went wrong while trying to retract your message.");
  547. done();
  548. }));
  549. it("can be retracted by its author, causing an timeout error in response",
  550. mock.initConverse(
  551. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  552. async function (done, _converse) {
  553. _converse.STANZA_TIMEOUT = 1;
  554. const muc_jid = 'lounge@montague.lit';
  555. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  556. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  557. const view = _converse.api.chatviews.get(muc_jid);
  558. const occupant = view.model.getOwnOccupant();
  559. expect(occupant.get('role')).toBe('moderator');
  560. occupant.save('role', 'member');
  561. await sendAndThenRetractMessage(_converse, view);
  562. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  563. expect(view.model.messages.length).toBe(1);
  564. expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
  565. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  566. expect(el.textContent.trim()).toBe('romeo has retracted this message');
  567. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  568. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
  569. expect(view.model.messages.length).toBe(1);
  570. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  571. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  572. const error_messages = view.el.querySelectorAll('.chat-error');
  573. expect(error_messages.length).toBe(2);
  574. expect(error_messages[0].textContent).toBe("Sorry, something went wrong while trying to retract your message.");
  575. expect(error_messages[1].textContent).toBe("Timeout Error: No response from server");
  576. done();
  577. }));
  578. });
  579. describe("when archived", function () {
  580. it("may be returned as a tombstone message",
  581. mock.initConverse(
  582. ['discoInitialized'], {},
  583. async function (done, _converse) {
  584. await test_utils.waitForRoster(_converse, 'current', 1);
  585. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  586. await test_utils.openChatBoxFor(_converse, contact_jid);
  587. await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  588. const sent_IQs = _converse.connection.IQ_stanzas;
  589. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
  590. const queryid = stanza.querySelector('query').getAttribute('queryid');
  591. const view = _converse.chatboxviews.get(contact_jid);
  592. const first_id = u.getUniqueId();
  593. spyOn(view.model, 'handleRetraction').and.callThrough();
  594. const first_message = u.toStanza(`
  595. <message id='${u.getUniqueId()}' to='${_converse.jid}'>
  596. <result xmlns='urn:xmpp:mam:2' queryid='${queryid}' id="${first_id}">
  597. <forwarded xmlns='urn:xmpp:forward:0'>
  598. <delay xmlns='urn:xmpp:delay' stamp='2019-09-20T23:01:15Z'/>
  599. <message type="chat" from="${contact_jid}" to="${_converse.bare_jid}" id="message-id-0">
  600. <origin-id xmlns='urn:xmpp:sid:0' id="origin-id-0"/>
  601. <body>😊</body>
  602. </message>
  603. </forwarded>
  604. </result>
  605. </message>
  606. `);
  607. _converse.connection._dataRecv(test_utils.createRequest(first_message));
  608. const tombstone = u.toStanza(`
  609. <message id='${u.getUniqueId()}' to='${_converse.jid}'>
  610. <result xmlns='urn:xmpp:mam:2' queryid='${queryid}' id="${u.getUniqueId()}">
  611. <forwarded xmlns='urn:xmpp:forward:0'>
  612. <delay xmlns='urn:xmpp:delay' stamp='2019-09-20T23:08:25Z'/>
  613. <message type="chat" from="${contact_jid}" to="${_converse.bare_jid}" id="message-id-1">
  614. <origin-id xmlns='urn:xmpp:sid:0' id="origin-id-1"/>
  615. <retracted stamp='2019-09-20T23:09:32Z' xmlns='urn:xmpp:message-retract:0'/>
  616. </message>
  617. </forwarded>
  618. </result>
  619. </message>
  620. `);
  621. _converse.connection._dataRecv(test_utils.createRequest(tombstone));
  622. const last_id = u.getUniqueId();
  623. const retraction = u.toStanza(`
  624. <message id='${u.getUniqueId()}' to='${_converse.jid}'>
  625. <result xmlns='urn:xmpp:mam:2' queryid='${queryid}' id="${last_id}">
  626. <forwarded xmlns='urn:xmpp:forward:0'>
  627. <delay xmlns='urn:xmpp:delay' stamp='2019-09-20T23:08:25Z'/>
  628. <message from="${contact_jid}" to='${_converse.bare_jid}' id='retract-message-1'>
  629. <apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
  630. <retract xmlns='urn:xmpp:message-retract:0'/>
  631. </apply-to>
  632. </message>
  633. </forwarded>
  634. </result>
  635. </message>
  636. `);
  637. _converse.connection._dataRecv(test_utils.createRequest(retraction));
  638. const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
  639. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  640. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  641. .c('first', {'index': '0'}).t(first_id).up()
  642. .c('last').t(last_id).up()
  643. .c('count').t('2');
  644. _converse.connection._dataRecv(test_utils.createRequest(iq_result));
  645. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 3);
  646. expect(view.model.messages.length).toBe(2);
  647. const message = view.model.messages.at(1);
  648. expect(message.get('retracted')).toBeTruthy();
  649. expect(message.get('is_tombstone')).toBe(true);
  650. expect(view.model.handleRetraction.calls.first().returnValue).toBe(false);
  651. expect(view.model.handleRetraction.calls.all()[1].returnValue).toBe(false);
  652. expect(view.model.handleRetraction.calls.all()[2].returnValue).toBe(true);
  653. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  654. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  655. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  656. expect(el.textContent.trim()).toBe('Mercutio has retracted this message');
  657. expect(u.hasClass('chat-msg--followup', el.parentElement)).toBe(false);
  658. done();
  659. }));
  660. it("may be returned as a tombstone groupchat message",
  661. mock.initConverse(
  662. ['discoInitialized'], {},
  663. async function (done, _converse) {
  664. const muc_jid = 'lounge@montague.lit';
  665. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  666. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  667. const view = _converse.chatboxviews.get(muc_jid);
  668. const sent_IQs = _converse.connection.IQ_stanzas;
  669. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
  670. const queryid = stanza.querySelector('query').getAttribute('queryid');
  671. const first_id = u.getUniqueId();
  672. const tombstone = u.toStanza(`
  673. <message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
  674. <result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="stanza-id">
  675. <forwarded xmlns="urn:xmpp:forward:0">
  676. <delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
  677. <message type="groupchat" from="${muc_jid}/eve" to="${_converse.bare_jid}" id="message-id-1">
  678. <origin-id xmlns='urn:xmpp:sid:0' id="origin-id-1"/>
  679. <retracted stamp="2019-09-20T23:09:32Z" xmlns="urn:xmpp:message-retract:0"/>
  680. </message>
  681. </forwarded>
  682. </result>
  683. </message>
  684. `);
  685. spyOn(view.model, 'handleRetraction').and.callThrough();
  686. const promise = new Promise(resolve => _converse.api.listen.once('messageAdded', resolve));
  687. _converse.connection._dataRecv(test_utils.createRequest(tombstone));
  688. const last_id = u.getUniqueId();
  689. const retraction = u.toStanza(`
  690. <message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
  691. <result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="${last_id}">
  692. <forwarded xmlns="urn:xmpp:forward:0">
  693. <delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
  694. <message type="groupchat" from="${muc_jid}/eve" to="${_converse.bare_jid}" id="retract-message-1">
  695. <apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
  696. <retract xmlns="urn:xmpp:message-retract:0"/>
  697. </apply-to>
  698. </message>
  699. </forwarded>
  700. </result>
  701. </message>
  702. `);
  703. _converse.connection._dataRecv(test_utils.createRequest(retraction));
  704. const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
  705. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  706. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  707. .c('first', {'index': '0'}).t(first_id).up()
  708. .c('last').t(last_id).up()
  709. .c('count').t('2');
  710. _converse.connection._dataRecv(test_utils.createRequest(iq_result));
  711. await promise;
  712. expect(view.model.messages.length).toBe(1);
  713. let message = view.model.messages.at(0);
  714. expect(message.get('retracted')).toBeTruthy();
  715. expect(message.get('is_tombstone')).toBe(true);
  716. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
  717. expect(view.model.handleRetraction.calls.first().returnValue).toBe(false);
  718. expect(view.model.handleRetraction.calls.all()[1].returnValue).toBe(true);
  719. expect(view.model.messages.length).toBe(1);
  720. message = view.model.messages.at(0);
  721. expect(message.get('retracted')).toBeTruthy();
  722. expect(message.get('is_tombstone')).toBe(true);
  723. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  724. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  725. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  726. expect(el.textContent.trim()).toBe('eve has retracted this message');
  727. done();
  728. }));
  729. it("may be returned as a tombstone moderated groupchat message",
  730. mock.initConverse(
  731. ['discoInitialized', 'chatBoxesFetched'], {},
  732. async function (done, _converse) {
  733. const muc_jid = 'lounge@montague.lit';
  734. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  735. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  736. const view = _converse.chatboxviews.get(muc_jid);
  737. const sent_IQs = _converse.connection.IQ_stanzas;
  738. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
  739. const queryid = stanza.querySelector('query').getAttribute('queryid');
  740. const first_id = u.getUniqueId();
  741. const tombstone = u.toStanza(`
  742. <message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
  743. <result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="stanza-id">
  744. <forwarded xmlns="urn:xmpp:forward:0">
  745. <delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
  746. <message type="groupchat" from="${muc_jid}/eve" to="${_converse.bare_jid}" id="message-id-1">
  747. <moderated by="${muc_jid}/bob" stamp="2019-09-20T23:09:32Z" xmlns='urn:xmpp:message-moderate:0'>
  748. <retracted xmlns="urn:xmpp:message-retract:0"/>
  749. <reason>This message contains inappropriate content</reason>
  750. </moderated>
  751. </message>
  752. </forwarded>
  753. </result>
  754. </message>
  755. `);
  756. spyOn(view.model, 'handleModeration').and.callThrough();
  757. const promise = new Promise(resolve => _converse.api.listen.once('messageAdded', resolve));
  758. _converse.connection._dataRecv(test_utils.createRequest(tombstone));
  759. const last_id = u.getUniqueId();
  760. const retraction = u.toStanza(`
  761. <message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
  762. <result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="${last_id}">
  763. <forwarded xmlns="urn:xmpp:forward:0">
  764. <delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
  765. <message type="groupchat" from="${muc_jid}" to="${_converse.bare_jid}" id="retract-message-1">
  766. <apply-to id="stanza-id" xmlns="urn:xmpp:fasten:0">
  767. <moderated by="${muc_jid}/bob" xmlns='urn:xmpp:message-moderate:0'>
  768. <retract xmlns="urn:xmpp:message-retract:0"/>
  769. <reason>This message contains inappropriate content</reason>
  770. </moderated>
  771. </apply-to>
  772. </message>
  773. </forwarded>
  774. </result>
  775. </message>
  776. `);
  777. _converse.connection._dataRecv(test_utils.createRequest(retraction));
  778. const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
  779. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  780. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  781. .c('first', {'index': '0'}).t(first_id).up()
  782. .c('last').t(last_id).up()
  783. .c('count').t('2');
  784. _converse.connection._dataRecv(test_utils.createRequest(iq_result));
  785. await promise;
  786. expect(view.model.messages.length).toBe(1);
  787. let message = view.model.messages.at(0);
  788. expect(message.get('retracted')).toBeTruthy();
  789. expect(message.get('is_tombstone')).toBe(true);
  790. await u.waitUntil(() => view.model.handleModeration.calls.count() === 2);
  791. expect(view.model.handleModeration.calls.first().returnValue).toBe(false);
  792. expect(view.model.handleModeration.calls.all()[1].returnValue).toBe(true);
  793. expect(view.model.messages.length).toBe(1);
  794. message = view.model.messages.at(0);
  795. expect(message.get('retracted')).toBeTruthy();
  796. expect(message.get('is_tombstone')).toBe(true);
  797. expect(message.get('moderation_reason')).toBe("This message contains inappropriate content");
  798. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  799. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  800. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  801. expect(el.textContent.trim()).toBe('A moderator has retracted this message from eve');
  802. const qel = view.el.querySelector('.chat-msg--retracted .chat-msg__message q');
  803. expect(qel.textContent.trim()).toBe('This message contains inappropriate content');
  804. done();
  805. }));
  806. });
  807. })
  808. }));