2
0

muc_messages.js 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877
  1. /*global mock, converse */
  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': 'owner',
  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. await u.waitUntil(() => converse.env.log.error.calls.count());
  338. expect(converse.env.log.error).toHaveBeenCalledWith(
  339. 'Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied'
  340. );
  341. expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
  342. expect(view.model.messages.length).toBe(0);
  343. done();
  344. }));
  345. it("keeps track of the sender's role and affiliation",
  346. mock.initConverse(
  347. ['rosterGroupsFetched'], {},
  348. async function (done, _converse) {
  349. const muc_jid = 'lounge@montague.lit';
  350. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  351. const view = _converse.api.chatviews.get(muc_jid);
  352. let msg = $msg({
  353. from: 'lounge@montague.lit/romeo',
  354. id: u.getUniqueId(),
  355. to: 'romeo@montague.lit',
  356. type: 'groupchat'
  357. }).c('body').t('I wrote this message!').tree();
  358. await view.model.handleMessageStanza(msg);
  359. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
  360. expect(view.model.messages.last().occupant.get('affiliation')).toBe('owner');
  361. expect(view.model.messages.last().occupant.get('role')).toBe('moderator');
  362. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  363. expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat chat-msg--with-avatar moderator owner');
  364. let presence = $pres({
  365. to:'romeo@montague.lit/orchard',
  366. from:'lounge@montague.lit/romeo',
  367. id: u.getUniqueId()
  368. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  369. .c('item').attrs({
  370. affiliation: 'member',
  371. jid: 'romeo@montague.lit/orchard',
  372. role: 'participant'
  373. }).up()
  374. .c('status').attrs({code:'110'}).up()
  375. .c('status').attrs({code:'210'}).nodeTree;
  376. _converse.connection._dataRecv(mock.createRequest(presence));
  377. msg = $msg({
  378. from: 'lounge@montague.lit/romeo',
  379. id: u.getUniqueId(),
  380. to: 'romeo@montague.lit',
  381. type: 'groupchat'
  382. }).c('body').t('Another message!').tree();
  383. await view.model.handleMessageStanza(msg);
  384. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  385. expect(view.model.messages.last().occupant.get('affiliation')).toBe('member');
  386. expect(view.model.messages.last().occupant.get('role')).toBe('participant');
  387. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  388. expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat chat-msg--with-avatar participant member');
  389. presence = $pres({
  390. to:'romeo@montague.lit/orchard',
  391. from:'lounge@montague.lit/romeo',
  392. id: u.getUniqueId()
  393. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  394. .c('item').attrs({
  395. affiliation: 'owner',
  396. jid: 'romeo@montague.lit/orchard',
  397. role: 'moderator'
  398. }).up()
  399. .c('status').attrs({code:'110'}).up()
  400. .c('status').attrs({code:'210'}).nodeTree;
  401. _converse.connection._dataRecv(mock.createRequest(presence));
  402. view.model.sendMessage('hello world');
  403. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 3);
  404. const occupant = await u.waitUntil(() => view.model.messages.filter(m => m.get('type') === 'groupchat')[2].occupant);
  405. expect(occupant.get('affiliation')).toBe('owner');
  406. expect(occupant.get('role')).toBe('moderator');
  407. expect(view.el.querySelectorAll('.chat-msg').length).toBe(3);
  408. await u.waitUntil(() => sizzle('.chat-msg', view.el).pop().classList.value.trim() === 'message chat-msg groupchat chat-msg--with-avatar moderator owner');
  409. const add_events = view.model.occupants._events.add.length;
  410. msg = $msg({
  411. from: 'lounge@montague.lit/some1',
  412. id: u.getUniqueId(),
  413. to: 'romeo@montague.lit',
  414. type: 'groupchat'
  415. }).c('body').t('Message from someone not in the MUC right now').tree();
  416. await view.model.handleMessageStanza(msg);
  417. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  418. expect(view.model.messages.last().occupant).toBeUndefined();
  419. // Check that there's a new "add" event handler, for when the occupant appears.
  420. expect(view.model.occupants._events.add.length).toBe(add_events+1);
  421. // Check that the occupant gets added/removed to the message as it
  422. // gets removed or added.
  423. presence = $pres({
  424. to:'romeo@montague.lit/orchard',
  425. from:'lounge@montague.lit/some1',
  426. id: u.getUniqueId()
  427. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  428. .c('item').attrs({jid: 'some1@montague.lit/orchard'});
  429. _converse.connection._dataRecv(mock.createRequest(presence));
  430. await u.waitUntil(() => view.model.messages.last().occupant);
  431. expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now');
  432. expect(view.model.messages.last().occupant.get('nick')).toBe('some1');
  433. // Check that the "add" event handler was removed.
  434. expect(view.model.occupants._events.add.length).toBe(add_events);
  435. presence = $pres({
  436. to:'romeo@montague.lit/orchard',
  437. type: 'unavailable',
  438. from:'lounge@montague.lit/some1',
  439. id: u.getUniqueId()
  440. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  441. .c('item').attrs({jid: 'some1@montague.lit/orchard'});
  442. _converse.connection._dataRecv(mock.createRequest(presence));
  443. await u.waitUntil(() => !view.model.messages.last().occupant);
  444. expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now');
  445. expect(view.model.messages.last().occupant).toBeUndefined();
  446. // Check that there's a new "add" event handler, for when the occupant appears.
  447. expect(view.model.occupants._events.add.length).toBe(add_events+1);
  448. presence = $pres({
  449. to:'romeo@montague.lit/orchard',
  450. from:'lounge@montague.lit/some1',
  451. id: u.getUniqueId()
  452. }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
  453. .c('item').attrs({jid: 'some1@montague.lit/orchard'});
  454. _converse.connection._dataRecv(mock.createRequest(presence));
  455. await u.waitUntil(() => view.model.messages.last().occupant);
  456. expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now');
  457. expect(view.model.messages.last().occupant.get('nick')).toBe('some1');
  458. // Check that the "add" event handler was removed.
  459. expect(view.model.occupants._events.add.length).toBe(add_events);
  460. done();
  461. }));
  462. it("keeps track whether you are the sender or not",
  463. mock.initConverse(
  464. ['rosterGroupsFetched'], {},
  465. async function (done, _converse) {
  466. const muc_jid = 'lounge@montague.lit';
  467. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  468. const view = _converse.api.chatviews.get(muc_jid);
  469. const msg = $msg({
  470. from: 'lounge@montague.lit/romeo',
  471. id: u.getUniqueId(),
  472. to: 'romeo@montague.lit',
  473. type: 'groupchat'
  474. }).c('body').t('I wrote this message!').tree();
  475. await view.model.handleMessageStanza(msg);
  476. expect(view.model.messages.last().get('sender')).toBe('me');
  477. done();
  478. }));
  479. it("can be replaced with a correction",
  480. mock.initConverse(
  481. ['rosterGroupsFetched'], {},
  482. async function (done, _converse) {
  483. const muc_jid = 'lounge@montague.lit';
  484. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  485. const view = _converse.api.chatviews.get(muc_jid);
  486. const stanza = $pres({
  487. to: 'romeo@montague.lit/_converse.js-29092160',
  488. from: 'coven@chat.shakespeare.lit/newguy'
  489. })
  490. .c('x', {xmlns: Strophe.NS.MUC_USER})
  491. .c('item', {
  492. 'affiliation': 'none',
  493. 'jid': 'newguy@montague.lit/_converse.js-290929789',
  494. 'role': 'participant'
  495. }).tree();
  496. _converse.connection._dataRecv(mock.createRequest(stanza));
  497. const msg_id = u.getUniqueId();
  498. await view.model.handleMessageStanza($msg({
  499. 'from': 'lounge@montague.lit/newguy',
  500. 'to': _converse.connection.jid,
  501. 'type': 'groupchat',
  502. 'id': msg_id,
  503. }).c('body').t('But soft, what light through yonder airlock breaks?').tree());
  504. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
  505. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  506. expect(view.el.querySelector('.chat-msg__text').textContent)
  507. .toBe('But soft, what light through yonder airlock breaks?');
  508. await view.model.handleMessageStanza($msg({
  509. 'from': 'lounge@montague.lit/newguy',
  510. 'to': _converse.connection.jid,
  511. 'type': 'groupchat',
  512. 'id': u.getUniqueId(),
  513. }).c('body').t('But soft, what light through yonder chimney breaks?').up()
  514. .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
  515. await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent ===
  516. 'But soft, what light through yonder chimney breaks?', 500);
  517. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  518. await u.waitUntil(() => view.el.querySelector('.chat-msg__content .fa-edit'));
  519. await view.model.handleMessageStanza($msg({
  520. 'from': 'lounge@montague.lit/newguy',
  521. 'to': _converse.connection.jid,
  522. 'type': 'groupchat',
  523. 'id': u.getUniqueId(),
  524. }).c('body').t('But soft, what light through yonder window breaks?').up()
  525. .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree());
  526. await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent ===
  527. 'But soft, what light through yonder window breaks?', 500);
  528. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  529. expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
  530. const edit = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .fa-edit'));
  531. edit.click();
  532. const modal = await u.waitUntil(() => view.el.querySelector('converse-chat-message').message_versions_modal);
  533. await u.waitUntil(() => u.isVisible(modal.el), 1000);
  534. const older_msgs = modal.el.querySelectorAll('.older-msg');
  535. expect(older_msgs.length).toBe(2);
  536. expect(older_msgs[0].childNodes[2].textContent).toBe('But soft, what light through yonder airlock breaks?');
  537. expect(older_msgs[0].childNodes[0].nodeName).toBe('TIME');
  538. expect(older_msgs[1].childNodes[0].nodeName).toBe('TIME');
  539. expect(older_msgs[1].childNodes[2].textContent).toBe('But soft, what light through yonder chimney breaks?');
  540. done();
  541. }));
  542. it("can be sent as a correction by using the up arrow",
  543. mock.initConverse(
  544. ['rosterGroupsFetched'], {},
  545. async function (done, _converse) {
  546. const muc_jid = 'lounge@montague.lit';
  547. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  548. const view = _converse.api.chatviews.get(muc_jid);
  549. const textarea = view.el.querySelector('textarea.chat-textarea');
  550. expect(textarea.value).toBe('');
  551. view.onKeyDown({
  552. target: textarea,
  553. keyCode: 38 // Up arrow
  554. });
  555. expect(textarea.value).toBe('');
  556. textarea.value = 'But soft, what light through yonder airlock breaks?';
  557. view.onKeyDown({
  558. target: textarea,
  559. preventDefault: function preventDefault () {},
  560. keyCode: 13 // Enter
  561. });
  562. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
  563. expect(view.el.querySelector('.chat-msg__text').textContent)
  564. .toBe('But soft, what light through yonder airlock breaks?');
  565. const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'});
  566. expect(textarea.value).toBe('');
  567. view.onKeyDown({
  568. target: textarea,
  569. keyCode: 38 // Up arrow
  570. });
  571. expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?');
  572. expect(view.model.messages.at(0).get('correcting')).toBe(true);
  573. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  574. await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')));
  575. spyOn(_converse.connection, 'send');
  576. textarea.value = 'But soft, what light through yonder window breaks?';
  577. view.onKeyDown({
  578. target: textarea,
  579. preventDefault: function preventDefault () {},
  580. keyCode: 13 // Enter
  581. });
  582. expect(_converse.connection.send).toHaveBeenCalled();
  583. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  584. const msg = _converse.connection.send.calls.all()[0].args[0];
  585. expect(msg.toLocaleString())
  586. .toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+
  587. `to="lounge@montague.lit" type="groupchat" `+
  588. `xmlns="jabber:client">`+
  589. `<body>But soft, what light through yonder window breaks?</body>`+
  590. `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
  591. `<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>`+
  592. `<origin-id id="${msg.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
  593. `</message>`);
  594. expect(view.model.messages.models.length).toBe(1);
  595. const corrected_message = view.model.messages.at(0);
  596. expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid'));
  597. expect(corrected_message.get('correcting')).toBe(false);
  598. const older_versions = corrected_message.get('older_versions');
  599. const keys = Object.keys(older_versions);
  600. expect(keys.length).toBe(1);
  601. expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?');
  602. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  603. expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false);
  604. // Check that messages from other users are skipped
  605. await view.model.handleMessageStanza($msg({
  606. 'from': muc_jid+'/someone-else',
  607. 'id': u.getUniqueId(),
  608. 'to': 'romeo@montague.lit',
  609. 'type': 'groupchat'
  610. }).c('body').t('Hello world').tree());
  611. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  612. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  613. // Test that pressing the down arrow cancels message correction
  614. expect(textarea.value).toBe('');
  615. view.onKeyDown({
  616. target: textarea,
  617. keyCode: 38 // Up arrow
  618. });
  619. expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
  620. expect(view.model.messages.at(0).get('correcting')).toBe(true);
  621. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  622. await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500);
  623. expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
  624. view.onKeyDown({
  625. target: textarea,
  626. keyCode: 40 // Down arrow
  627. });
  628. expect(textarea.value).toBe('');
  629. expect(view.model.messages.at(0).get('correcting')).toBe(false);
  630. expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
  631. await u.waitUntil(() => !u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500);
  632. done();
  633. }));
  634. it("will be shown as received upon MUC reflection",
  635. mock.initConverse(
  636. ['rosterGroupsFetched'], {},
  637. async function (done, _converse) {
  638. await mock.waitForRoster(_converse, 'current');
  639. const muc_jid = 'lounge@montague.lit';
  640. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  641. const view = _converse.api.chatviews.get(muc_jid);
  642. const textarea = view.el.querySelector('textarea.chat-textarea');
  643. textarea.value = 'But soft, what light through yonder airlock breaks?';
  644. view.onKeyDown({
  645. target: textarea,
  646. preventDefault: function preventDefault () {},
  647. keyCode: 13 // Enter
  648. });
  649. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  650. expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(0);
  651. const msg_obj = view.model.messages.at(0);
  652. const stanza = u.toStanza(`
  653. <message xmlns="jabber:client"
  654. from="${msg_obj.get('from')}"
  655. to="${_converse.connection.jid}"
  656. type="groupchat">
  657. <body>${msg_obj.get('message')}</body>
  658. <stanza-id xmlns="urn:xmpp:sid:0"
  659. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  660. by="lounge@montague.lit"/>
  661. <origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
  662. </message>`);
  663. await view.model.handleMessageStanza(stanza);
  664. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
  665. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  666. expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(1);
  667. expect(view.model.messages.length).toBe(1);
  668. const message = view.model.messages.at(0);
  669. expect(message.get('stanza_id lounge@montague.lit')).toBe('5f3dbc5e-e1d3-4077-a492-693f3769c7ad');
  670. expect(message.get('origin_id')).toBe(msg_obj.get('origin_id'));
  671. done();
  672. }));
  673. it("gets updated with its stanza-id upon MUC reflection",
  674. mock.initConverse(
  675. ['rosterGroupsFetched'], {},
  676. async function (done, _converse) {
  677. const muc_jid = 'room@muc.example.com';
  678. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  679. const view = _converse.api.chatviews.get(muc_jid);
  680. view.model.sendMessage('hello world');
  681. await u.waitUntil(() => view.model.messages.length === 1);
  682. const msg = view.model.messages.at(0);
  683. expect(msg.get('stanza_id')).toBeUndefined();
  684. expect(msg.get('origin_id')).toBe(msg.get('origin_id'));
  685. const stanza = u.toStanza(`
  686. <message xmlns="jabber:client"
  687. from="room@muc.example.com/romeo"
  688. to="${_converse.connection.jid}"
  689. type="groupchat">
  690. <body>Hello world</body>
  691. <stanza-id xmlns="urn:xmpp:sid:0"
  692. id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
  693. by="room@muc.example.com"/>
  694. <origin-id xmlns="urn:xmpp:sid:0" id="${msg.get('origin_id')}"/>
  695. </message>`);
  696. spyOn(view.model, 'updateMessage').and.callThrough();
  697. _converse.connection._dataRecv(mock.createRequest(stanza));
  698. await u.waitUntil(() => view.model.updateMessage.calls.count() === 1);
  699. expect(view.model.messages.length).toBe(1);
  700. expect(view.model.messages.at(0).get('stanza_id room@muc.example.com')).toBe("5f3dbc5e-e1d3-4077-a492-693f3769c7ad");
  701. expect(view.model.messages.at(0).get('origin_id')).toBe(msg.get('origin_id'));
  702. done();
  703. }));
  704. it("can cause a delivery receipt to be returned",
  705. mock.initConverse(
  706. ['rosterGroupsFetched'], {},
  707. async function (done, _converse) {
  708. await mock.waitForRoster(_converse, 'current');
  709. const muc_jid = 'lounge@montague.lit';
  710. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  711. const view = _converse.api.chatviews.get(muc_jid);
  712. const textarea = view.el.querySelector('textarea.chat-textarea');
  713. textarea.value = 'But soft, what light through yonder airlock breaks?';
  714. view.onKeyDown({
  715. target: textarea,
  716. preventDefault: function preventDefault () {},
  717. keyCode: 13 // Enter
  718. });
  719. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  720. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  721. const msg_obj = view.model.messages.at(0);
  722. const stanza = u.toStanza(`
  723. <message xml:lang="en" to="romeo@montague.lit/orchard"
  724. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  725. <received xmlns="urn:xmpp:receipts" id="${msg_obj.get('msgid')}"/>
  726. <origin-id xmlns="urn:xmpp:sid:0" id="CE08D448-5ED8-4B6A-BB5B-07ED9DFE4FF0"/>
  727. </message>`);
  728. spyOn(stanza_utils, "parseMUCMessage").and.callThrough();
  729. _converse.connection._dataRecv(mock.createRequest(stanza));
  730. await u.waitUntil(() => stanza_utils.parseMUCMessage.calls.count() === 1);
  731. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  732. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  733. done();
  734. }));
  735. it("can cause a chat marker to be returned",
  736. mock.initConverse(
  737. ['rosterGroupsFetched'], {},
  738. async function (done, _converse) {
  739. await mock.waitForRoster(_converse, 'current');
  740. const muc_jid = 'lounge@montague.lit';
  741. await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
  742. const view = _converse.api.chatviews.get(muc_jid);
  743. const textarea = view.el.querySelector('textarea.chat-textarea');
  744. textarea.value = 'But soft, what light through yonder airlock breaks?';
  745. view.onKeyDown({
  746. target: textarea,
  747. preventDefault: function preventDefault () {},
  748. keyCode: 13 // Enter
  749. });
  750. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  751. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  752. expect(view.el.querySelector('.chat-msg .chat-msg__body').textContent.trim())
  753. .toBe("But soft, what light through yonder airlock breaks?");
  754. const msg_obj = view.model.messages.at(0);
  755. let stanza = u.toStanza(`
  756. <message xml:lang="en" to="romeo@montague.lit/orchard"
  757. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  758. <received xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
  759. </message>`);
  760. const stanza_utils = converse.env.stanza_utils;
  761. spyOn(stanza_utils, "getChatMarker").and.callThrough();
  762. _converse.connection._dataRecv(mock.createRequest(stanza));
  763. await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 1);
  764. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  765. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  766. stanza = u.toStanza(`
  767. <message xml:lang="en" to="romeo@montague.lit/orchard"
  768. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  769. <displayed xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
  770. </message>`);
  771. _converse.connection._dataRecv(mock.createRequest(stanza));
  772. await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 2);
  773. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  774. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  775. stanza = u.toStanza(`
  776. <message xml:lang="en" to="romeo@montague.lit/orchard"
  777. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  778. <acknowledged xmlns="urn:xmpp:chat-markers:0" id="${msg_obj.get('msgid')}"/>
  779. </message>`);
  780. _converse.connection._dataRecv(mock.createRequest(stanza));
  781. await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 3);
  782. expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
  783. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  784. stanza = u.toStanza(`
  785. <message xml:lang="en" to="romeo@montague.lit/orchard"
  786. from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
  787. <body>'tis I!</body>
  788. <markable xmlns="urn:xmpp:chat-markers:0"/>
  789. </message>`);
  790. _converse.connection._dataRecv(mock.createRequest(stanza));
  791. await u.waitUntil(() => stanza_utils.getChatMarker.calls.count() === 4);
  792. await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
  793. expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0);
  794. done();
  795. }));
  796. });