2
0

retractions.js 62 KB

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