retractions.js 63 KB

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