muc_messages.js 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876
  1. /*global mock */
  2. const { Promise, Strophe, $msg, $pres, sizzle, stanza_utils } = converse.env;
  3. const u = converse.env.utils;
  4. const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
  5. describe("A Groupchat Message", function () {
  6. beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000));
  7. afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
  8. describe("which is succeeded by an error message", function () {
  9. it("will have the error displayed below it",
  10. mock.initConverse(
  11. ['rosterGroupsFetched'], {},
  12. async function (done, _converse) {
  13. const muc_jid = 'lounge@montague.lit';
  14. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  15. const view = _converse.api.chatviews.get(muc_jid);
  16. const textarea = view.el.querySelector('textarea.chat-textarea');
  17. textarea.value = 'hello world'
  18. const enter_event = {
  19. 'target': textarea,
  20. 'preventDefault': function preventDefault () {},
  21. 'stopPropagation': function stopPropagation () {},
  22. 'keyCode': 13 // Enter
  23. }
  24. view.onKeyDown(enter_event);
  25. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  26. const msg = view.model.messages.at(0);
  27. const err_msg_text = "Message rejected because you're sending messages too quickly";
  28. const error = u.toStanza(`
  29. <message xmlns="jabber:client" id="${msg.get('msgid')}" from="${muc_jid}" to="${_converse.jid}" type="error">
  30. <error type="wait">
  31. <policy-violation xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
  32. <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">${err_msg_text}</text>
  33. </error>
  34. <body>hello world</body>
  35. </message>
  36. `);
  37. _converse.connection._dataRecv(mock.createRequest(error));
  38. expect(await u.waitUntil(() => view.el.querySelector('.chat-msg__error')?.textContent?.trim())).toBe(err_msg_text);
  39. expect(view.model.messages.length).toBe(1);
  40. const message = view.model.messages.at(0);
  41. expect(message.get('received')).toBeUndefined();
  42. expect(message.get('body')).toBe('hello world');
  43. expect(message.get('error_text')).toBe(err_msg_text);
  44. expect(message.get('editable')).toBe(false);
  45. done();
  46. }));
  47. });
  48. describe("an info message", function () {
  49. it("is not rendered as a followup message",
  50. mock.initConverse(
  51. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  52. async function (done, _converse) {
  53. const muc_jid = 'lounge@montague.lit';
  54. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  55. const view = _converse.api.chatviews.get(muc_jid);
  56. let presence = u.toStanza(`
  57. <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo">
  58. <x xmlns="http://jabber.org/protocol/muc#user">
  59. <status code="201"/>
  60. <item role="moderator" affiliation="owner" jid="${_converse.jid}"/>
  61. <status code="110"/>
  62. </x>
  63. </presence>
  64. `);
  65. _converse.connection._dataRecv(mock.createRequest(presence));
  66. await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 1);
  67. presence = u.toStanza(`
  68. <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo1">
  69. <x xmlns="http://jabber.org/protocol/muc#user">
  70. <status code="210"/>
  71. <item role="moderator" affiliation="owner" jid="${_converse.jid}"/>
  72. <status code="110"/>
  73. </x>
  74. </presence>
  75. `);
  76. _converse.connection._dataRecv(mock.createRequest(presence));
  77. await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2);
  78. const messages = view.el.querySelectorAll('.chat-info');
  79. expect(u.hasClass('chat-msg--followup', messages[0])).toBe(false);
  80. expect(u.hasClass('chat-msg--followup', messages[1])).toBe(false);
  81. done();
  82. }));
  83. it("is not shown if its a duplicate",
  84. mock.initConverse(
  85. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  86. async function (done, _converse) {
  87. const muc_jid = 'lounge@montague.lit';
  88. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  89. const view = _converse.api.chatviews.get(muc_jid);
  90. const presence = u.toStanza(`
  91. <presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo">
  92. <x xmlns="http://jabber.org/protocol/muc#user">
  93. <status code="201"/>
  94. <item role="moderator" affiliation="owner" jid="${_converse.jid}"/>
  95. <status code="110"/>
  96. </x>
  97. </presence>
  98. `);
  99. // XXX: We wait for createInfoMessages to complete, if we don't
  100. // we still get two info messages due to messages
  101. // created from presences not being queued and run
  102. // sequentially (i.e. by waiting for promises to resolve)
  103. // like we do with message stanzas.
  104. spyOn(view.model, 'createInfoMessages').and.callThrough();
  105. _converse.connection._dataRecv(mock.createRequest(presence));
  106. await u.waitUntil(() => view.model.createInfoMessages.calls.count());
  107. await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 1);
  108. _converse.connection._dataRecv(mock.createRequest(presence));
  109. await u.waitUntil(() => view.model.createInfoMessages.calls.count() === 2);
  110. expect(view.el.querySelectorAll('.chat-info').length).toBe(1);
  111. done();
  112. }));
  113. });
  114. it("is rejected if it's an unencapsulated forwarded message",
  115. mock.initConverse(
  116. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  117. async function (done, _converse) {
  118. const muc_jid = 'lounge@montague.lit';
  119. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  120. const impersonated_jid = `${muc_jid}/alice`;
  121. const received_stanza = u.toStanza(`
  122. <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
  123. <forwarded xmlns='urn:xmpp:forward:0'>
  124. <delay xmlns='urn:xmpp:delay' stamp='2019-07-10T23:08:25Z'/>
  125. <message from='${impersonated_jid}'
  126. id='0202197'
  127. to='${_converse.bare_jid}'
  128. type='groupchat'
  129. xmlns='jabber:client'>
  130. <body>Yet I should kill thee with much cherishing.</body>
  131. </message>
  132. </forwarded>
  133. </message>
  134. `);
  135. const view = _converse.api.chatviews.get(muc_jid);
  136. spyOn(view.model, 'onMessage').and.callThrough();
  137. spyOn(converse.env.log, 'error');
  138. _converse.connection._dataRecv(mock.createRequest(received_stanza));
  139. await u.waitUntil(() => view.model.onMessage.calls.count() === 1);
  140. expect(converse.env.log.error).toHaveBeenCalledWith(
  141. `Ignoring unencapsulated forwarded message from ${muc_jid}/mallory`
  142. );
  143. expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
  144. expect(view.model.messages.length).toBe(0);
  145. done();
  146. }));
  147. it("can contain a chat state notification and will still be shown",
  148. mock.initConverse(
  149. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  150. async function (done, _converse) {
  151. const muc_jid = 'lounge@montague.lit';
  152. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  153. const view = _converse.api.chatviews.get(muc_jid);
  154. if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); }
  155. const message = 'romeo: Your attention is required';
  156. const nick = mock.chatroom_names[0],
  157. msg = $msg({
  158. from: 'lounge@montague.lit/'+nick,
  159. id: u.getUniqueId(),
  160. to: 'romeo@montague.lit',
  161. type: 'groupchat'
  162. }).c('body').t(message)
  163. .c('active', {'xmlns': "http://jabber.org/protocol/chatstates"})
  164. .tree();
  165. await view.model.handleMessageStanza(msg);
  166. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  167. expect(view.el.querySelector('.chat-msg')).not.toBe(null);
  168. done();
  169. }));
  170. it("can not be expected to have a unique id attribute",
  171. mock.initConverse(
  172. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  173. async function (done, _converse) {
  174. const muc_jid = 'lounge@montague.lit';
  175. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  176. const view = _converse.api.chatviews.get(muc_jid);
  177. if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); }
  178. const id = u.getUniqueId();
  179. let msg = $msg({
  180. from: 'lounge@montague.lit/some1',
  181. id: id,
  182. to: 'romeo@montague.lit',
  183. type: 'groupchat'
  184. }).c('body').t('First message').tree();
  185. await view.model.handleMessageStanza(msg);
  186. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  187. msg = $msg({
  188. from: 'lounge@montague.lit/some2',
  189. id: id,
  190. to: 'romeo@montague.lit',
  191. type: 'groupchat'
  192. }).c('body').t('Another message').tree();
  193. await view.model.handleMessageStanza(msg);
  194. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
  195. expect(view.model.messages.length).toBe(2);
  196. done();
  197. }));
  198. it("is ignored if it has the same archive-id of an already received one",
  199. mock.initConverse(
  200. ['rosterGroupsFetched'], {},
  201. async function (done, _converse) {
  202. const muc_jid = 'room@muc.example.com';
  203. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  204. const view = _converse.api.chatviews.get(muc_jid);
  205. spyOn(view.model, 'getDuplicateMessage').and.callThrough();
  206. let stanza = u.toStanza(`
  207. <message xmlns="jabber:client"
  208. from="room@muc.example.com/some1"
  209. to="${_converse.connection.jid}"
  210. type="groupchat">
  211. <body>Typical body text</body>
  212. <stanza-id xmlns="urn:xmpp:sid:0"
  213. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  214. by="room@muc.example.com"/>
  215. </message>`);
  216. _converse.connection._dataRecv(mock.createRequest(stanza));
  217. await u.waitUntil(() => view.model.messages.length === 1);
  218. await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 1);
  219. let result = await view.model.getDuplicateMessage.calls.all()[0].returnValue;
  220. expect(result).toBe(undefined);
  221. stanza = u.toStanza(`
  222. <message xmlns="jabber:client"
  223. to="${_converse.connection.jid}"
  224. from="room@muc.example.com">
  225. <result xmlns="urn:xmpp:mam:2" queryid="82d9db27-6cf8-4787-8c2c-5a560263d823" id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad">
  226. <forwarded xmlns="urn:xmpp:forward:0">
  227. <delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:17:23Z"/>
  228. <message from="room@muc.example.com/some1" type="groupchat">
  229. <body>Typical body text</body>
  230. </message>
  231. </forwarded>
  232. </result>
  233. </message>`);
  234. spyOn(view.model, 'updateMessage');
  235. view.model.handleMAMResult({ 'messages': [stanza] });
  236. await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 2);
  237. result = await view.model.getDuplicateMessage.calls.all()[1].returnValue;
  238. expect(result instanceof _converse.Message).toBe(true);
  239. expect(view.model.messages.length).toBe(1);
  240. await u.waitUntil(() => view.model.updateMessage.calls.count());
  241. done();
  242. }));
  243. it("is ignored if it has the same stanza-id of an already received one",
  244. mock.initConverse(
  245. ['rosterGroupsFetched'], {},
  246. async function (done, _converse) {
  247. const muc_jid = 'room@muc.example.com';
  248. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  249. const view = _converse.api.chatviews.get(muc_jid);
  250. spyOn(view.model, 'getStanzaIdQueryAttrs').and.callThrough();
  251. let stanza = u.toStanza(`
  252. <message xmlns="jabber:client"
  253. from="room@muc.example.com/some1"
  254. to="${_converse.connection.jid}"
  255. type="groupchat">
  256. <body>Typical body text</body>
  257. <stanza-id xmlns="urn:xmpp:sid:0"
  258. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  259. by="room@muc.example.com"/>
  260. </message>`);
  261. _converse.connection._dataRecv(mock.createRequest(stanza));
  262. await u.waitUntil(() => view.model.messages.length === 1);
  263. await u.waitUntil(() => view.model.getStanzaIdQueryAttrs.calls.count() === 1);
  264. let result = await view.model.getStanzaIdQueryAttrs.calls.all()[0].returnValue;
  265. expect(result instanceof Array).toBe(true);
  266. expect(result[0] instanceof Object).toBe(true);
  267. expect(result[0]['stanza_id room@muc.example.com']).toBe("5f3dbc5e-e1d3-4077-a492-693f3769c7ad");
  268. stanza = u.toStanza(`
  269. <message xmlns="jabber:client"
  270. from="room@muc.example.com/some1"
  271. to="${_converse.connection.jid}"
  272. type="groupchat">
  273. <body>Typical body text</body>
  274. <stanza-id xmlns="urn:xmpp:sid:0"
  275. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  276. by="room@muc.example.com"/>
  277. </message>`);
  278. spyOn(view.model, 'updateMessage');
  279. spyOn(view.model, 'getDuplicateMessage').and.callThrough();
  280. _converse.connection._dataRecv(mock.createRequest(stanza));
  281. await u.waitUntil(() => view.model.getDuplicateMessage.calls.count());
  282. result = await view.model.getDuplicateMessage.calls.all()[0].returnValue;
  283. expect(result instanceof _converse.Message).toBe(true);
  284. expect(view.model.messages.length).toBe(1);
  285. await u.waitUntil(() => view.model.updateMessage.calls.count());
  286. done();
  287. }));
  288. it("will be discarded if it's a malicious message meant to look like a carbon copy",
  289. mock.initConverse(
  290. ['rosterGroupsFetched'], {},
  291. async function (done, _converse) {
  292. await mock.waitForRoster(_converse, 'current');
  293. await mock.openControlBox(_converse);
  294. const muc_jid = 'xsf@muc.xmpp.org';
  295. const sender_jid = `${muc_jid}/romeo`;
  296. const impersonated_jid = `${muc_jid}/i_am_groot`
  297. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  298. const stanza = $pres({
  299. to: 'romeo@montague.lit/_converse.js-29092160',
  300. from: sender_jid
  301. })
  302. .c('x', {xmlns: Strophe.NS.MUC_USER})
  303. .c('item', {
  304. 'affiliation': 'none',
  305. 'jid': 'newguy@montague.lit/_converse.js-290929789',
  306. 'role': 'participant'
  307. }).tree();
  308. _converse.connection._dataRecv(mock.createRequest(stanza));
  309. /*
  310. * <message to="romeo@montague.im/poezio" id="718d40df-3948-4798-a99b-35cc9f03cc4f-641" type="groupchat" from="xsf@muc.xmpp.org/romeo">
  311. * <received xmlns="urn:xmpp:carbons:2">
  312. * <forwarded xmlns="urn:xmpp:forward:0">
  313. * <message xmlns="jabber:client" to="xsf@muc.xmpp.org" type="groupchat" from="xsf@muc.xmpp.org/i_am_groot">
  314. * <body>I am groot.</body>
  315. * </message>
  316. * </forwarded>
  317. * </received>
  318. * </message>
  319. */
  320. const msg = $msg({
  321. 'from': sender_jid,
  322. 'id': _converse.connection.getUniqueId(),
  323. 'to': _converse.connection.jid,
  324. 'type': 'groupchat',
  325. 'xmlns': 'jabber:client'
  326. }).c('received', {'xmlns': 'urn:xmpp:carbons:2'})
  327. .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'})
  328. .c('message', {
  329. 'xmlns': 'jabber:client',
  330. 'from': impersonated_jid,
  331. 'to': muc_jid,
  332. 'type': 'groupchat'
  333. }).c('body').t('I am groot').tree();
  334. const view = _converse.api.chatviews.get(muc_jid);
  335. spyOn(converse.env.log, 'error');
  336. await view.model.handleMAMResult({ 'messages': [msg] });
  337. expect(converse.env.log.error).toHaveBeenCalledWith(
  338. 'Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied'
  339. );
  340. expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
  341. expect(view.model.messages.length).toBe(0);
  342. done();
  343. }));
  344. it("keeps track of the sender's role and affiliation",
  345. mock.initConverse(
  346. ['rosterGroupsFetched'], {},
  347. async function (done, _converse) {
  348. const muc_jid = 'lounge@montague.lit';
  349. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  350. const view = _converse.api.chatviews.get(muc_jid);
  351. let msg = $msg({
  352. from: 'lounge@montague.lit/romeo',
  353. id: u.getUniqueId(),
  354. to: 'romeo@montague.lit',
  355. type: 'groupchat'
  356. }).c('body').t('I wrote this message!').tree();
  357. await view.model.handleMessageStanza(msg);
  358. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
  359. expect(view.model.messages.last().occupant.get('affiliation')).toBe('owner');
  360. expect(view.model.messages.last().occupant.get('role')).toBe('moderator');
  361. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  362. expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat chat-msg--with-avatar moderator owner');
  363. let presence = $pres({
  364. to:'romeo@montague.lit/orchard',
  365. from:'lounge@montague.lit/romeo',
  366. id: u.getUniqueId()
  367. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  368. .c('item').attrs({
  369. affiliation: 'member',
  370. jid: 'romeo@montague.lit/orchard',
  371. role: 'participant'
  372. }).up()
  373. .c('status').attrs({code:'110'}).up()
  374. .c('status').attrs({code:'210'}).nodeTree;
  375. _converse.connection._dataRecv(mock.createRequest(presence));
  376. msg = $msg({
  377. from: 'lounge@montague.lit/romeo',
  378. id: u.getUniqueId(),
  379. to: 'romeo@montague.lit',
  380. type: 'groupchat'
  381. }).c('body').t('Another message!').tree();
  382. await view.model.handleMessageStanza(msg);
  383. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  384. expect(view.model.messages.last().occupant.get('affiliation')).toBe('member');
  385. expect(view.model.messages.last().occupant.get('role')).toBe('participant');
  386. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  387. expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat chat-msg--with-avatar participant member');
  388. presence = $pres({
  389. to:'romeo@montague.lit/orchard',
  390. from:'lounge@montague.lit/romeo',
  391. id: u.getUniqueId()
  392. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  393. .c('item').attrs({
  394. affiliation: 'owner',
  395. jid: 'romeo@montague.lit/orchard',
  396. role: 'moderator'
  397. }).up()
  398. .c('status').attrs({code:'110'}).up()
  399. .c('status').attrs({code:'210'}).nodeTree;
  400. _converse.connection._dataRecv(mock.createRequest(presence));
  401. view.model.sendMessage('hello world');
  402. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 3);
  403. const occupant = await u.waitUntil(() => view.model.messages.filter(m => m.get('type') === 'groupchat')[2].occupant);
  404. expect(occupant.get('affiliation')).toBe('owner');
  405. expect(occupant.get('role')).toBe('moderator');
  406. expect(view.el.querySelectorAll('.chat-msg').length).toBe(3);
  407. await u.waitUntil(() => sizzle('.chat-msg', view.el).pop().classList.value.trim() === 'message chat-msg groupchat chat-msg--with-avatar moderator owner');
  408. const add_events = view.model.occupants._events.add.length;
  409. msg = $msg({
  410. from: 'lounge@montague.lit/some1',
  411. id: u.getUniqueId(),
  412. to: 'romeo@montague.lit',
  413. type: 'groupchat'
  414. }).c('body').t('Message from someone not in the MUC right now').tree();
  415. await view.model.handleMessageStanza(msg);
  416. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  417. expect(view.model.messages.last().occupant).toBeUndefined();
  418. // Check that there's a new "add" event handler, for when the occupant appears.
  419. expect(view.model.occupants._events.add.length).toBe(add_events+1);
  420. // Check that the occupant gets added/removed to the message as it
  421. // gets removed or added.
  422. presence = $pres({
  423. to:'romeo@montague.lit/orchard',
  424. from:'lounge@montague.lit/some1',
  425. id: u.getUniqueId()
  426. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  427. .c('item').attrs({jid: 'some1@montague.lit/orchard'});
  428. _converse.connection._dataRecv(mock.createRequest(presence));
  429. await u.waitUntil(() => view.model.messages.last().occupant);
  430. expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now');
  431. expect(view.model.messages.last().occupant.get('nick')).toBe('some1');
  432. // Check that the "add" event handler was removed.
  433. expect(view.model.occupants._events.add.length).toBe(add_events);
  434. presence = $pres({
  435. to:'romeo@montague.lit/orchard',
  436. type: 'unavailable',
  437. from:'lounge@montague.lit/some1',
  438. id: u.getUniqueId()
  439. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  440. .c('item').attrs({jid: 'some1@montague.lit/orchard'});
  441. _converse.connection._dataRecv(mock.createRequest(presence));
  442. await u.waitUntil(() => !view.model.messages.last().occupant);
  443. expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now');
  444. expect(view.model.messages.last().occupant).toBeUndefined();
  445. // Check that there's a new "add" event handler, for when the occupant appears.
  446. expect(view.model.occupants._events.add.length).toBe(add_events+1);
  447. presence = $pres({
  448. to:'romeo@montague.lit/orchard',
  449. from:'lounge@montague.lit/some1',
  450. id: u.getUniqueId()
  451. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  452. .c('item').attrs({jid: 'some1@montague.lit/orchard'});
  453. _converse.connection._dataRecv(mock.createRequest(presence));
  454. await u.waitUntil(() => view.model.messages.last().occupant);
  455. expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now');
  456. expect(view.model.messages.last().occupant.get('nick')).toBe('some1');
  457. // Check that the "add" event handler was removed.
  458. expect(view.model.occupants._events.add.length).toBe(add_events);
  459. done();
  460. }));
  461. it("keeps track whether you are the sender or not",
  462. mock.initConverse(
  463. ['rosterGroupsFetched'], {},
  464. async function (done, _converse) {
  465. const muc_jid = 'lounge@montague.lit';
  466. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  467. const view = _converse.api.chatviews.get(muc_jid);
  468. const msg = $msg({
  469. from: 'lounge@montague.lit/romeo',
  470. id: u.getUniqueId(),
  471. to: 'romeo@montague.lit',
  472. type: 'groupchat'
  473. }).c('body').t('I wrote this message!').tree();
  474. await view.model.handleMessageStanza(msg);
  475. expect(view.model.messages.last().get('sender')).toBe('me');
  476. done();
  477. }));
  478. it("can be replaced with a correction",
  479. mock.initConverse(
  480. ['rosterGroupsFetched'], {},
  481. async function (done, _converse) {
  482. const muc_jid = 'lounge@montague.lit';
  483. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  484. const view = _converse.api.chatviews.get(muc_jid);
  485. const stanza = $pres({
  486. to: 'romeo@montague.lit/_converse.js-29092160',
  487. from: 'coven@chat.shakespeare.lit/newguy'
  488. })
  489. .c('x', {xmlns: Strophe.NS.MUC_USER})
  490. .c('item', {
  491. 'affiliation': 'none',
  492. 'jid': 'newguy@montague.lit/_converse.js-290929789',
  493. 'role': 'participant'
  494. }).tree();
  495. _converse.connection._dataRecv(mock.createRequest(stanza));
  496. const msg_id = u.getUniqueId();
  497. await view.model.handleMessageStanza($msg({
  498. 'from': 'lounge@montague.lit/newguy',
  499. 'to': _converse.connection.jid,
  500. 'type': 'groupchat',
  501. 'id': msg_id,
  502. }).c('body').t('But soft, what light through yonder airlock breaks?').tree());
  503. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
  504. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  505. expect(view.el.querySelector('.chat-msg__text').textContent)
  506. .toBe('But soft, what light through yonder airlock breaks?');
  507. await view.model.handleMessageStanza($msg({
  508. 'from': 'lounge@montague.lit/newguy',
  509. 'to': _converse.connection.jid,
  510. 'type': 'groupchat',
  511. 'id': u.getUniqueId(),
  512. }).c('body').t('But soft, what light through yonder chimney breaks?').up()
  513. .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
  514. await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent ===
  515. 'But soft, what light through yonder chimney breaks?', 500);
  516. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  517. await u.waitUntil(() => view.el.querySelector('.chat-msg__content .fa-edit'));
  518. await view.model.handleMessageStanza($msg({
  519. 'from': 'lounge@montague.lit/newguy',
  520. 'to': _converse.connection.jid,
  521. 'type': 'groupchat',
  522. 'id': u.getUniqueId(),
  523. }).c('body').t('But soft, what light through yonder window breaks?').up()
  524. .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
  525. await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent ===
  526. 'But soft, what light through yonder window breaks?', 500);
  527. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  528. expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
  529. const edit = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .fa-edit'));
  530. edit.click();
  531. const modal = await u.waitUntil(() => view.el.querySelector('converse-chat-message').message_versions_modal);
  532. await u.waitUntil(() => u.isVisible(modal.el), 1000);
  533. const older_msgs = modal.el.querySelectorAll('.older-msg');
  534. expect(older_msgs.length).toBe(2);
  535. expect(older_msgs[0].childNodes[2].textContent).toBe('But soft, what light through yonder airlock breaks?');
  536. expect(older_msgs[0].childNodes[0].nodeName).toBe('TIME');
  537. expect(older_msgs[1].childNodes[0].nodeName).toBe('TIME');
  538. expect(older_msgs[1].childNodes[2].textContent).toBe('But soft, what light through yonder chimney breaks?');
  539. done();
  540. }));
  541. it("can be sent as a correction by using the up arrow",
  542. mock.initConverse(
  543. ['rosterGroupsFetched'], {},
  544. async function (done, _converse) {
  545. const muc_jid = 'lounge@montague.lit';
  546. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  547. const view = _converse.api.chatviews.get(muc_jid);
  548. const textarea = view.el.querySelector('textarea.chat-textarea');
  549. expect(textarea.value).toBe('');
  550. view.onKeyDown({
  551. target: textarea,
  552. keyCode: 38 // Up arrow
  553. });
  554. expect(textarea.value).toBe('');
  555. textarea.value = 'But soft, what light through yonder airlock breaks?';
  556. view.onKeyDown({
  557. target: textarea,
  558. preventDefault: function preventDefault () {},
  559. keyCode: 13 // Enter
  560. });
  561. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  562. expect(view.el.querySelector('.chat-msg__text').textContent)
  563. .toBe('But soft, what light through yonder airlock breaks?');
  564. const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'});
  565. expect(textarea.value).toBe('');
  566. view.onKeyDown({
  567. target: textarea,
  568. keyCode: 38 // Up arrow
  569. });
  570. expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?');
  571. expect(view.model.messages.at(0).get('correcting')).toBe(true);
  572. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  573. await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')));
  574. spyOn(_converse.connection, 'send');
  575. textarea.value = 'But soft, what light through yonder window breaks?';
  576. view.onKeyDown({
  577. target: textarea,
  578. preventDefault: function preventDefault () {},
  579. keyCode: 13 // Enter
  580. });
  581. expect(_converse.connection.send).toHaveBeenCalled();
  582. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  583. const msg = _converse.connection.send.calls.all()[0].args[0];
  584. expect(msg.toLocaleString())
  585. .toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+
  586. `to="lounge@montague.lit" type="groupchat" `+
  587. `xmlns="jabber:client">`+
  588. `<body>But soft, what light through yonder window breaks?</body>`+
  589. `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
  590. `<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>`+
  591. `<origin-id id="${msg.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
  592. `</message>`);
  593. expect(view.model.messages.models.length).toBe(1);
  594. const corrected_message = view.model.messages.at(0);
  595. expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid'));
  596. expect(corrected_message.get('correcting')).toBe(false);
  597. const older_versions = corrected_message.get('older_versions');
  598. const keys = Object.keys(older_versions);
  599. expect(keys.length).toBe(1);
  600. expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?');
  601. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  602. expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false);
  603. // Check that messages from other users are skipped
  604. await view.model.handleMessageStanza($msg({
  605. 'from': muc_jid+'/someone-else',
  606. 'id': u.getUniqueId(),
  607. 'to': 'romeo@montague.lit',
  608. 'type': 'groupchat'
  609. }).c('body').t('Hello world').tree());
  610. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  611. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  612. // Test that pressing the down arrow cancels message correction
  613. expect(textarea.value).toBe('');
  614. view.onKeyDown({
  615. target: textarea,
  616. keyCode: 38 // Up arrow
  617. });
  618. expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
  619. expect(view.model.messages.at(0).get('correcting')).toBe(true);
  620. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  621. await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500);
  622. expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
  623. view.onKeyDown({
  624. target: textarea,
  625. keyCode: 40 // Down arrow
  626. });
  627. expect(textarea.value).toBe('');
  628. expect(view.model.messages.at(0).get('correcting')).toBe(false);
  629. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  630. await u.waitUntil(() => !u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500);
  631. done();
  632. }));
  633. it("will be shown as received upon MUC reflection",
  634. mock.initConverse(
  635. ['rosterGroupsFetched'], {},
  636. async function (done, _converse) {
  637. await mock.waitForRoster(_converse, 'current');
  638. const muc_jid = 'lounge@montague.lit';
  639. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  640. const view = _converse.api.chatviews.get(muc_jid);
  641. const textarea = view.el.querySelector('textarea.chat-textarea');
  642. textarea.value = 'But soft, what light through yonder airlock breaks?';
  643. view.onKeyDown({
  644. target: textarea,
  645. preventDefault: function preventDefault () {},
  646. keyCode: 13 // Enter
  647. });
  648. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  649. expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(0);
  650. const msg_obj = view.model.messages.at(0);
  651. const stanza = u.toStanza(`
  652. <message xmlns="jabber:client"
  653. from="${msg_obj.get('from')}"
  654. to="${_converse.connection.jid}"
  655. type="groupchat">
  656. <body>${msg_obj.get('message')}</body>
  657. <stanza-id xmlns="urn:xmpp:sid:0"
  658. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  659. by="lounge@montague.lit"/>
  660. <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
  661. </message>`);
  662. await view.model.handleMessageStanza(stanza);
  663. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
  664. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  665. expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(1);
  666. expect(view.model.messages.length).toBe(1);
  667. const message = view.model.messages.at(0);
  668. expect(message.get('stanza_id lounge@montague.lit')).toBe('5f3dbc5e-e1d3-4077-a492-693f3769c7ad');
  669. expect(message.get('origin_id')).toBe(msg_obj.get('origin_id'));
  670. done();
  671. }));
  672. it("gets updated with its stanza-id upon MUC reflection",
  673. mock.initConverse(
  674. ['rosterGroupsFetched'], {},
  675. async function (done, _converse) {
  676. const muc_jid = 'room@muc.example.com';
  677. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  678. const view = _converse.api.chatviews.get(muc_jid);
  679. view.model.sendMessage('hello world');
  680. await u.waitUntil(() => view.model.messages.length === 1);
  681. const msg = view.model.messages.at(0);
  682. expect(msg.get('stanza_id')).toBeUndefined();
  683. expect(msg.get('origin_id')).toBe(msg.get('origin_id'));
  684. const stanza = u.toStanza(`
  685. <message xmlns="jabber:client"
  686. from="room@muc.example.com/romeo"
  687. to="${_converse.connection.jid}"
  688. type="groupchat">
  689. <body>Hello world</body>
  690. <stanza-id xmlns="urn:xmpp:sid:0"
  691. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  692. by="room@muc.example.com"/>
  693. <origin-id xmlns="urn:xmpp:sid:0" id="${msg.get('origin_id')}"/>
  694. </message>`);
  695. spyOn(view.model, 'updateMessage').and.callThrough();
  696. _converse.connection._dataRecv(mock.createRequest(stanza));
  697. await u.waitUntil(() => view.model.updateMessage.calls.count() === 1);
  698. expect(view.model.messages.length).toBe(1);
  699. expect(view.model.messages.at(0).get('stanza_id room@muc.example.com')).toBe("5f3dbc5e-e1d3-4077-a492-693f3769c7ad");
  700. expect(view.model.messages.at(0).get('origin_id')).toBe(msg.get('origin_id'));
  701. done();
  702. }));
  703. it("can cause a delivery receipt to be returned",
  704. mock.initConverse(
  705. ['rosterGroupsFetched'], {},
  706. async function (done, _converse) {
  707. await mock.waitForRoster(_converse, 'current');
  708. const muc_jid = 'lounge@montague.lit';
  709. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  710. const view = _converse.api.chatviews.get(muc_jid);
  711. const textarea = view.el.querySelector('textarea.chat-textarea');
  712. textarea.value = 'But soft, what light through yonder airlock breaks?';
  713. view.onKeyDown({
  714. target: textarea,
  715. preventDefault: function preventDefault () {},
  716. keyCode: 13 // Enter
  717. });
  718. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  719. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  720. const msg_obj = view.model.messages.at(0);
  721. const stanza = u.toStanza(`
  722. <message xml:lang="en" to="romeo@montague.lit/orchard"
  723. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  724. <received xmlns="urn:xmpp:receipts" id="${msg_obj.get('msgid')}"/>
  725. <origin-id xmlns="urn:xmpp:sid:0" id="CE08D448-5ED8-4B6A-BB5B-07ED9DFE4FF0"/>
  726. </message>`);
  727. spyOn(stanza_utils, "parseMUCMessage").and.callThrough();
  728. _converse.connection._dataRecv(mock.createRequest(stanza));
  729. await u.waitUntil(() => stanza_utils.parseMUCMessage.calls.count() === 1);
  730. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  731. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  732. done();
  733. }));
  734. it("can cause a chat marker to be returned",
  735. mock.initConverse(
  736. ['rosterGroupsFetched'], {},
  737. async function (done, _converse) {
  738. await mock.waitForRoster(_converse, 'current');
  739. const muc_jid = 'lounge@montague.lit';
  740. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  741. const view = _converse.api.chatviews.get(muc_jid);
  742. const textarea = view.el.querySelector('textarea.chat-textarea');
  743. textarea.value = 'But soft, what light through yonder airlock breaks?';
  744. view.onKeyDown({
  745. target: textarea,
  746. preventDefault: function preventDefault () {},
  747. keyCode: 13 // Enter
  748. });
  749. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  750. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  751. expect(view.el.querySelector('.chat-msg .chat-msg__body').textContent.trim())
  752. .toBe("But soft, what light through yonder airlock breaks?");
  753. const msg_obj = view.model.messages.at(0);
  754. let stanza = u.toStanza(`
  755. <message xml:lang="en" to="romeo@montague.lit/orchard"
  756. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  757. <received xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
  758. </message>`);
  759. const stanza_utils = converse.env.stanza_utils;
  760. spyOn(stanza_utils, "getChatMarker").and.callThrough();
  761. _converse.connection._dataRecv(mock.createRequest(stanza));
  762. await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 1);
  763. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  764. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  765. stanza = u.toStanza(`
  766. <message xml:lang="en" to="romeo@montague.lit/orchard"
  767. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  768. <displayed xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
  769. </message>`);
  770. _converse.connection._dataRecv(mock.createRequest(stanza));
  771. await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 2);
  772. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  773. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  774. stanza = u.toStanza(`
  775. <message xml:lang="en" to="romeo@montague.lit/orchard"
  776. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  777. <acknowledged xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
  778. </message>`);
  779. _converse.connection._dataRecv(mock.createRequest(stanza));
  780. await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 3);
  781. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  782. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  783. stanza = u.toStanza(`
  784. <message xml:lang="en" to="romeo@montague.lit/orchard"
  785. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  786. <body>'tis I!</body>
  787. <markable xmlns="urn:xmpp:chat-markers:0"/>
  788. </message>`);
  789. _converse.connection._dataRecv(mock.createRequest(stanza));
  790. await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 4);
  791. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
  792. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  793. done();
  794. }));
  795. });