retractions.js 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  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 removed 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 message = view.model.messages.at(0);
  249. expect(view.model.messages.length).toBe(1);
  250. expect(message.get('retracted')).toBeFalsy();
  251. expect(message.get('editable')).toBeTruthy();
  252. const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
  253. retract_button.click();
  254. await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
  255. const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
  256. submit_button.click();
  257. const sent_stanzas = _converse.connection.sent_stanzas;
  258. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  259. const msg_obj = view.model.messages.at(0);
  260. const retraction_stanza = await u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('message apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
  261. expect(Strophe.serialize(retraction_stanza)).toBe(
  262. `<message id="${retraction_stanza.getAttribute('id')}" to="${contact_jid}" type="chat" xmlns="jabber:client">`+
  263. `<store xmlns="urn:xmpp:hints"/>`+
  264. `<apply-to id="${msg_obj.get('origin_id')}" xmlns="urn:xmpp:fasten:0">`+
  265. `<retract xmlns="urn:xmpp:message-retract:0"/>`+
  266. `</apply-to>`+
  267. `</message>`);
  268. expect(view.model.messages.length).toBe(1);
  269. expect(message.get('retracted')).toBeTruthy();
  270. expect(message.get('editable')).toBeFalsy();
  271. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  272. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
  273. expect(el.textContent.trim()).toBe('Romeo Montague has removed this message');
  274. done();
  275. }));
  276. });
  277. describe("A Received Groupchat Message", function () {
  278. it("can be followed up by a retraction by the author",
  279. mock.initConverse(
  280. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  281. async function (done, _converse) {
  282. const muc_jid = 'lounge@montague.lit';
  283. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  284. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  285. const received_stanza = u.toStanza(`
  286. <message to='${_converse.jid}' from='${muc_jid}/eve' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  287. <body>Hello world</body>
  288. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  289. <origin-id xmlns='urn:xmpp:sid:0' id='origin-id-1' by='${muc_jid}'/>
  290. </message>
  291. `);
  292. const view = _converse.api.chatviews.get(muc_jid);
  293. await view.model.onMessage(received_stanza);
  294. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  295. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  296. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  297. const retraction_stanza = u.toStanza(`
  298. <message type="groupchat" id='retraction-id-1' from="${muc_jid}/eve" to="${muc_jid}/romeo">
  299. <apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
  300. <retract by="${muc_jid}/eve" xmlns="urn:xmpp:message-retract:0" />
  301. </apply-to>
  302. </message>
  303. `);
  304. _converse.connection._dataRecv(test_utils.createRequest(retraction_stanza));
  305. // We opportunistically save the message as retracted, even before receiving the retraction message
  306. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  307. expect(view.model.messages.length).toBe(1);
  308. expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
  309. expect(view.model.messages.at(0).get('editable')).toBe(false);
  310. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  311. const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
  312. expect(msg_el.textContent.trim()).toBe('eve has removed this message');
  313. expect(msg_el.querySelector('.chat-msg--retracted q')).toBe(null);
  314. done();
  315. }));
  316. it("can be retracted by a moderator, with the IQ response received before the retraction message",
  317. mock.initConverse(
  318. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  319. async function (done, _converse) {
  320. const muc_jid = 'lounge@montague.lit';
  321. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  322. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  323. const view = _converse.api.chatviews.get(muc_jid);
  324. const occupant = view.model.getOwnOccupant();
  325. expect(occupant.get('role')).toBe('moderator');
  326. const received_stanza = u.toStanza(`
  327. <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  328. <body>Visit this site to get free Bitcoin!</body>
  329. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  330. </message>
  331. `);
  332. await view.model.onMessage(received_stanza);
  333. await u.waitUntil(() => view.model.messages.length === 1);
  334. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  335. const reason = "This content is inappropriate for this forum!"
  336. const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
  337. retract_button.click();
  338. await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
  339. const reason_input = document.querySelector('#converse-modals .modal input[name="reason"]');
  340. reason_input.value = 'This content is inappropriate for this forum!';
  341. const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
  342. submit_button.click();
  343. const sent_IQs = _converse.connection.IQ_stanzas;
  344. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
  345. const message = view.model.messages.at(0);
  346. const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
  347. expect(Strophe.serialize(stanza)).toBe(
  348. `<iq id="${stanza.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
  349. `<apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">`+
  350. `<moderate xmlns="urn:xmpp:message-moderate:0">`+
  351. `<retract xmlns="urn:xmpp:message-retract:0"/>`+
  352. `<reason>This content is inappropriate for this forum!</reason>`+
  353. `</moderate>`+
  354. `</apply-to>`+
  355. `</iq>`);
  356. const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'});
  357. _converse.connection._dataRecv(test_utils.createRequest(result_iq));
  358. // We opportunistically save the message as retracted, even before receiving the retraction message
  359. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  360. expect(view.model.messages.length).toBe(1);
  361. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  362. expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
  363. expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
  364. expect(view.model.messages.at(0).get('editable')).toBe(false);
  365. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  366. const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
  367. expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has removed this message');
  368. const qel = msg_el.querySelector('q');
  369. expect(qel.textContent.trim()).toBe('This content is inappropriate for this forum!');
  370. // The server responds with a retraction message
  371. const retraction = u.toStanza(`
  372. <message type="groupchat" id='retraction-id-1' from="${muc_jid}" to="${muc_jid}/romeo">
  373. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  374. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  375. <retract xmlns='urn:xmpp:message-retract:0' />
  376. <reason>${reason}</reason>
  377. </moderated>
  378. </apply-to>
  379. </message>`);
  380. await view.model.onMessage(retraction);
  381. expect(view.model.messages.length).toBe(1);
  382. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  383. expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
  384. expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
  385. expect(view.model.messages.at(0).get('editable')).toBe(false);
  386. done();
  387. }));
  388. it("can not be retracted if the MUC doesn't support message moderation",
  389. mock.initConverse(
  390. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  391. async function (done, _converse) {
  392. const muc_jid = 'lounge@montague.lit';
  393. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  394. const view = _converse.api.chatviews.get(muc_jid);
  395. const occupant = view.model.getOwnOccupant();
  396. expect(occupant.get('role')).toBe('moderator');
  397. const received_stanza = u.toStanza(`
  398. <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  399. <body>Visit this site to get free Bitcoin!</body>
  400. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  401. </message>
  402. `);
  403. await view.model.onMessage(received_stanza);
  404. await u.waitUntil(() => view.el.querySelector('.chat-msg__content'));
  405. expect(view.el.querySelector('.chat-msg__content .chat-msg__action-retract')).toBe(null);
  406. const result = await view.model.canRetractMessages();
  407. expect(result).toBe(false);
  408. done();
  409. }));
  410. it("can be retracted by a moderator, with the retraction message received before the IQ response",
  411. mock.initConverse(
  412. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  413. async function (done, _converse) {
  414. const muc_jid = 'lounge@montague.lit';
  415. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  416. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  417. const view = _converse.api.chatviews.get(muc_jid);
  418. const occupant = view.model.getOwnOccupant();
  419. expect(occupant.get('role')).toBe('moderator');
  420. const received_stanza = u.toStanza(`
  421. <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  422. <body>Visit this site to get free Bitcoin!</body>
  423. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  424. </message>
  425. `);
  426. await view.model.onMessage(received_stanza);
  427. await u.waitUntil(() => view.model.messages.length === 1);
  428. expect(view.model.messages.length).toBe(1);
  429. const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
  430. retract_button.click();
  431. await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
  432. const reason_input = document.querySelector('#converse-modals .modal input[name="reason"]');
  433. const reason = "This content is inappropriate for this forum!"
  434. reason_input.value = reason;
  435. const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
  436. submit_button.click();
  437. const sent_IQs = _converse.connection.IQ_stanzas;
  438. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
  439. const message = view.model.messages.at(0);
  440. const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
  441. // The server responds with a retraction message
  442. const retraction = u.toStanza(`
  443. <message type="groupchat" id='retraction-id-1' from="${muc_jid}" to="${muc_jid}/romeo">
  444. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  445. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  446. <retract xmlns='urn:xmpp:message-retract:0' />
  447. <reason>${reason}</reason>
  448. </moderated>
  449. </apply-to>
  450. </message>`);
  451. await view.model.onMessage(retraction);
  452. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  453. expect(view.model.messages.length).toBe(1);
  454. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  455. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  456. const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  457. expect(msg_el.textContent).toBe('romeo has removed this message');
  458. const qel = view.el.querySelector('.chat-msg--retracted .chat-msg__message q');
  459. expect(qel.textContent).toBe('This content is inappropriate for this forum!');
  460. const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'});
  461. _converse.connection._dataRecv(test_utils.createRequest(result_iq));
  462. expect(view.model.messages.length).toBe(1);
  463. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  464. expect(view.model.messages.at(0).get('moderated_by')).toBe(_converse.bare_jid);
  465. expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
  466. expect(view.model.messages.at(0).get('editable')).toBe(false);
  467. done();
  468. }));
  469. });
  470. describe("A Sent Groupchat Message", function () {
  471. it("can be retracted by its author",
  472. mock.initConverse(
  473. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  474. async function (done, _converse) {
  475. const muc_jid = 'lounge@montague.lit';
  476. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  477. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  478. const view = _converse.api.chatviews.get(muc_jid);
  479. const occupant = view.model.getOwnOccupant();
  480. expect(occupant.get('role')).toBe('moderator');
  481. occupant.save('role', 'member');
  482. const retraction_stanza = await sendAndThenRetractMessage(_converse, view);
  483. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  484. const msg_obj = view.model.messages.at(0);
  485. expect(Strophe.serialize(retraction_stanza)).toBe(
  486. `<message id="${retraction_stanza.getAttribute('id')}" to="${muc_jid}" type="groupchat" xmlns="jabber:client">`+
  487. `<store xmlns="urn:xmpp:hints"/>`+
  488. `<apply-to id="${msg_obj.get('origin_id')}" xmlns="urn:xmpp:fasten:0">`+
  489. `<retract xmlns="urn:xmpp:message-retract:0"/>`+
  490. `</apply-to>`+
  491. `</message>`);
  492. const message = view.model.messages.at(0);
  493. expect(message.get('retracted')).toBeTruthy();
  494. expect(message.get('is_ephemeral')).toBe(false);
  495. expect(message.get('editable')).toBeFalsy();
  496. const stanza_id = message.get(`stanza_id ${muc_jid}`);
  497. // The server responds with a retraction message
  498. const reflection = u.toStanza(`
  499. <message type="groupchat" id="${retraction_stanza.getAttribute('id')}" from="${muc_jid}" to="${muc_jid}/romeo">
  500. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  501. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  502. <retract xmlns='urn:xmpp:message-retract:0' />
  503. </moderated>
  504. </apply-to>
  505. </message>`);
  506. spyOn(view.model, 'handleRetraction').and.callThrough();
  507. _converse.connection._dataRecv(test_utils.createRequest(reflection));
  508. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
  509. expect(view.model.messages.length).toBe(1);
  510. expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
  511. expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
  512. expect(view.model.messages.at(0).get('editable')).toBe(false);
  513. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  514. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  515. expect(el.textContent).toBe('romeo has removed this message');
  516. done();
  517. }));
  518. it("can be retracted by its author, causing an error message in response",
  519. mock.initConverse(
  520. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  521. async function (done, _converse) {
  522. const muc_jid = 'lounge@montague.lit';
  523. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  524. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  525. const view = _converse.api.chatviews.get(muc_jid);
  526. const occupant = view.model.getOwnOccupant();
  527. expect(occupant.get('role')).toBe('moderator');
  528. occupant.save('role', 'member');
  529. const retraction_stanza = await sendAndThenRetractMessage(_converse, view);
  530. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  531. expect(view.model.messages.length).toBe(1);
  532. expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
  533. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  534. expect(el.textContent.trim()).toBe('romeo has removed this message');
  535. const message = view.model.messages.at(0);
  536. const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
  537. // The server responds with an error message
  538. const error = u.toStanza(`
  539. <message type="error" id="${retraction_stanza.getAttribute('id')}" from="${muc_jid}" to="${view.model.get('jid')}/romeo">
  540. <error by='${muc_jid}' type='auth'>
  541. <forbidden xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
  542. </error>
  543. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  544. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  545. <retract xmlns='urn:xmpp:message-retract:0' />
  546. </moderated>
  547. </apply-to>
  548. </message>`);
  549. _converse.connection._dataRecv(test_utils.createRequest(error));
  550. await u.waitUntil(() => view.el.querySelectorAll('.chat-error').length === 1);
  551. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
  552. expect(view.model.messages.length).toBe(1);
  553. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  554. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  555. expect(view.model.messages.at(0).get('editable')).toBeTruthy();
  556. expect(view.el.querySelectorAll('.chat-error').length).toBe(1);
  557. const errmsg = view.el.querySelector('.chat-error');
  558. expect(errmsg.textContent).toBe("Sorry, something went wrong while trying to retract your message.");
  559. done();
  560. }));
  561. it("can be retracted by its author, causing an timeout error in response",
  562. mock.initConverse(
  563. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  564. async function (done, _converse) {
  565. _converse.STANZA_TIMEOUT = 1;
  566. const muc_jid = 'lounge@montague.lit';
  567. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  568. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  569. const view = _converse.api.chatviews.get(muc_jid);
  570. const occupant = view.model.getOwnOccupant();
  571. expect(occupant.get('role')).toBe('moderator');
  572. occupant.save('role', 'member');
  573. await sendAndThenRetractMessage(_converse, view);
  574. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  575. expect(view.model.messages.length).toBe(1);
  576. expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
  577. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  578. expect(el.textContent.trim()).toBe('romeo has removed this message');
  579. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  580. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
  581. expect(view.model.messages.length).toBe(1);
  582. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  583. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  584. expect(view.model.messages.at(0).get('editable')).toBeTruthy();
  585. const error_messages = view.el.querySelectorAll('.chat-error');
  586. expect(error_messages.length).toBe(2);
  587. expect(error_messages[0].textContent).toBe("Sorry, something went wrong while trying to retract your message.");
  588. expect(error_messages[1].textContent).toBe("Timeout Error: No response from server");
  589. done();
  590. }));
  591. it("can be retracted by a moderator",
  592. mock.initConverse(
  593. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  594. async function (done, _converse) {
  595. const muc_jid = 'lounge@montague.lit';
  596. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  597. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  598. const view = _converse.api.chatviews.get(muc_jid);
  599. const occupant = view.model.getOwnOccupant();
  600. expect(occupant.get('role')).toBe('moderator');
  601. view.model.sendMessage('Visit this site to get free bitcoin');
  602. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  603. const stanza_id = 'retraction-id-1';
  604. const msg_obj = view.model.messages.at(0);
  605. const reflection_stanza = u.toStanza(`
  606. <message xmlns="jabber:client"
  607. from="${msg_obj.get('from')}"
  608. to="${_converse.connection.jid}"
  609. type="groupchat">
  610. <msg_body>${msg_obj.get('message')}</msg_body>
  611. <stanza-id xmlns="urn:xmpp:sid:0"
  612. id="${stanza_id}"
  613. by="lounge@montague.lit"/>
  614. <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
  615. </message>`);
  616. await view.model.onMessage(reflection_stanza);
  617. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
  618. expect(view.model.messages.length).toBe(1);
  619. expect(view.model.messages.at(0).get('editable')).toBe(true);
  620. // The server responds with a retraction message
  621. const reason = "This content is inappropriate for this forum!"
  622. const retraction = u.toStanza(`
  623. <message type="groupchat" id='retraction-id-1' from="${muc_jid}" to="${muc_jid}/romeo">
  624. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  625. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  626. <retract xmlns='urn:xmpp:message-retract:0' />
  627. <reason>${reason}</reason>
  628. </moderated>
  629. </apply-to>
  630. </message>`);
  631. await view.model.onMessage(retraction);
  632. expect(view.model.messages.length).toBe(1);
  633. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  634. expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
  635. expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
  636. expect(view.model.messages.at(0).get('editable')).toBe(false);
  637. done();
  638. }));
  639. });
  640. describe("when archived", function () {
  641. it("may be returned as a tombstone message",
  642. mock.initConverse(
  643. ['discoInitialized'], {},
  644. async function (done, _converse) {
  645. await test_utils.waitForRoster(_converse, 'current', 1);
  646. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  647. await test_utils.openChatBoxFor(_converse, contact_jid);
  648. await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  649. const sent_IQs = _converse.connection.IQ_stanzas;
  650. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
  651. const queryid = stanza.querySelector('query').getAttribute('queryid');
  652. const view = _converse.chatboxviews.get(contact_jid);
  653. const first_id = u.getUniqueId();
  654. spyOn(view.model, 'handleRetraction').and.callThrough();
  655. const first_message = u.toStanza(`
  656. <message id='${u.getUniqueId()}' to='${_converse.jid}'>
  657. <result xmlns='urn:xmpp:mam:2' queryid='${queryid}' id="${first_id}">
  658. <forwarded xmlns='urn:xmpp:forward:0'>
  659. <delay xmlns='urn:xmpp:delay' stamp='2019-09-20T23:01:15Z'/>
  660. <message type="chat" from="${contact_jid}" to="${_converse.bare_jid}" id="message-id-0">
  661. <origin-id xmlns='urn:xmpp:sid:0' id="origin-id-0"/>
  662. <body>😊</body>
  663. </message>
  664. </forwarded>
  665. </result>
  666. </message>
  667. `);
  668. _converse.connection._dataRecv(test_utils.createRequest(first_message));
  669. const tombstone = u.toStanza(`
  670. <message id='${u.getUniqueId()}' to='${_converse.jid}'>
  671. <result xmlns='urn:xmpp:mam:2' queryid='${queryid}' id="${u.getUniqueId()}">
  672. <forwarded xmlns='urn:xmpp:forward:0'>
  673. <delay xmlns='urn:xmpp:delay' stamp='2019-09-20T23:08:25Z'/>
  674. <message type="chat" from="${contact_jid}" to="${_converse.bare_jid}" id="message-id-1">
  675. <origin-id xmlns='urn:xmpp:sid:0' id="origin-id-1"/>
  676. <retracted stamp='2019-09-20T23:09:32Z' xmlns='urn:xmpp:message-retract:0'/>
  677. </message>
  678. </forwarded>
  679. </result>
  680. </message>
  681. `);
  682. _converse.connection._dataRecv(test_utils.createRequest(tombstone));
  683. const last_id = u.getUniqueId();
  684. const retraction = u.toStanza(`
  685. <message id='${u.getUniqueId()}' to='${_converse.jid}'>
  686. <result xmlns='urn:xmpp:mam:2' queryid='${queryid}' id="${last_id}">
  687. <forwarded xmlns='urn:xmpp:forward:0'>
  688. <delay xmlns='urn:xmpp:delay' stamp='2019-09-20T23:08:25Z'/>
  689. <message from="${contact_jid}" to='${_converse.bare_jid}' id='retract-message-1'>
  690. <apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
  691. <retract xmlns='urn:xmpp:message-retract:0'/>
  692. </apply-to>
  693. </message>
  694. </forwarded>
  695. </result>
  696. </message>
  697. `);
  698. _converse.connection._dataRecv(test_utils.createRequest(retraction));
  699. const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
  700. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  701. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  702. .c('first', {'index': '0'}).t(first_id).up()
  703. .c('last').t(last_id).up()
  704. .c('count').t('2');
  705. _converse.connection._dataRecv(test_utils.createRequest(iq_result));
  706. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 3);
  707. expect(view.model.messages.length).toBe(2);
  708. const message = view.model.messages.at(1);
  709. expect(message.get('retracted')).toBeTruthy();
  710. expect(message.get('is_tombstone')).toBe(true);
  711. expect(view.model.handleRetraction.calls.first().returnValue).toBe(false);
  712. expect(view.model.handleRetraction.calls.all()[1].returnValue).toBe(false);
  713. expect(view.model.handleRetraction.calls.all()[2].returnValue).toBe(true);
  714. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  715. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  716. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  717. expect(el.textContent.trim()).toBe('Mercutio has removed this message');
  718. expect(u.hasClass('chat-msg--followup', el.parentElement)).toBe(false);
  719. done();
  720. }));
  721. it("may be returned as a tombstone groupchat message",
  722. mock.initConverse(
  723. ['discoInitialized'], {},
  724. async function (done, _converse) {
  725. const muc_jid = 'lounge@montague.lit';
  726. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  727. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  728. const view = _converse.chatboxviews.get(muc_jid);
  729. const sent_IQs = _converse.connection.IQ_stanzas;
  730. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
  731. const queryid = stanza.querySelector('query').getAttribute('queryid');
  732. const first_id = u.getUniqueId();
  733. const tombstone = u.toStanza(`
  734. <message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
  735. <result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="stanza-id">
  736. <forwarded xmlns="urn:xmpp:forward:0">
  737. <delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
  738. <message type="groupchat" from="${muc_jid}/eve" to="${_converse.bare_jid}" id="message-id-1">
  739. <origin-id xmlns='urn:xmpp:sid:0' id="origin-id-1"/>
  740. <retracted stamp="2019-09-20T23:09:32Z" xmlns="urn:xmpp:message-retract:0"/>
  741. </message>
  742. </forwarded>
  743. </result>
  744. </message>
  745. `);
  746. spyOn(view.model, 'handleRetraction').and.callThrough();
  747. const promise = new Promise(resolve => _converse.api.listen.once('messageAdded', resolve));
  748. _converse.connection._dataRecv(test_utils.createRequest(tombstone));
  749. const last_id = u.getUniqueId();
  750. const retraction = u.toStanza(`
  751. <message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
  752. <result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="${last_id}">
  753. <forwarded xmlns="urn:xmpp:forward:0">
  754. <delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
  755. <message type="groupchat" from="${muc_jid}/eve" to="${_converse.bare_jid}" id="retract-message-1">
  756. <apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
  757. <retract xmlns="urn:xmpp:message-retract:0"/>
  758. </apply-to>
  759. </message>
  760. </forwarded>
  761. </result>
  762. </message>
  763. `);
  764. _converse.connection._dataRecv(test_utils.createRequest(retraction));
  765. const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
  766. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  767. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  768. .c('first', {'index': '0'}).t(first_id).up()
  769. .c('last').t(last_id).up()
  770. .c('count').t('2');
  771. _converse.connection._dataRecv(test_utils.createRequest(iq_result));
  772. await promise;
  773. expect(view.model.messages.length).toBe(1);
  774. let message = view.model.messages.at(0);
  775. expect(message.get('retracted')).toBeTruthy();
  776. expect(message.get('is_tombstone')).toBe(true);
  777. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
  778. expect(view.model.handleRetraction.calls.first().returnValue).toBe(false);
  779. expect(view.model.handleRetraction.calls.all()[1].returnValue).toBe(true);
  780. expect(view.model.messages.length).toBe(1);
  781. message = view.model.messages.at(0);
  782. expect(message.get('retracted')).toBeTruthy();
  783. expect(message.get('is_tombstone')).toBe(true);
  784. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  785. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  786. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  787. expect(el.textContent.trim()).toBe('eve has removed this message');
  788. done();
  789. }));
  790. it("may be returned as a tombstone moderated groupchat message",
  791. mock.initConverse(
  792. ['discoInitialized', 'chatBoxesFetched'], {},
  793. async function (done, _converse) {
  794. const muc_jid = 'lounge@montague.lit';
  795. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  796. await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  797. const view = _converse.chatboxviews.get(muc_jid);
  798. const sent_IQs = _converse.connection.IQ_stanzas;
  799. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
  800. const queryid = stanza.querySelector('query').getAttribute('queryid');
  801. const first_id = u.getUniqueId();
  802. const tombstone = u.toStanza(`
  803. <message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
  804. <result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="stanza-id">
  805. <forwarded xmlns="urn:xmpp:forward:0">
  806. <delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
  807. <message type="groupchat" from="${muc_jid}/eve" to="${_converse.bare_jid}" id="message-id-1">
  808. <moderated by="${muc_jid}/bob" stamp="2019-09-20T23:09:32Z" xmlns='urn:xmpp:message-moderate:0'>
  809. <retracted xmlns="urn:xmpp:message-retract:0"/>
  810. <reason>This message contains inappropriate content</reason>
  811. </moderated>
  812. </message>
  813. </forwarded>
  814. </result>
  815. </message>
  816. `);
  817. spyOn(view.model, 'handleModeration').and.callThrough();
  818. const promise = new Promise(resolve => _converse.api.listen.once('messageAdded', resolve));
  819. _converse.connection._dataRecv(test_utils.createRequest(tombstone));
  820. const last_id = u.getUniqueId();
  821. const retraction = u.toStanza(`
  822. <message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
  823. <result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="${last_id}">
  824. <forwarded xmlns="urn:xmpp:forward:0">
  825. <delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
  826. <message type="groupchat" from="${muc_jid}" to="${_converse.bare_jid}" id="retract-message-1">
  827. <apply-to id="stanza-id" xmlns="urn:xmpp:fasten:0">
  828. <moderated by="${muc_jid}/bob" xmlns='urn:xmpp:message-moderate:0'>
  829. <retract xmlns="urn:xmpp:message-retract:0"/>
  830. <reason>This message contains inappropriate content</reason>
  831. </moderated>
  832. </apply-to>
  833. </message>
  834. </forwarded>
  835. </result>
  836. </message>
  837. `);
  838. _converse.connection._dataRecv(test_utils.createRequest(retraction));
  839. const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
  840. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  841. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  842. .c('first', {'index': '0'}).t(first_id).up()
  843. .c('last').t(last_id).up()
  844. .c('count').t('2');
  845. _converse.connection._dataRecv(test_utils.createRequest(iq_result));
  846. await promise;
  847. expect(view.model.messages.length).toBe(1);
  848. let message = view.model.messages.at(0);
  849. expect(message.get('retracted')).toBeTruthy();
  850. expect(message.get('is_tombstone')).toBe(true);
  851. await u.waitUntil(() => view.model.handleModeration.calls.count() === 2);
  852. expect(view.model.handleModeration.calls.first().returnValue).toBe(false);
  853. expect(view.model.handleModeration.calls.all()[1].returnValue).toBe(true);
  854. expect(view.model.messages.length).toBe(1);
  855. message = view.model.messages.at(0);
  856. expect(message.get('retracted')).toBeTruthy();
  857. expect(message.get('is_tombstone')).toBe(true);
  858. expect(message.get('moderation_reason')).toBe("This message contains inappropriate content");
  859. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  860. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  861. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  862. expect(el.textContent.trim()).toBe('A moderator has removed this message');
  863. const qel = view.el.querySelector('.chat-msg--retracted .chat-msg__message q');
  864. expect(qel.textContent.trim()).toBe('This message contains inappropriate content');
  865. done();
  866. }));
  867. });
  868. })
  869. }));