2
0

retractions.js 63 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143
  1. /*global mock */
  2. const { Strophe, $iq } = converse.env;
  3. const u = converse.env.utils;
  4. const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
  5. async function sendAndThenRetractMessage (_converse, view) {
  6. view.model.sendMessage('hello world');
  7. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text').length === 1);
  8. const msg_obj = view.model.messages.last();
  9. const reflection_stanza = u.toStanza(`
  10. <message xmlns="jabber:client"
  11. from="${msg_obj.get('from')}"
  12. to="${_converse.connection.jid}"
  13. type="groupchat">
  14. <msg_body>${msg_obj.get('message')}</msg_body>
  15. <stanza-id xmlns="urn:xmpp:sid:0"
  16. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  17. by="lounge@montague.lit"/>
  18. <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
  19. </message>`);
  20. await view.model.handleMessageStanza(reflection_stanza);
  21. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
  22. const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
  23. retract_button.click();
  24. await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
  25. const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
  26. submit_button.click();
  27. const sent_stanzas = _converse.connection.sent_stanzas;
  28. return u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('message apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
  29. }
  30. describe("Message Retractions", function () {
  31. beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000));
  32. afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
  33. describe("A groupchat message retraction", function () {
  34. it("is not applied if it's not from the right author",
  35. mock.initConverse(
  36. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  37. async function (done, _converse) {
  38. const muc_jid = 'lounge@montague.lit';
  39. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  40. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  41. const received_stanza = u.toStanza(`
  42. <message to='${_converse.jid}' from='${muc_jid}/eve' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  43. <body>Hello world</body>
  44. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  45. </message>
  46. `);
  47. const view = _converse.api.chatviews.get(muc_jid);
  48. await view.model.handleMessageStanza(received_stanza);
  49. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  50. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  51. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  52. const retraction_stanza = u.toStanza(`
  53. <message type="groupchat" id='retraction-id-1' from="${muc_jid}/mallory" to="${muc_jid}/romeo">
  54. <apply-to id="stanza-id-1" xmlns="urn:xmpp:fasten:0">
  55. <retract xmlns="urn:xmpp:message-retract:0" />
  56. </apply-to>
  57. </message>
  58. `);
  59. spyOn(view.model, 'handleRetraction').and.callThrough();
  60. _converse.connection._dataRecv(mock.createRequest(retraction_stanza));
  61. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
  62. expect(await view.model.handleRetraction.calls.first().returnValue).toBe(true);
  63. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  64. expect(view.model.messages.length).toBe(2);
  65. expect(view.model.messages.at(1).get('retracted')).toBeTruthy();
  66. expect(view.model.messages.at(1).get('is_ephemeral')).toBeFalsy();
  67. expect(view.model.messages.at(1).get('dangling_retraction')).toBe(true);
  68. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  69. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  70. done();
  71. }));
  72. it("can be received before the message it pertains to",
  73. mock.initConverse(
  74. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  75. async function (done, _converse) {
  76. const date = (new Date()).toISOString();
  77. const muc_jid = 'lounge@montague.lit';
  78. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  79. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  80. const retraction_stanza = u.toStanza(`
  81. <message type="groupchat" id='retraction-id-1' from="${muc_jid}/eve" to="${muc_jid}/romeo">
  82. <apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
  83. <retract by="${muc_jid}/eve" xmlns="urn:xmpp:message-retract:0" />
  84. </apply-to>
  85. </message>
  86. `);
  87. const view = _converse.api.chatviews.get(muc_jid);
  88. spyOn(converse.env.log, 'warn');
  89. spyOn(view.model, 'handleRetraction').and.callThrough();
  90. _converse.connection._dataRecv(mock.createRequest(retraction_stanza));
  91. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
  92. await u.waitUntil(() => view.model.messages.length === 1);
  93. expect(await view.model.handleRetraction.calls.first().returnValue).toBe(true);
  94. expect(view.model.messages.length).toBe(1);
  95. expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
  96. expect(view.model.messages.at(0).get('dangling_retraction')).toBe(true);
  97. const received_stanza = u.toStanza(`
  98. <message to='${_converse.jid}' from='${muc_jid}/eve' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  99. <body>Hello world</body>
  100. <delay xmlns='urn:xmpp:delay' stamp='${date}'/>
  101. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  102. <origin-id xmlns="urn:xmpp:sid:0" id="origin-id-1"/>
  103. </message>
  104. `);
  105. _converse.connection._dataRecv(mock.createRequest(received_stanza));
  106. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
  107. expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
  108. expect(view.model.messages.length).toBe(1);
  109. const message = view.model.messages.at(0)
  110. expect(message.get('retracted')).toBeTruthy();
  111. expect(message.get('dangling_retraction')).toBe(false);
  112. expect(message.get('origin_id')).toBe('origin-id-1');
  113. expect(message.get(`stanza_id ${muc_jid}`)).toBe('stanza-id-1');
  114. expect(message.get('time')).toBe(date);
  115. expect(message.get('type')).toBe('groupchat');
  116. expect(await view.model.handleRetraction.calls.all().pop().returnValue).toBe(true);
  117. done();
  118. }));
  119. });
  120. describe("A groupchat message moderator retraction", function () {
  121. it("can be received before the message it pertains to",
  122. mock.initConverse(
  123. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  124. async function (done, _converse) {
  125. const date = (new Date()).toISOString();
  126. const muc_jid = 'lounge@montague.lit';
  127. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  128. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  129. const retraction_stanza = u.toStanza(`
  130. <message xmlns="jabber:client" from="${muc_jid}" type="groupchat" id="retraction-id-1">
  131. <apply-to xmlns="urn:xmpp:fasten:0" id="stanza-id-1">
  132. <moderated xmlns="urn:xmpp:message-moderate:0" by="${muc_jid}/madison">
  133. <retract xmlns="urn:xmpp:message-retract:0"/>
  134. <reason>Insults</reason>
  135. </moderated>
  136. </apply-to>
  137. </message>
  138. `);
  139. const view = _converse.api.chatviews.get(muc_jid);
  140. spyOn(converse.env.log, 'warn');
  141. spyOn(view.model, 'handleModeration').and.callThrough();
  142. _converse.connection._dataRecv(mock.createRequest(retraction_stanza));
  143. await u.waitUntil(() => view.model.handleModeration.calls.count() === 1);
  144. await u.waitUntil(() => view.model.messages.length === 1);
  145. expect(await view.model.handleModeration.calls.first().returnValue).toBe(true);
  146. expect(view.model.messages.length).toBe(1);
  147. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  148. expect(view.model.messages.at(0).get('dangling_moderation')).toBe(true);
  149. const received_stanza = u.toStanza(`
  150. <message to='${_converse.jid}' from='${muc_jid}/eve' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  151. <body>Hello world</body>
  152. <delay xmlns='urn:xmpp:delay' stamp='${date}'/>
  153. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  154. </message>
  155. `);
  156. _converse.connection._dataRecv(mock.createRequest(received_stanza));
  157. await u.waitUntil(() => view.model.handleModeration.calls.count() === 2);
  158. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
  159. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  160. expect(view.model.messages.length).toBe(1);
  161. const message = view.model.messages.at(0)
  162. expect(message.get('moderated')).toBe('retracted');
  163. expect(message.get('dangling_moderation')).toBe(false);
  164. expect(message.get(`stanza_id ${muc_jid}`)).toBe('stanza-id-1');
  165. expect(message.get('time')).toBe(date);
  166. expect(message.get('type')).toBe('groupchat');
  167. expect(await view.model.handleModeration.calls.all().pop().returnValue).toBe(true);
  168. done();
  169. }));
  170. });
  171. describe("A message retraction", function () {
  172. it("can be received before the message it pertains to",
  173. mock.initConverse(
  174. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  175. async function (done, _converse) {
  176. const date = (new Date()).toISOString();
  177. await mock.waitForRoster(_converse, 'current', 1);
  178. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
  179. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  180. const view = await mock.openChatBoxFor(_converse, contact_jid);
  181. spyOn(view.model, 'handleRetraction').and.callThrough();
  182. const retraction_stanza = u.toStanza(`
  183. <message id="${u.getUniqueId()}"
  184. to="${_converse.bare_jid}"
  185. from="${contact_jid}"
  186. type="chat"
  187. xmlns="jabber:client">
  188. <apply-to id="2e972ea0-0050-44b7-a830-f6638a2595b3" xmlns="urn:xmpp:fasten:0">
  189. <retract xmlns="urn:xmpp:message-retract:0"/>
  190. </apply-to>
  191. </message>
  192. `);
  193. _converse.connection._dataRecv(mock.createRequest(retraction_stanza));
  194. await u.waitUntil(() => view.model.messages.length === 1);
  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(mock.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 mock.waitForRoster(_converse, 'current', 1);
  229. await mock.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 mock.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(mock.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(mock.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(mock.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 mock.waitForRoster(_converse, 'current', 1);
  289. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  290. const view = await mock.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 mock.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.handleMessageStanza(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(mock.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 mock.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.handleMessageStanza(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(mock.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.handleMessageStanza(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 mock.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.handleMessageStanza(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 mock.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.handleMessageStanza(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.handleMessageStanza(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(mock.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 mock.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(mock.createRequest(reflection));
  553. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
  554. expect(view.model.messages.length).toBe(1);
  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 mock.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(() => view.el.querySelector('.chat-content__notifications').textContent.includes("romeo is no longer a moderator"));
  575. const retraction_stanza = await sendAndThenRetractMessage(_converse, view);
  576. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  577. expect(view.model.messages.length).toBe(1);
  578. expect(view.model.messages.last().get('retracted')).toBeTruthy();
  579. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  580. expect(el.textContent.trim()).toBe('romeo has removed this message');
  581. const message = view.model.messages.last();
  582. const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
  583. // The server responds with an error message
  584. const error = u.toStanza(`
  585. <message type="error" id="${retraction_stanza.getAttribute('id')}" from="${muc_jid}" to="${view.model.get('jid')}/romeo">
  586. <error by='${muc_jid}' type='auth'>
  587. <forbidden xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
  588. </error>
  589. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  590. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  591. <retract xmlns='urn:xmpp:message-retract:0' />
  592. </moderated>
  593. </apply-to>
  594. </message>`);
  595. _converse.connection._dataRecv(mock.createRequest(error));
  596. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__error').length === 1);
  597. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
  598. expect(view.model.messages.length).toBe(1);
  599. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  600. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  601. expect(view.model.messages.at(0).get('editable')).toBeTruthy();
  602. const errmsg = view.el.querySelector('.chat-msg__error');
  603. expect(errmsg.textContent.trim()).toBe("You're not allowed to retract your message.");
  604. done();
  605. }));
  606. it("can be retracted by its author, causing a timeout error in response",
  607. mock.initConverse(
  608. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  609. async function (done, _converse) {
  610. _converse.STANZA_TIMEOUT = 1;
  611. const muc_jid = 'lounge@montague.lit';
  612. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  613. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  614. const view = _converse.api.chatviews.get(muc_jid);
  615. const occupant = view.model.getOwnOccupant();
  616. expect(occupant.get('role')).toBe('moderator');
  617. occupant.save('role', 'member');
  618. await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.includes("romeo is no longer a moderator"))
  619. await sendAndThenRetractMessage(_converse, view);
  620. expect(view.model.messages.length).toBe(1);
  621. expect(view.model.messages.last().get('retracted')).toBeTruthy();
  622. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  623. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  624. expect(el.textContent.trim()).toBe('romeo has removed this message');
  625. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  626. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
  627. expect(view.model.messages.length).toBe(1);
  628. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  629. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  630. expect(view.model.messages.at(0).get('editable')).toBeTruthy();
  631. const error_messages = view.el.querySelectorAll('.chat-msg__error');
  632. expect(error_messages.length).toBe(1);
  633. expect(error_messages[0].textContent.trim()).toBe('A timeout happened while while trying to retract your message.');
  634. done();
  635. }));
  636. it("can be retracted by a moderator",
  637. mock.initConverse(
  638. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  639. async function (done, _converse) {
  640. const muc_jid = 'lounge@montague.lit';
  641. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  642. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  643. const view = _converse.api.chatviews.get(muc_jid);
  644. const occupant = view.model.getOwnOccupant();
  645. expect(occupant.get('role')).toBe('moderator');
  646. view.model.sendMessage('Visit this site to get free bitcoin');
  647. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  648. const stanza_id = 'retraction-id-1';
  649. const msg_obj = view.model.messages.at(0);
  650. const reflection_stanza = u.toStanza(`
  651. <message xmlns="jabber:client"
  652. from="${msg_obj.get('from')}"
  653. to="${_converse.connection.jid}"
  654. type="groupchat">
  655. <msg_body>${msg_obj.get('message')}</msg_body>
  656. <stanza-id xmlns="urn:xmpp:sid:0"
  657. id="${stanza_id}"
  658. by="lounge@montague.lit"/>
  659. <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
  660. </message>`);
  661. await view.model.handleMessageStanza(reflection_stanza);
  662. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
  663. expect(view.model.messages.length).toBe(1);
  664. expect(view.model.messages.at(0).get('editable')).toBe(true);
  665. // The server responds with a retraction message
  666. const reason = "This content is inappropriate for this forum!"
  667. const retraction = u.toStanza(`
  668. <message type="groupchat" id='retraction-id-1' from="${muc_jid}" to="${muc_jid}/romeo">
  669. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  670. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  671. <retract xmlns='urn:xmpp:message-retract:0' />
  672. <reason>${reason}</reason>
  673. </moderated>
  674. </apply-to>
  675. </message>`);
  676. await view.model.handleMessageStanza(retraction);
  677. expect(view.model.messages.length).toBe(1);
  678. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  679. expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
  680. expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
  681. expect(view.model.messages.at(0).get('editable')).toBe(false);
  682. done();
  683. }));
  684. it("can be retracted by the sender if they're a moderator",
  685. mock.initConverse(
  686. ['rosterGroupsFetched', 'chatBoxesFetched'], {'allow_message_retraction': 'moderator'},
  687. async function (done, _converse) {
  688. const muc_jid = 'lounge@montague.lit';
  689. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  690. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  691. const view = _converse.api.chatviews.get(muc_jid);
  692. const occupant = view.model.getOwnOccupant();
  693. expect(occupant.get('role')).toBe('moderator');
  694. view.model.sendMessage('Visit this site to get free bitcoin');
  695. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  696. const stanza_id = 'retraction-id-1';
  697. const msg_obj = view.model.messages.at(0);
  698. const reflection_stanza = u.toStanza(`
  699. <message xmlns="jabber:client"
  700. from="${msg_obj.get('from')}"
  701. to="${_converse.connection.jid}"
  702. type="groupchat">
  703. <msg_body>${msg_obj.get('message')}</msg_body>
  704. <stanza-id xmlns="urn:xmpp:sid:0"
  705. id="${stanza_id}"
  706. by="lounge@montague.lit"/>
  707. <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
  708. </message>`);
  709. await view.model.handleMessageStanza(reflection_stanza);
  710. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
  711. expect(view.model.messages.length).toBe(1);
  712. expect(view.model.messages.at(0).get('editable')).toBe(true);
  713. const retract_button = await u.waitUntil(() => view.msgs_container.querySelector('.chat-msg__content .chat-msg__action-retract'));
  714. retract_button.click();
  715. await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
  716. const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
  717. submit_button.click();
  718. const sent_IQs = _converse.connection.IQ_stanzas;
  719. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
  720. expect(Strophe.serialize(stanza)).toBe(
  721. `<iq id="${stanza.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
  722. `<apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">`+
  723. `<moderate xmlns="urn:xmpp:message-moderate:0">`+
  724. `<retract xmlns="urn:xmpp:message-retract:0"/>`+
  725. `<reason></reason>`+
  726. `</moderate>`+
  727. `</apply-to>`+
  728. `</iq>`);
  729. const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'});
  730. _converse.connection._dataRecv(mock.createRequest(result_iq));
  731. // We opportunistically save the message as retracted, even before receiving the retraction message
  732. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  733. expect(view.model.messages.length).toBe(1);
  734. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  735. expect(view.model.messages.at(0).get('moderation_reason')).toBe(undefined);
  736. expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
  737. expect(view.model.messages.at(0).get('editable')).toBe(false);
  738. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  739. const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
  740. expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has removed this message');
  741. expect(msg_el.querySelector('q')).toBe(null);
  742. // The server responds with a retraction message
  743. const retraction = u.toStanza(`
  744. <message type="groupchat" id='retraction-id-1' from="${muc_jid}" to="${muc_jid}/romeo">
  745. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  746. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  747. <retract xmlns='urn:xmpp:message-retract:0' />
  748. </moderated>
  749. </apply-to>
  750. </message>`);
  751. await view.model.handleMessageStanza(retraction);
  752. expect(view.model.messages.length).toBe(1);
  753. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  754. expect(view.model.messages.at(0).get('moderation_reason')).toBe(undefined);
  755. expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
  756. expect(view.model.messages.at(0).get('editable')).toBe(false);
  757. done();
  758. }));
  759. });
  760. describe("when archived", function () {
  761. it("may be returned as a tombstone message",
  762. mock.initConverse(
  763. ['discoInitialized'], {},
  764. async function (done, _converse) {
  765. await mock.waitForRoster(_converse, 'current', 1);
  766. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  767. await mock.openChatBoxFor(_converse, contact_jid);
  768. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
  769. const sent_IQs = _converse.connection.IQ_stanzas;
  770. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
  771. const queryid = stanza.querySelector('query').getAttribute('queryid');
  772. const view = _converse.chatboxviews.get(contact_jid);
  773. const first_id = u.getUniqueId();
  774. spyOn(view.model, 'handleRetraction').and.callThrough();
  775. const first_message = u.toStanza(`
  776. <message id='${u.getUniqueId()}' to='${_converse.jid}'>
  777. <result xmlns='urn:xmpp:mam:2' queryid='${queryid}' id="${first_id}">
  778. <forwarded xmlns='urn:xmpp:forward:0'>
  779. <delay xmlns='urn:xmpp:delay' stamp='2019-09-20T23:01:15Z'/>
  780. <message type="chat" from="${contact_jid}" to="${_converse.bare_jid}" id="message-id-0">
  781. <origin-id xmlns='urn:xmpp:sid:0' id="origin-id-0"/>
  782. <body>😊</body>
  783. </message>
  784. </forwarded>
  785. </result>
  786. </message>
  787. `);
  788. _converse.connection._dataRecv(mock.createRequest(first_message));
  789. const tombstone = u.toStanza(`
  790. <message id='${u.getUniqueId()}' to='${_converse.jid}'>
  791. <result xmlns='urn:xmpp:mam:2' queryid='${queryid}' id="${u.getUniqueId()}">
  792. <forwarded xmlns='urn:xmpp:forward:0'>
  793. <delay xmlns='urn:xmpp:delay' stamp='2019-09-20T23:08:25Z'/>
  794. <message type="chat" from="${contact_jid}" to="${_converse.bare_jid}" id="message-id-1">
  795. <origin-id xmlns='urn:xmpp:sid:0' id="origin-id-1"/>
  796. <retracted stamp='2019-09-20T23:09:32Z' xmlns='urn:xmpp:message-retract:0'/>
  797. </message>
  798. </forwarded>
  799. </result>
  800. </message>
  801. `);
  802. _converse.connection._dataRecv(mock.createRequest(tombstone));
  803. const last_id = u.getUniqueId();
  804. const retraction = u.toStanza(`
  805. <message id='${u.getUniqueId()}' to='${_converse.jid}'>
  806. <result xmlns='urn:xmpp:mam:2' queryid='${queryid}' id="${last_id}">
  807. <forwarded xmlns='urn:xmpp:forward:0'>
  808. <delay xmlns='urn:xmpp:delay' stamp='2019-09-20T23:08:25Z'/>
  809. <message from="${contact_jid}" to='${_converse.bare_jid}' id='retract-message-1'>
  810. <apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
  811. <retract xmlns='urn:xmpp:message-retract:0'/>
  812. </apply-to>
  813. </message>
  814. </forwarded>
  815. </result>
  816. </message>
  817. `);
  818. _converse.connection._dataRecv(mock.createRequest(retraction));
  819. const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
  820. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  821. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  822. .c('first', {'index': '0'}).t(first_id).up()
  823. .c('last').t(last_id).up()
  824. .c('count').t('2');
  825. _converse.connection._dataRecv(mock.createRequest(iq_result));
  826. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 3);
  827. expect(view.model.messages.length).toBe(2);
  828. const message = view.model.messages.at(1);
  829. expect(message.get('retracted')).toBeTruthy();
  830. expect(message.get('is_tombstone')).toBe(true);
  831. expect(await view.model.handleRetraction.calls.first().returnValue).toBe(false);
  832. expect(await view.model.handleRetraction.calls.all()[1].returnValue).toBe(false);
  833. expect(await view.model.handleRetraction.calls.all()[2].returnValue).toBe(true);
  834. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
  835. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  836. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  837. expect(el.textContent.trim()).toBe('Mercutio has removed this message');
  838. expect(u.hasClass('chat-msg--followup', el.parentElement)).toBe(false);
  839. done();
  840. }));
  841. it("may be returned as a tombstone groupchat message",
  842. mock.initConverse(
  843. ['discoInitialized'], {},
  844. async function (done, _converse) {
  845. const muc_jid = 'lounge@montague.lit';
  846. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  847. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  848. const view = _converse.chatboxviews.get(muc_jid);
  849. const sent_IQs = _converse.connection.IQ_stanzas;
  850. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
  851. const queryid = stanza.querySelector('query').getAttribute('queryid');
  852. const first_id = u.getUniqueId();
  853. const tombstone = u.toStanza(`
  854. <message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
  855. <result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="stanza-id">
  856. <forwarded xmlns="urn:xmpp:forward:0">
  857. <delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
  858. <message type="groupchat" from="${muc_jid}/eve" to="${_converse.bare_jid}" id="message-id-1">
  859. <origin-id xmlns='urn:xmpp:sid:0' id="origin-id-1"/>
  860. <retracted stamp="2019-09-20T23:09:32Z" xmlns="urn:xmpp:message-retract:0"/>
  861. </message>
  862. </forwarded>
  863. </result>
  864. </message>
  865. `);
  866. spyOn(view.model, 'handleRetraction').and.callThrough();
  867. _converse.connection._dataRecv(mock.createRequest(tombstone));
  868. const last_id = u.getUniqueId();
  869. const retraction = u.toStanza(`
  870. <message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
  871. <result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="${last_id}">
  872. <forwarded xmlns="urn:xmpp:forward:0">
  873. <delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
  874. <message type="groupchat" from="${muc_jid}/eve" to="${_converse.bare_jid}" id="retract-message-1">
  875. <apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
  876. <retract xmlns="urn:xmpp:message-retract:0"/>
  877. </apply-to>
  878. </message>
  879. </forwarded>
  880. </result>
  881. </message>
  882. `);
  883. _converse.connection._dataRecv(mock.createRequest(retraction));
  884. const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
  885. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  886. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  887. .c('first', {'index': '0'}).t(first_id).up()
  888. .c('last').t(last_id).up()
  889. .c('count').t('2');
  890. _converse.connection._dataRecv(mock.createRequest(iq_result));
  891. await u.waitUntil(() => view.model.messages.length === 1);
  892. let message = view.model.messages.at(0);
  893. expect(message.get('retracted')).toBeTruthy();
  894. expect(message.get('is_tombstone')).toBe(true);
  895. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
  896. expect(await view.model.handleRetraction.calls.first().returnValue).toBe(false);
  897. expect(await view.model.handleRetraction.calls.all()[1].returnValue).toBe(true);
  898. expect(view.model.messages.length).toBe(1);
  899. message = view.model.messages.at(0);
  900. expect(message.get('retracted')).toBeTruthy();
  901. expect(message.get('is_tombstone')).toBe(true);
  902. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
  903. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  904. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  905. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  906. expect(el.textContent.trim()).toBe('eve has removed this message');
  907. done();
  908. }));
  909. it("may be returned as a tombstone moderated groupchat message",
  910. mock.initConverse(
  911. ['discoInitialized', 'chatBoxesFetched'], {},
  912. async function (done, _converse) {
  913. const muc_jid = 'lounge@montague.lit';
  914. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  915. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  916. const view = _converse.chatboxviews.get(muc_jid);
  917. const sent_IQs = _converse.connection.IQ_stanzas;
  918. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
  919. const queryid = stanza.querySelector('query').getAttribute('queryid');
  920. const first_id = u.getUniqueId();
  921. const tombstone = u.toStanza(`
  922. <message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
  923. <result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="stanza-id">
  924. <forwarded xmlns="urn:xmpp:forward:0">
  925. <delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
  926. <message type="groupchat" from="${muc_jid}/eve" to="${_converse.bare_jid}" id="message-id-1">
  927. <moderated by="${muc_jid}/bob" stamp="2019-09-20T23:09:32Z" xmlns='urn:xmpp:message-moderate:0'>
  928. <retracted xmlns="urn:xmpp:message-retract:0"/>
  929. <reason>This message contains inappropriate content</reason>
  930. </moderated>
  931. </message>
  932. </forwarded>
  933. </result>
  934. </message>
  935. `);
  936. spyOn(view.model, 'handleModeration').and.callThrough();
  937. _converse.connection._dataRecv(mock.createRequest(tombstone));
  938. const last_id = u.getUniqueId();
  939. const retraction = u.toStanza(`
  940. <message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
  941. <result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="${last_id}">
  942. <forwarded xmlns="urn:xmpp:forward:0">
  943. <delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
  944. <message type="groupchat" from="${muc_jid}" to="${_converse.bare_jid}" id="retract-message-1">
  945. <apply-to id="stanza-id" xmlns="urn:xmpp:fasten:0">
  946. <moderated by="${muc_jid}/bob" xmlns='urn:xmpp:message-moderate:0'>
  947. <retract xmlns="urn:xmpp:message-retract:0"/>
  948. <reason>This message contains inappropriate content</reason>
  949. </moderated>
  950. </apply-to>
  951. </message>
  952. </forwarded>
  953. </result>
  954. </message>
  955. `);
  956. _converse.connection._dataRecv(mock.createRequest(retraction));
  957. const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
  958. .c('fin', {'xmlns': 'urn:xmpp:mam:2'})
  959. .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
  960. .c('first', {'index': '0'}).t(first_id).up()
  961. .c('last').t(last_id).up()
  962. .c('count').t('2');
  963. _converse.connection._dataRecv(mock.createRequest(iq_result));
  964. await u.waitUntil(() => view.model.messages.length);
  965. expect(view.model.messages.length).toBe(1);
  966. let message = view.model.messages.at(0);
  967. expect(message.get('retracted')).toBeTruthy();
  968. expect(message.get('is_tombstone')).toBe(true);
  969. await u.waitUntil(() => view.model.handleModeration.calls.count() === 2);
  970. expect(await view.model.handleModeration.calls.first().returnValue).toBe(false);
  971. expect(await view.model.handleModeration.calls.all()[1].returnValue).toBe(true);
  972. expect(view.model.messages.length).toBe(1);
  973. message = view.model.messages.at(0);
  974. expect(message.get('retracted')).toBeTruthy();
  975. expect(message.get('is_tombstone')).toBe(true);
  976. expect(message.get('moderation_reason')).toBe("This message contains inappropriate content");
  977. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length, 500);
  978. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  979. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  980. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  981. expect(el.textContent.trim()).toBe('A moderator has removed this message');
  982. const qel = view.el.querySelector('.chat-msg--retracted .chat-msg__message q');
  983. expect(qel.textContent.trim()).toBe('This message contains inappropriate content');
  984. done();
  985. }));
  986. });
  987. })