retractions.js 63 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142
  1. /*global mock */
  2. const { Strophe, $iq } = converse.env;
  3. const u = converse.env.utils;
  4. async function sendAndThenRetractMessage (_converse, view) {
  5. view.model.sendMessage('hello world');
  6. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text').length === 1);
  7. const msg_obj = view.model.messages.last();
  8. const reflection_stanza = u.toStanza(`
  9. <message xmlns="jabber:client"
  10. from="${msg_obj.get('from')}"
  11. to="${_converse.connection.jid}"
  12. type="groupchat">
  13. <msg_body>${msg_obj.get('message')}</msg_body>
  14. <stanza-id xmlns="urn:xmpp:sid:0"
  15. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  16. by="lounge@montague.lit"/>
  17. <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
  18. </message>`);
  19. await view.model.handleMessageStanza(reflection_stanza);
  20. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
  21. const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
  22. retract_button.click();
  23. await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
  24. const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
  25. submit_button.click();
  26. const sent_stanzas = _converse.connection.sent_stanzas;
  27. return u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('message apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
  28. }
  29. describe("Message Retractions", function () {
  30. describe("A groupchat message retraction", function () {
  31. it("is not applied if it's not from the right author",
  32. mock.initConverse(
  33. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  34. async function (done, _converse) {
  35. const muc_jid = 'lounge@montague.lit';
  36. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  37. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  38. const received_stanza = u.toStanza(`
  39. <message to='${_converse.jid}' from='${muc_jid}/eve' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  40. <body>Hello world</body>
  41. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  42. </message>
  43. `);
  44. const view = _converse.api.chatviews.get(muc_jid);
  45. await view.model.handleMessageStanza(received_stanza);
  46. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  47. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  48. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  49. const retraction_stanza = u.toStanza(`
  50. <message type="groupchat" id='retraction-id-1' from="${muc_jid}/mallory" to="${muc_jid}/romeo">
  51. <apply-to id="stanza-id-1" xmlns="urn:xmpp:fasten:0">
  52. <retract xmlns="urn:xmpp:message-retract:0" />
  53. </apply-to>
  54. </message>
  55. `);
  56. spyOn(view.model, 'handleRetraction').and.callThrough();
  57. _converse.connection._dataRecv(mock.createRequest(retraction_stanza));
  58. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
  59. expect(await view.model.handleRetraction.calls.first().returnValue).toBe(true);
  60. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  61. expect(view.model.messages.length).toBe(2);
  62. expect(view.model.messages.at(1).get('retracted')).toBeTruthy();
  63. expect(view.model.messages.at(1).get('is_ephemeral')).toBeFalsy();
  64. expect(view.model.messages.at(1).get('dangling_retraction')).toBe(true);
  65. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  66. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  67. done();
  68. }));
  69. it("can be received before the message it pertains to",
  70. mock.initConverse(
  71. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  72. async function (done, _converse) {
  73. const date = (new Date()).toISOString();
  74. const muc_jid = 'lounge@montague.lit';
  75. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  76. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  77. const retraction_stanza = u.toStanza(`
  78. <message type="groupchat" id='retraction-id-1' from="${muc_jid}/eve" to="${muc_jid}/romeo">
  79. <apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
  80. <retract by="${muc_jid}/eve" xmlns="urn:xmpp:message-retract:0" />
  81. </apply-to>
  82. </message>
  83. `);
  84. const view = _converse.api.chatviews.get(muc_jid);
  85. spyOn(converse.env.log, 'warn');
  86. spyOn(view.model, 'handleRetraction').and.callThrough();
  87. _converse.connection._dataRecv(mock.createRequest(retraction_stanza));
  88. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
  89. await u.waitUntil(() => view.model.messages.length === 1);
  90. expect(await view.model.handleRetraction.calls.first().returnValue).toBe(true);
  91. expect(view.model.messages.length).toBe(1);
  92. expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
  93. expect(view.model.messages.at(0).get('dangling_retraction')).toBe(true);
  94. const received_stanza = u.toStanza(`
  95. <message to='${_converse.jid}' from='${muc_jid}/eve' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  96. <body>Hello world</body>
  97. <delay xmlns='urn:xmpp:delay' stamp='${date}'/>
  98. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  99. <origin-id xmlns="urn:xmpp:sid:0" id="origin-id-1"/>
  100. </message>
  101. `);
  102. _converse.connection._dataRecv(mock.createRequest(received_stanza));
  103. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
  104. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1, 1000);
  105. expect(view.model.messages.length).toBe(1);
  106. const message = view.model.messages.at(0)
  107. expect(message.get('retracted')).toBeTruthy();
  108. expect(message.get('dangling_retraction')).toBe(false);
  109. expect(message.get('origin_id')).toBe('origin-id-1');
  110. expect(message.get(`stanza_id ${muc_jid}`)).toBe('stanza-id-1');
  111. expect(message.get('time')).toBe(date);
  112. expect(message.get('type')).toBe('groupchat');
  113. expect(await view.model.handleRetraction.calls.all().pop().returnValue).toBe(true);
  114. done();
  115. }));
  116. });
  117. describe("A groupchat message moderator retraction", function () {
  118. it("can be received before the message it pertains to",
  119. mock.initConverse(
  120. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  121. async function (done, _converse) {
  122. const date = (new Date()).toISOString();
  123. const muc_jid = 'lounge@montague.lit';
  124. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  125. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  126. const retraction_stanza = u.toStanza(`
  127. <message xmlns="jabber:client" from="${muc_jid}" type="groupchat" id="retraction-id-1">
  128. <apply-to xmlns="urn:xmpp:fasten:0" id="stanza-id-1">
  129. <moderated xmlns="urn:xmpp:message-moderate:0" by="${muc_jid}/madison">
  130. <retract xmlns="urn:xmpp:message-retract:0"/>
  131. <reason>Insults</reason>
  132. </moderated>
  133. </apply-to>
  134. </message>
  135. `);
  136. const view = _converse.api.chatviews.get(muc_jid);
  137. spyOn(converse.env.log, 'warn');
  138. spyOn(view.model, 'handleModeration').and.callThrough();
  139. _converse.connection._dataRecv(mock.createRequest(retraction_stanza));
  140. await u.waitUntil(() => view.model.handleModeration.calls.count() === 1);
  141. await u.waitUntil(() => view.model.messages.length === 1);
  142. expect(await view.model.handleModeration.calls.first().returnValue).toBe(true);
  143. expect(view.model.messages.length).toBe(1);
  144. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  145. expect(view.model.messages.at(0).get('dangling_moderation')).toBe(true);
  146. const received_stanza = u.toStanza(`
  147. <message to='${_converse.jid}' from='${muc_jid}/eve' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  148. <body>Hello world</body>
  149. <delay xmlns='urn:xmpp:delay' stamp='${date}'/>
  150. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  151. </message>
  152. `);
  153. _converse.connection._dataRecv(mock.createRequest(received_stanza));
  154. await u.waitUntil(() => view.model.handleModeration.calls.count() === 2);
  155. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
  156. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  157. expect(view.model.messages.length).toBe(1);
  158. const message = view.model.messages.at(0)
  159. expect(message.get('moderated')).toBe('retracted');
  160. expect(message.get('dangling_moderation')).toBe(false);
  161. expect(message.get(`stanza_id ${muc_jid}`)).toBe('stanza-id-1');
  162. expect(message.get('time')).toBe(date);
  163. expect(message.get('type')).toBe('groupchat');
  164. expect(await view.model.handleModeration.calls.all().pop().returnValue).toBe(true);
  165. done();
  166. }));
  167. });
  168. describe("A message retraction", function () {
  169. it("can be received before the message it pertains to",
  170. mock.initConverse(
  171. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  172. async function (done, _converse) {
  173. const date = (new Date()).toISOString();
  174. await mock.waitForRoster(_converse, 'current', 1);
  175. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
  176. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  177. const view = await mock.openChatBoxFor(_converse, contact_jid);
  178. spyOn(view.model, 'handleRetraction').and.callThrough();
  179. const retraction_stanza = u.toStanza(`
  180. <message id="${u.getUniqueId()}"
  181. to="${_converse.bare_jid}"
  182. from="${contact_jid}"
  183. type="chat"
  184. xmlns="jabber:client">
  185. <apply-to id="2e972ea0-0050-44b7-a830-f6638a2595b3" xmlns="urn:xmpp:fasten:0">
  186. <retract xmlns="urn:xmpp:message-retract:0"/>
  187. </apply-to>
  188. </message>
  189. `);
  190. _converse.connection._dataRecv(mock.createRequest(retraction_stanza));
  191. await u.waitUntil(() => view.model.messages.length === 1);
  192. const message = view.model.messages.at(0);
  193. expect(message.get('dangling_retraction')).toBe(true);
  194. expect(message.get('is_ephemeral')).toBe(false);
  195. expect(message.get('retracted')).toBeTruthy();
  196. expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
  197. const stanza = u.toStanza(`
  198. <message xmlns="jabber:client"
  199. to="${_converse.bare_jid}"
  200. type="chat"
  201. id="2e972ea0-0050-44b7-a830-f6638a2595b3"
  202. from="${contact_jid}">
  203. <body>Hello world</body>
  204. <delay xmlns='urn:xmpp:delay' stamp='${date}'/>
  205. <markable xmlns="urn:xmpp:chat-markers:0"/>
  206. <origin-id xmlns="urn:xmpp:sid:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
  207. <stanza-id xmlns="urn:xmpp:sid:0" id="IxVDLJ0RYbWcWvqC" by="${_converse.bare_jid}"/>
  208. </message>`);
  209. _converse.connection._dataRecv(mock.createRequest(stanza));
  210. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
  211. expect(view.model.messages.length).toBe(1);
  212. expect(message.get('retracted')).toBeTruthy();
  213. expect(message.get('dangling_retraction')).toBe(false);
  214. expect(message.get('origin_id')).toBe('2e972ea0-0050-44b7-a830-f6638a2595b3');
  215. expect(message.get('time')).toBe(date);
  216. expect(message.get('type')).toBe('chat');
  217. done();
  218. }));
  219. });
  220. describe("A Received Chat Message", function () {
  221. it("can be followed up by a retraction",
  222. mock.initConverse(
  223. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  224. async function (done, _converse) {
  225. await mock.waitForRoster(_converse, 'current', 1);
  226. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
  227. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  228. const view = await mock.openChatBoxFor(_converse, contact_jid);
  229. let stanza = u.toStanza(`
  230. <message xmlns="jabber:client"
  231. to="${_converse.bare_jid}"
  232. type="chat"
  233. id="29132ea0-0121-2897-b121-36638c259554"
  234. from="${contact_jid}">
  235. <body>😊</body>
  236. <markable xmlns="urn:xmpp:chat-markers:0"/>
  237. <origin-id xmlns="urn:xmpp:sid:0" id="29132ea0-0121-2897-b121-36638c259554"/>
  238. <stanza-id xmlns="urn:xmpp:sid:0" id="kxViLhgbnNMcWv10" by="${_converse.bare_jid}"/>
  239. </message>`);
  240. _converse.connection._dataRecv(mock.createRequest(stanza));
  241. await u.waitUntil(() => view.model.messages.length === 1);
  242. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  243. stanza = u.toStanza(`
  244. <message xmlns="jabber:client"
  245. to="${_converse.bare_jid}"
  246. type="chat"
  247. id="2e972ea0-0050-44b7-a830-f6638a2595b3"
  248. from="${contact_jid}">
  249. <body>This message will be retracted</body>
  250. <markable xmlns="urn:xmpp:chat-markers:0"/>
  251. <origin-id xmlns="urn:xmpp:sid:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
  252. <stanza-id xmlns="urn:xmpp:sid:0" id="IxVDLJ0RYbWcWvqC" by="${_converse.bare_jid}"/>
  253. </message>`);
  254. _converse.connection._dataRecv(mock.createRequest(stanza));
  255. await u.waitUntil(() => view.model.messages.length === 2);
  256. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
  257. const retraction_stanza = u.toStanza(`
  258. <message id="${u.getUniqueId()}"
  259. to="${_converse.bare_jid}"
  260. from="${contact_jid}"
  261. type="chat"
  262. xmlns="jabber:client">
  263. <apply-to id="2e972ea0-0050-44b7-a830-f6638a2595b3" xmlns="urn:xmpp:fasten:0">
  264. <retract xmlns="urn:xmpp:message-retract:0"/>
  265. </apply-to>
  266. </message>
  267. `);
  268. _converse.connection._dataRecv(mock.createRequest(retraction_stanza));
  269. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  270. expect(view.model.messages.length).toBe(2);
  271. const message = view.model.messages.at(1);
  272. expect(message.get('retracted')).toBeTruthy();
  273. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  274. const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
  275. expect(msg_el.textContent.trim()).toBe('Mercutio has removed this message');
  276. expect(u.hasClass('chat-msg--followup', view.el.querySelector('.chat-msg--retracted'))).toBe(true);
  277. done();
  278. }));
  279. });
  280. describe("A Sent Chat Message", function () {
  281. it("can be retracted by its author",
  282. mock.initConverse(
  283. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  284. async function (done, _converse) {
  285. await mock.waitForRoster(_converse, 'current', 1);
  286. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  287. const view = await mock.openChatBoxFor(_converse, contact_jid);
  288. view.model.sendMessage('hello world');
  289. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  290. const message = view.model.messages.at(0);
  291. expect(view.model.messages.length).toBe(1);
  292. expect(message.get('retracted')).toBeFalsy();
  293. expect(message.get('editable')).toBeTruthy();
  294. const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
  295. retract_button.click();
  296. await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
  297. const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
  298. submit_button.click();
  299. const sent_stanzas = _converse.connection.sent_stanzas;
  300. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  301. const msg_obj = view.model.messages.at(0);
  302. const retraction_stanza = await u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('message apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
  303. expect(Strophe.serialize(retraction_stanza)).toBe(
  304. `<message id="${retraction_stanza.getAttribute('id')}" to="${contact_jid}" type="chat" xmlns="jabber:client">`+
  305. `<store xmlns="urn:xmpp:hints"/>`+
  306. `<apply-to id="${msg_obj.get('origin_id')}" xmlns="urn:xmpp:fasten:0">`+
  307. `<retract xmlns="urn:xmpp:message-retract:0"/>`+
  308. `</apply-to>`+
  309. `</message>`);
  310. expect(view.model.messages.length).toBe(1);
  311. expect(message.get('retracted')).toBeTruthy();
  312. expect(message.get('editable')).toBeFalsy();
  313. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  314. const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
  315. expect(el.textContent.trim()).toBe('Romeo Montague has removed this message');
  316. done();
  317. }));
  318. });
  319. describe("A Received Groupchat Message", function () {
  320. it("can be followed up by a retraction by the author",
  321. mock.initConverse(
  322. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  323. async function (done, _converse) {
  324. const muc_jid = 'lounge@montague.lit';
  325. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  326. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  327. const received_stanza = u.toStanza(`
  328. <message to='${_converse.jid}' from='${muc_jid}/eve' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  329. <body>Hello world</body>
  330. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  331. <origin-id xmlns='urn:xmpp:sid:0' id='origin-id-1' by='${muc_jid}'/>
  332. </message>
  333. `);
  334. const view = _converse.api.chatviews.get(muc_jid);
  335. await view.model.handleMessageStanza(received_stanza);
  336. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  337. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  338. expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
  339. const retraction_stanza = u.toStanza(`
  340. <message type="groupchat" id='retraction-id-1' from="${muc_jid}/eve" to="${muc_jid}/romeo">
  341. <apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
  342. <retract by="${muc_jid}/eve" xmlns="urn:xmpp:message-retract:0" />
  343. </apply-to>
  344. </message>
  345. `);
  346. _converse.connection._dataRecv(mock.createRequest(retraction_stanza));
  347. // We opportunistically save the message as retracted, even before receiving the retraction message
  348. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  349. expect(view.model.messages.length).toBe(1);
  350. expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
  351. expect(view.model.messages.at(0).get('editable')).toBe(false);
  352. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  353. const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
  354. expect(msg_el.textContent.trim()).toBe('eve has removed this message');
  355. expect(msg_el.querySelector('.chat-msg--retracted q')).toBe(null);
  356. done();
  357. }));
  358. it("can be retracted by a moderator, with the IQ response received before the retraction message",
  359. mock.initConverse(
  360. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  361. async function (done, _converse) {
  362. const muc_jid = 'lounge@montague.lit';
  363. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  364. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  365. const view = _converse.api.chatviews.get(muc_jid);
  366. const occupant = view.model.getOwnOccupant();
  367. expect(occupant.get('role')).toBe('moderator');
  368. const received_stanza = u.toStanza(`
  369. <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  370. <body>Visit this site to get free Bitcoin!</body>
  371. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  372. </message>
  373. `);
  374. await view.model.handleMessageStanza(received_stanza);
  375. await u.waitUntil(() => view.model.messages.length === 1);
  376. expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
  377. const reason = "This content is inappropriate for this forum!"
  378. const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
  379. retract_button.click();
  380. await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
  381. const reason_input = document.querySelector('#converse-modals .modal input[name="reason"]');
  382. reason_input.value = 'This content is inappropriate for this forum!';
  383. const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
  384. submit_button.click();
  385. const sent_IQs = _converse.connection.IQ_stanzas;
  386. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
  387. const message = view.model.messages.at(0);
  388. const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
  389. expect(Strophe.serialize(stanza)).toBe(
  390. `<iq id="${stanza.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
  391. `<apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">`+
  392. `<moderate xmlns="urn:xmpp:message-moderate:0">`+
  393. `<retract xmlns="urn:xmpp:message-retract:0"/>`+
  394. `<reason>This content is inappropriate for this forum!</reason>`+
  395. `</moderate>`+
  396. `</apply-to>`+
  397. `</iq>`);
  398. const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'});
  399. _converse.connection._dataRecv(mock.createRequest(result_iq));
  400. // We opportunistically save the message as retracted, even before receiving the retraction message
  401. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  402. expect(view.model.messages.length).toBe(1);
  403. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  404. expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
  405. expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
  406. expect(view.model.messages.at(0).get('editable')).toBe(false);
  407. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  408. const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
  409. expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has removed this message');
  410. const qel = msg_el.querySelector('q');
  411. expect(qel.textContent.trim()).toBe('This content is inappropriate for this forum!');
  412. // The server responds with a retraction message
  413. const retraction = u.toStanza(`
  414. <message type="groupchat" id='retraction-id-1' from="${muc_jid}" to="${muc_jid}/romeo">
  415. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  416. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  417. <retract xmlns='urn:xmpp:message-retract:0' />
  418. <reason>${reason}</reason>
  419. </moderated>
  420. </apply-to>
  421. </message>`);
  422. await view.model.handleMessageStanza(retraction);
  423. expect(view.model.messages.length).toBe(1);
  424. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  425. expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
  426. expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
  427. expect(view.model.messages.at(0).get('editable')).toBe(false);
  428. done();
  429. }));
  430. it("can not be retracted if the MUC doesn't support message moderation",
  431. mock.initConverse(
  432. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  433. async function (done, _converse) {
  434. const muc_jid = 'lounge@montague.lit';
  435. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  436. const view = _converse.api.chatviews.get(muc_jid);
  437. const occupant = view.model.getOwnOccupant();
  438. expect(occupant.get('role')).toBe('moderator');
  439. const received_stanza = u.toStanza(`
  440. <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  441. <body>Visit this site to get free Bitcoin!</body>
  442. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  443. </message>
  444. `);
  445. await view.model.handleMessageStanza(received_stanza);
  446. await u.waitUntil(() => view.el.querySelector('.chat-msg__content'));
  447. expect(view.el.querySelector('.chat-msg__content .chat-msg__action-retract')).toBe(null);
  448. const result = await view.model.canModerateMessages();
  449. expect(result).toBe(false);
  450. done();
  451. }));
  452. it("can be retracted by a moderator, with the retraction message received before the IQ response",
  453. mock.initConverse(
  454. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  455. async function (done, _converse) {
  456. const muc_jid = 'lounge@montague.lit';
  457. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  458. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  459. const view = _converse.api.chatviews.get(muc_jid);
  460. const occupant = view.model.getOwnOccupant();
  461. expect(occupant.get('role')).toBe('moderator');
  462. const received_stanza = u.toStanza(`
  463. <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  464. <body>Visit this site to get free Bitcoin!</body>
  465. <stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
  466. </message>
  467. `);
  468. await view.model.handleMessageStanza(received_stanza);
  469. await u.waitUntil(() => view.model.messages.length === 1);
  470. expect(view.model.messages.length).toBe(1);
  471. const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
  472. retract_button.click();
  473. await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
  474. const reason_input = document.querySelector('#converse-modals .modal input[name="reason"]');
  475. const reason = "This content is inappropriate for this forum!"
  476. reason_input.value = reason;
  477. const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
  478. submit_button.click();
  479. const sent_IQs = _converse.connection.IQ_stanzas;
  480. const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
  481. const message = view.model.messages.at(0);
  482. const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
  483. // The server responds with a retraction message
  484. const retraction = u.toStanza(`
  485. <message type="groupchat" id='retraction-id-1' from="${muc_jid}" to="${muc_jid}/romeo">
  486. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  487. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  488. <retract xmlns='urn:xmpp:message-retract:0' />
  489. <reason>${reason}</reason>
  490. </moderated>
  491. </apply-to>
  492. </message>`);
  493. await view.model.handleMessageStanza(retraction);
  494. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
  495. expect(view.model.messages.length).toBe(1);
  496. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  497. expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
  498. const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
  499. expect(msg_el.textContent).toBe('romeo has removed this message');
  500. const qel = view.el.querySelector('.chat-msg--retracted .chat-msg__message q');
  501. expect(qel.textContent).toBe('This content is inappropriate for this forum!');
  502. const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'});
  503. _converse.connection._dataRecv(mock.createRequest(result_iq));
  504. expect(view.model.messages.length).toBe(1);
  505. expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
  506. expect(view.model.messages.at(0).get('moderated_by')).toBe(_converse.bare_jid);
  507. expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
  508. expect(view.model.messages.at(0).get('editable')).toBe(false);
  509. done();
  510. }));
  511. });
  512. describe("A Sent Groupchat Message", function () {
  513. it("can be retracted by its author",
  514. mock.initConverse(
  515. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  516. async function (done, _converse) {
  517. const muc_jid = 'lounge@montague.lit';
  518. const features = [...mock.default_muc_features, Strophe.NS.MODERATE];
  519. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
  520. const view = _converse.api.chatviews.get(muc_jid);
  521. const occupant = view.model.getOwnOccupant();
  522. expect(occupant.get('role')).toBe('moderator');
  523. occupant.save('role', 'member');
  524. const retraction_stanza = await sendAndThenRetractMessage(_converse, view);
  525. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1, 1000);
  526. console.log('XXX: First message retracted by author');
  527. const msg_obj = view.model.messages.last();
  528. expect(msg_obj.get('retracted')).toBeTruthy();
  529. expect(Strophe.serialize(retraction_stanza)).toBe(
  530. `<message id="${retraction_stanza.getAttribute('id')}" to="${muc_jid}" type="groupchat" xmlns="jabber:client">`+
  531. `<store xmlns="urn:xmpp:hints"/>`+
  532. `<apply-to id="${msg_obj.get('origin_id')}" xmlns="urn:xmpp:fasten:0">`+
  533. `<retract xmlns="urn:xmpp:message-retract:0"/>`+
  534. `</apply-to>`+
  535. `</message>`);
  536. const message = view.model.messages.last();
  537. expect(message.get('is_ephemeral')).toBe(false);
  538. expect(message.get('editable')).toBeFalsy();
  539. const stanza_id = message.get(`stanza_id ${muc_jid}`);
  540. // The server responds with a retraction message
  541. const reflection = u.toStanza(`
  542. <message type="groupchat" id="${retraction_stanza.getAttribute('id')}" from="${muc_jid}" to="${muc_jid}/romeo">
  543. <apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
  544. <moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
  545. <retract xmlns='urn:xmpp:message-retract:0' />
  546. </moderated>
  547. </apply-to>
  548. </message>`);
  549. spyOn(view.model, 'handleRetraction').and.callThrough();
  550. _converse.connection._dataRecv(mock.createRequest(reflection));
  551. await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1, 1000);
  552. console.log('XXX: Handle retraction was called on reflection');
  553. await u.waitUntil(() => view.model.messages.length === 1, 1000);
  554. console.log('XXX: We have one message');
  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, 1000);
  577. expect(view.model.messages.length).toBe(1);
  578. await u.waitUntil(() => view.model.messages.last().get('retracted'), 1000);
  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, 1000);
  597. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0, 1000);
  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')).toBe(false);
  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. await u.waitUntil(() => message.get('retracted'));
  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. })