retractions.js 67 KB

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