/*global mock, converse */ const Model = converse.env.Model; const Strophe = converse.env.Strophe; const $iq = converse.env.$iq; const $msg = converse.env.$msg; const dayjs = converse.env.dayjs; const u = converse.env.utils; const sizzle = converse.env.sizzle; const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; // See: https://xmpp.org/rfcs/rfc3921.html // Implements the protocol defined in https://xmpp.org/extensions/xep-0313.html#config describe("Message Archive Management", function () { beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000)); afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout)); describe("The XEP-0313 Archive", function () { it("is queried when the user scrolls up", mock.initConverse(['discoInitialized'], {'archived_messages_page_size': 2}, async function (done, _converse) { await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid); const view = _converse.chatboxviews.get(contact_jid); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); const sent_IQs = _converse.connection.IQ_stanzas; let stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop()); const queryid = stanza.querySelector('query').getAttribute('queryid'); let msg = $msg({'id': _converse.connection.getUniqueId(), 'to': _converse.bare_jid}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to': _converse.bare_jid, 'id': _converse.connection.getUniqueId(), 'from': contact_jid, 'type':'chat' }).c('body').t("Meet me at the dance"); _converse.connection._dataRecv(mock.createRequest(msg)); msg = $msg({'id': _converse.connection.getUniqueId(), 'to': _converse.bare_jid}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to': _converse.bare_jid, 'id': _converse.connection.getUniqueId(), 'from': contact_jid, 'type':'chat' }).c('body').t("Thrice the brinded cat hath mew'd."); _converse.connection._dataRecv(mock.createRequest(msg)); const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')}) .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('first', {'index': '0'}).t('23452-4534-1').up() .c('last').t('09af3-cc343-b409f').up() .c('count').t('16'); _converse.connection._dataRecv(mock.createRequest(iq_result)); await new Promise(resolve => view.model.messages.once('rendered', resolve)); expect(view.model.messages.length).toBe(2); while (sent_IQs.length) { sent_IQs.pop(); } _converse.api.trigger('chatBoxScrolledUp', view); stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop()); expect(Strophe.serialize(stanza)).toBe( ``+ ``+ ``+ `urn:xmpp:mam:2mercutio@montague.lit`+ ``+ `${view.model.messages.at(0).get('stanza_id romeo@montague.lit')}2`+ `` ); done(); })); it("is queried when the user enters a new MUC", mock.initConverse(['discoInitialized'], { 'archived_messages_page_size': 2, 'muc_clear_messages_on_leave': false, }, async function (done, _converse) { const sent_IQs = _converse.connection.IQ_stanzas; const muc_jid = 'orchard@chat.shakespeare.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); let view = _converse.chatboxviews.get(muc_jid); let iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop()); expect(Strophe.serialize(iq_get)).toBe( ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ `2`+ ``+ ``); let first_msg_id = _converse.connection.getUniqueId(); let last_msg_id = _converse.connection.getUniqueId(); let message = u.toStanza( ` 2nd Message `); _converse.connection._dataRecv(mock.createRequest(message)); message = u.toStanza( ` 3rd Message `); _converse.connection._dataRecv(mock.createRequest(message)); // Clear so that we don't match the older query while (sent_IQs.length) { sent_IQs.pop(); } // XXX: Even though the count is 3, when fetching messages for // the first time, we don't paginate, so that message // is not fetched. The user needs to manually load older // messages for it to be fetched. // TODO: we need to add a clickable link to load older messages let result = u.toStanza( ` ${first_msg_id} ${last_msg_id} 3 `); _converse.connection._dataRecv(mock.createRequest(result)); await u.waitUntil(() => view.model.messages.length === 2); view.close(); // Clear so that we don't match the older query while (sent_IQs.length) { sent_IQs.pop(); } await u.waitUntil(() => _converse.chatboxes.length === 1); await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); view = _converse.chatboxviews.get(muc_jid); await u.waitUntil(() => view.model.messages.length); iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop()); expect(Strophe.serialize(iq_get)).toBe( ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ `${message.querySelector('result').getAttribute('id')}2`+ ``+ ``); first_msg_id = _converse.connection.getUniqueId(); last_msg_id = _converse.connection.getUniqueId(); message = u.toStanza( ` 4th Message `); _converse.connection._dataRecv(mock.createRequest(message)); message = u.toStanza( ` 5th Message `); _converse.connection._dataRecv(mock.createRequest(message)); // Clear so that we don't match the older query while (sent_IQs.length) { sent_IQs.pop(); } result = u.toStanza( ` ${first_msg_id} ${last_msg_id} 5 `); _converse.connection._dataRecv(mock.createRequest(result)); await u.waitUntil(() => view.model.messages.length === 4); iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop()); expect(Strophe.serialize(iq_get)).toBe( ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ `${last_msg_id}`+ `2`+ ``+ ``+ ``); const msg_id = _converse.connection.getUniqueId(); message = u.toStanza( ` 6th Message `); _converse.connection._dataRecv(mock.createRequest(message)); result = u.toStanza( ` ${msg_id} ${msg_id} 6 `); _converse.connection._dataRecv(mock.createRequest(result)); await u.waitUntil(() => view.model.messages.length === 5); await u.waitUntil(() => view.content.querySelectorAll('.chat-msg__text').length); await u.waitUntil(() => Array.from(view.content.querySelectorAll('.chat-msg__text')) .map(e => e.textContent).join(' ') === "2nd Message 3rd Message 4th Message 5th Message 6th Message", 1000); done(); })); it("queries for messages since the most recent cached message in a newly entered MUC", mock.initConverse(['discoInitialized'], { 'archived_messages_page_size': 2, 'muc_nickname_from_jid': false, 'muc_clear_messages_on_leave': false, }, async function (done, _converse) { const { api } = _converse; const sent_IQs = _converse.connection.IQ_stanzas; const muc_jid = 'orchard@chat.shakespeare.lit'; const nick = 'romeo'; const room_creation_promise = api.rooms.open(muc_jid); await mock.getRoomFeatures(_converse, muc_jid); await mock.waitForReservedNick(_converse, muc_jid, nick); await mock.receiveOwnMUCPresence(_converse, muc_jid, nick); await room_creation_promise; const view = _converse.chatboxviews.get(muc_jid); await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED)); // Create "cached" message to test that only messages newer than the // last cached message with body text will be fetched view.model.messages.create({ 'type': 'groupchat', 'to': muc_jid, 'from': `${_converse.bare_jid}/orchard`, 'body': 'Hello world', 'message': 'Hello world', 'time': '2021-02-02T12:00:00Z' }); // Hack: Manually set attributes that would otherwise happen in fetchMessages view.model.messages.fetched_flag = true; view.model.afterMessagesFetched(view.model.messages); view.model.messages.fetched.resolve(); const affs = _converse.muc_fetch_members; const all_affiliations = Array.isArray(affs) ? affs : (affs ? ['member', 'admin', 'owner'] : []); await mock.returnMemberLists(_converse, muc_jid, [], all_affiliations); const iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop()); expect(Strophe.serialize(iq_get)).toBe( ``+ ``+ ``+ `urn:xmpp:mam:2`+ `2021-02-02T12:00:00.000Z`+ ``+ `2`+ ``+ ``); return done(); })); }); describe("An archived message", function () { describe("when received", function () { it("is discarded if it doesn't come from the right sender", mock.initConverse( ['discoInitialized'], {}, async function (done, _converse) { await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid); const view = _converse.chatboxviews.get(contact_jid); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); const sent_IQs = _converse.connection.IQ_stanzas; const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop()); const queryid = stanza.querySelector('query').getAttribute('queryid'); let msg = $msg({'id': _converse.connection.getUniqueId(), 'from': 'impersonator@capulet.lit', 'to': _converse.bare_jid}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to': _converse.bare_jid, 'id': _converse.connection.getUniqueId(), 'from': contact_jid, 'type':'chat' }).c('body').t("Meet me at the dance"); spyOn(converse.env.log, 'warn'); _converse.connection._dataRecv(mock.createRequest(msg)); expect(converse.env.log.warn).toHaveBeenCalledWith(`Ignoring alleged MAM message from ${msg.nodeTree.getAttribute('from')}`); msg = $msg({'id': _converse.connection.getUniqueId(), 'to': _converse.bare_jid}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to': _converse.bare_jid, 'id': _converse.connection.getUniqueId(), 'from': contact_jid, 'type':'chat' }).c('body').t("Thrice the brinded cat hath mew'd."); _converse.connection._dataRecv(mock.createRequest(msg)); const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')}) .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('first', {'index': '0'}).t('23452-4534-1').up() .c('last').t('09af3-cc343-b409f').up() .c('count').t('16'); _converse.connection._dataRecv(mock.createRequest(iq_result)); await new Promise(resolve => view.model.messages.once('rendered', resolve)); expect(view.model.messages.length).toBe(1); expect(view.model.messages.at(0).get('message')).toBe("Thrice the brinded cat hath mew'd."); done(); })); it("is not discarded if it comes from the right sender", mock.initConverse( ['discoInitialized'], {}, async function (done, _converse) { await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid); const view = _converse.chatboxviews.get(contact_jid); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); const sent_IQs = _converse.connection.IQ_stanzas; const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop()); const queryid = stanza.querySelector('query').getAttribute('queryid'); let msg = $msg({'id': _converse.connection.getUniqueId(), 'from': _converse.bare_jid, 'to': _converse.bare_jid}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to': _converse.bare_jid, 'id': _converse.connection.getUniqueId(), 'from': contact_jid, 'type':'chat' }).c('body').t("Meet me at the dance"); spyOn(converse.env.log, 'warn'); _converse.connection._dataRecv(mock.createRequest(msg)); msg = $msg({'id': _converse.connection.getUniqueId(), 'to': _converse.bare_jid}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to': _converse.bare_jid, 'id': _converse.connection.getUniqueId(), 'from': contact_jid, 'type':'chat' }).c('body').t("Thrice the brinded cat hath mew'd."); _converse.connection._dataRecv(mock.createRequest(msg)); const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')}) .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('first', {'index': '0'}).t('23452-4534-1').up() .c('last').t('09af3-cc343-b409f').up() .c('count').t('16'); _converse.connection._dataRecv(mock.createRequest(iq_result)); await new Promise(resolve => view.model.messages.once('rendered', resolve)); expect(view.model.messages.length).toBe(2); expect(view.model.messages.at(0).get('message')).toBe("Meet me at the dance"); expect(view.model.messages.at(1).get('message')).toBe("Thrice the brinded cat hath mew'd."); done(); })); it("updates the is_archived value of an already cached version", mock.initConverse( ['discoInitialized'], {}, async function (done, _converse) { await mock.openAndEnterChatRoom(_converse, 'trek-radio@conference.lightwitch.org', 'romeo'); const view = _converse.chatboxviews.get('trek-radio@conference.lightwitch.org'); let stanza = u.toStanza( ` Hello `); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length); expect(view.model.messages.length).toBe(1); expect(view.model.messages.at(0).get('is_archived')).toBe(false); expect(view.model.messages.at(0).get('stanza_id trek-radio@conference.lightwitch.org')).toBe('45fbbf2a-1059-479d-9283-c8effaf05621'); stanza = u.toStanza( ` Hello `); spyOn(view.model, 'getDuplicateMessage').and.callThrough(); spyOn(view.model, 'updateMessage').and.callThrough(); _converse.handleMAMResult(view.model, { 'messages': [stanza] }); await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); expect(view.model.getDuplicateMessage.calls.count()).toBe(1); const result = view.model.getDuplicateMessage.calls.all()[0].returnValue expect(result instanceof _converse.Message).toBe(true); expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); await u.waitUntil(() => view.model.updateMessage.calls.count()); expect(view.model.messages.length).toBe(1); expect(view.model.messages.at(0).get('is_archived')).toBe(true); expect(view.model.messages.at(0).get('stanza_id trek-radio@conference.lightwitch.org')).toBe('45fbbf2a-1059-479d-9283-c8effaf05621'); done(); })); it("isn't shown as duplicate by comparing its stanza id or archive id", mock.initConverse( ['discoInitialized'], {}, async function (done, _converse) { await mock.openAndEnterChatRoom(_converse, 'trek-radio@conference.lightwitch.org', 'jcbrand'); const view = _converse.chatboxviews.get('trek-radio@conference.lightwitch.org'); let stanza = u.toStanza( ` negan `); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length); // Not sure whether such a race-condition might pose a problem // in "real-world" situations. stanza = u.toStanza( ` negan `); spyOn(view.model, 'getDuplicateMessage').and.callThrough(); _converse.handleMAMResult(view.model, { 'messages': [stanza] }); await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); expect(view.model.getDuplicateMessage.calls.count()).toBe(1); const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue expect(result instanceof _converse.Message).toBe(true); expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); done(); })); it("isn't shown as duplicate by comparing only the archive id", mock.initConverse( ['discoInitialized'], {}, async function (done, _converse) { await mock.openAndEnterChatRoom(_converse, 'discuss@conference.conversejs.org', 'romeo'); const view = _converse.chatboxviews.get('discuss@conference.conversejs.org'); let stanza = u.toStanza( ` looks like omemo fails completely with "bundle is undefined" when there is a device in the devicelist that has no keys published `); _converse.handleMAMResult(view.model, { 'messages': [stanza] }); await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length); expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); stanza = u.toStanza( ` looks like omemo fails completely with "bundle is undefined" when there is a device in the devicelist that has no keys published `); spyOn(view.model, 'getDuplicateMessage').and.callThrough(); _converse.handleMAMResult(view.model, { 'messages': [stanza] }); await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); expect(view.model.getDuplicateMessage.calls.count()).toBe(1); const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue expect(result instanceof _converse.Message).toBe(true); expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); done(); })) }); }); describe("The archive.query API", function () { it("can be used to query for all archived messages", mock.initConverse(['discoInitialized'], {}, async function (done, _converse) { const sendIQ = _converse.connection.sendIQ; await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); let sent_stanza, IQ_id; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); _converse.api.archive.query(); await u.waitUntil(() => sent_stanza); const queryid = sent_stanza.querySelector('query').getAttribute('queryid'); expect(Strophe.serialize(sent_stanza)).toBe( ``); done(); })); it("can be used to query for all messages to/from a particular JID", mock.initConverse([], {}, async function (done, _converse) { await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); _converse.api.archive.query({'with':'juliet@capulet.lit'}); await u.waitUntil(() => sent_stanza); const queryid = sent_stanza.querySelector('query').getAttribute('queryid'); expect(Strophe.serialize(sent_stanza)).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ `juliet@capulet.lit`+ ``+ ``+ ``+ ``); done(); })); it("can be used to query for archived messages from a chat room", mock.initConverse(['statusInitialized'], {}, async function (done, _converse) { const room_jid = 'coven@chat.shakespeare.lit'; _converse.api.archive.query({'with': room_jid, 'groupchat': true}); await mock.waitUntilDiscoConfirmed(_converse, room_jid, null, [Strophe.NS.MAM]); const sent_stanzas = _converse.connection.sent_stanzas; const stanza = await u.waitUntil( () => sent_stanzas.filter(s => sizzle(`[xmlns="${Strophe.NS.MAM}"]`, s).length).pop()); const queryid = stanza.querySelector('query').getAttribute('queryid'); expect(Strophe.serialize(stanza)).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ ``+ ``); done(); })); it("checks whether returned MAM messages from a MUC room are from the right JID", mock.initConverse(['statusInitialized'], {}, async function (done, _converse) { const room_jid = 'coven@chat.shakespeare.lit'; const promise = _converse.api.archive.query({'with': room_jid, 'groupchat': true, 'max':'10'}); await mock.waitUntilDiscoConfirmed(_converse, room_jid, null, [Strophe.NS.MAM]); const sent_stanzas = _converse.connection.sent_stanzas; const sent_stanza = await u.waitUntil( () => sent_stanzas.filter(s => sizzle(`[xmlns="${Strophe.NS.MAM}"]`, s).length).pop()); const queryid = sent_stanza.querySelector('query').getAttribute('queryid'); /* * * * * * Thrice the brinded cat hath mew'd. * * * * * * * */ const msg1 = $msg({'id':'iasd207', 'from': 'other@chat.shakespear.lit', 'to': 'romeo@montague.lit'}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'34482-21985-73620'}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to':'romeo@montague.lit', 'id':'162BEBB1-F6DB-4D9A-9BD8-CFDCC801A0B2', 'from':'coven@chat.shakespeare.lit/firstwitch', 'type':'groupchat' }) .c('body').t("Thrice the brinded cat hath mew'd."); _converse.connection._dataRecv(mock.createRequest(msg1)); /* Send an stanza to indicate the end of the result set. * * * * * 28482-98726-73623 * 09af3-cc343-b409f * 20 * * */ const stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')}) .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('first', {'index': '0'}).t('23452-4534-1').up() .c('last').t('09af3-cc343-b409f').up() .c('count').t('16'); _converse.connection._dataRecv(mock.createRequest(stanza)); const result = await promise; expect(result.messages.length).toBe(0); done(); })); it("can be used to query for all messages in a certain timespan", mock.initConverse([], {}, async function (done, _converse) { await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); const start = '2010-06-07T00:00:00Z'; const end = '2010-07-07T13:23:54Z'; _converse.api.archive.query({ 'start': start, 'end': end }); await u.waitUntil(() => sent_stanza); const queryid = sent_stanza.querySelector('query').getAttribute('queryid'); expect(Strophe.serialize(sent_stanza)).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ `${dayjs(start).toISOString()}`+ ``+ ``+ `${dayjs(end).toISOString()}`+ ``+ ``+ ``+ `` ); done(); })); it("throws a TypeError if an invalid date is provided", mock.initConverse([], {}, async function (done, _converse) { await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); try { await _converse.api.archive.query({'start': 'not a real date'}); } catch (e) { expect(() => {throw e}).toThrow(new TypeError('archive.query: invalid date provided for: start')); } done(); })); it("can be used to query for all messages after a certain time", mock.initConverse([], {}, async function (done, _converse) { await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) { _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); } const start = '2010-06-07T00:00:00Z'; _converse.api.archive.query({'start': start}); await u.waitUntil(() => sent_stanza); const queryid = sent_stanza.querySelector('query').getAttribute('queryid'); expect(Strophe.serialize(sent_stanza)).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ `${dayjs(start).toISOString()}`+ ``+ ``+ ``+ `` ); done(); })); it("can be used to query for a limited set of results", mock.initConverse([], {}, async function (done, _converse) { await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); const start = '2010-06-07T00:00:00Z'; _converse.api.archive.query({'start': start, 'max':10}); await u.waitUntil(() => sent_stanza); const queryid = sent_stanza.querySelector('query').getAttribute('queryid'); expect(Strophe.serialize(sent_stanza)).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ `${dayjs(start).toISOString()}`+ ``+ ``+ ``+ `10`+ ``+ ``+ `` ); done(); })); it("can be used to page through results", mock.initConverse([], {}, async function (done, _converse) { await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); const start = '2010-06-07T00:00:00Z'; _converse.api.archive.query({ 'start': start, 'after': '09af3-cc343-b409f', 'max':10 }); await u.waitUntil(() => sent_stanza); const queryid = sent_stanza.querySelector('query').getAttribute('queryid'); expect(Strophe.serialize(sent_stanza)).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ `${dayjs(start).toISOString()}`+ ``+ ``+ ``+ `09af3-cc343-b409f`+ `10`+ ``+ ``+ ``); done(); })); it("accepts \"before\" with an empty string as value to reverse the order", mock.initConverse([], {}, async function (done, _converse) { await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); _converse.api.archive.query({'before': '', 'max':10}); await u.waitUntil(() => sent_stanza); const queryid = sent_stanza.querySelector('query').getAttribute('queryid'); expect(Strophe.serialize(sent_stanza)).toBe( ``+ ``+ ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ ``+ ``+ `10`+ ``+ ``+ ``); done(); })); it("returns an object which includes the messages and a _converse.RSM object", mock.initConverse([], {}, async function (done, _converse) { await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); const promise = _converse.api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'}); await u.waitUntil(() => sent_stanza); const queryid = sent_stanza.querySelector('query').getAttribute('queryid'); /* * * * * * Call me but love, and I'll be new baptized; Henceforth I never will be Romeo. * * * * */ const msg1 = $msg({'id':'aeb212', 'to':'juliet@capulet.lit/chamber'}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73623'}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to':'juliet@capulet.lit/balcony', 'from':'romeo@montague.lit/orchard', 'type':'chat' }) .c('body').t("Call me but love, and I'll be new baptized;"); _converse.connection._dataRecv(mock.createRequest(msg1)); const msg2 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73624'}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to':'juliet@capulet.lit/balcony', 'from':'romeo@montague.lit/orchard', 'type':'chat' }) .c('body').t("Henceforth I never will be Romeo."); _converse.connection._dataRecv(mock.createRequest(msg2)); /* Send an stanza to indicate the end of the result set. * * * * * 28482-98726-73623 * 09af3-cc343-b409f * 20 * * */ const stanza = $iq({'type': 'result', 'id': IQ_id}) .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('first', {'index': '0'}).t('23452-4534-1').up() .c('last').t('09af3-cc343-b409f').up() .c('count').t('16'); _converse.connection._dataRecv(mock.createRequest(stanza)); const result = await promise; expect(result.messages.length).toBe(2); expect(result.messages[0].outerHTML).toBe(msg1.nodeTree.outerHTML); expect(result.messages[1].outerHTML).toBe(msg2.nodeTree.outerHTML); expect(result.rsm.query.max).toBe('10'); expect(result.rsm.result.count).toBe(16); expect(result.rsm.result.first).toBe('23452-4534-1'); expect(result.rsm.result.last).toBe('09af3-cc343-b409f'); done() })); }); describe("The default preference", function () { it("is set once server support for MAM has been confirmed", mock.initConverse([], {}, async function (done, _converse) { const entity = await _converse.api.disco.entities.get(_converse.domain); spyOn(_converse, 'onMAMPreferences').and.callThrough(); _converse.message_archiving = 'never'; const feature = new Model({ 'var': Strophe.NS.MAM }); spyOn(feature, 'save').and.callFake(feature.set); // Save will complain about a url not being set entity.onFeatureAdded(feature); const IQ_stanzas = _converse.connection.IQ_stanzas; let sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(s => sizzle('iq[type="get"] prefs[xmlns="urn:xmpp:mam:2"]', s).length).pop()); expect(Strophe.serialize(sent_stanza)).toBe( ``+ ``+ ``); /* Example 20. Server responds with current preferences * * * * * * * */ let stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')}) .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'roster'}) .c('always').c('jid').t('romeo@montague.lit').up().up() .c('never').c('jid').t('montague@montague.lit'); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => _converse.onMAMPreferences.calls.count()); expect(_converse.onMAMPreferences).toHaveBeenCalled(); sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(s => sizzle('iq[type="set"] prefs[xmlns="urn:xmpp:mam:2"]', s).length).pop()); expect(Strophe.serialize(sent_stanza)).toBe( ``+ ``+ `romeo@montague.lit`+ `montague@montague.lit`+ ``+ `` ); expect(feature.get('preference')).toBe(undefined); /* * * * romeo@montague.lit * * * montague@montague.lit * * * */ stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')}) .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'always'}) .c('always').up() .c('never'); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => feature.save.calls.count()); expect(feature.save).toHaveBeenCalled(); expect(feature.get('preferences')['default']).toBe('never'); // eslint-disable-line dot-notation done(); })); }); }); describe("Chatboxes", function () { describe("A Chatbox", function () { it("will fetch archived messages once it's opened", mock.initConverse(['discoInitialized'], {}, async function (done, _converse) { await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sent_stanza = iq; IQ_id = sendIQ.bind(this)(iq, callback, errback); }); await u.waitUntil(() => sent_stanza); const stanza_el = sent_stanza; const queryid = stanza_el.querySelector('query').getAttribute('queryid'); expect(Strophe.serialize(sent_stanza)).toBe( ``+ ``+ ``+ `urn:xmpp:mam:2`+ `mercutio@montague.lit`+ ``+ `50`+ ``+ `` ); const msg1 = $msg({'id':'aeb212', 'to': contact_jid}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73623'}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to': contact_jid, 'from': _converse.bare_jid, 'type':'chat' }) .c('body').t("Call me but love, and I'll be new baptized;"); _converse.connection._dataRecv(mock.createRequest(msg1)); const msg2 = $msg({'id':'aeb213', 'to': contact_jid}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73624'}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to': contact_jid, 'from': _converse.bare_jid, 'type':'chat' }) .c('body').t("Henceforth I never will be Romeo."); _converse.connection._dataRecv(mock.createRequest(msg2)); const stanza = $iq({'type': 'result', 'id': IQ_id}) .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('first', {'index': '0'}).t('23452-4534-1').up() .c('last').t('09af3-cc343-b409f').up() .c('count').t('16'); _converse.connection._dataRecv(mock.createRequest(stanza)); done(); })); it("will show an error message if the MAM query times out", mock.initConverse(['discoInitialized'], {}, async function (done, _converse) { const sendIQ = _converse.connection.sendIQ; let timeout_happened = false; spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { sendIQ.bind(this)(iq, callback, errback); if (!timeout_happened) { if (typeof(iq.tree) === "function") { iq = iq.tree(); } if (sizzle('query[xmlns="urn:xmpp:mam:2"]', iq).length) { // We emulate a timeout event callback(null); timeout_happened = true; } } }); await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid); await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); const IQ_stanzas = _converse.connection.IQ_stanzas; let sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle('query[xmlns="urn:xmpp:mam:2"]', iq).length).pop()); let queryid = sent_stanza.querySelector('query').getAttribute('queryid'); expect(Strophe.serialize(sent_stanza)).toBe( ``+ ``+ ``+ `urn:xmpp:mam:2`+ `mercutio@montague.lit`+ ``+ `50`+ ``+ ``); const view = _converse.chatboxviews.get(contact_jid); expect(view.model.messages.length).toBe(1); expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false); expect(view.model.messages.at(0).get('type')).toBe('error'); expect(view.model.messages.at(0).get('message')).toBe('Timeout while trying to fetch archived messages.'); let err_message = await u.waitUntil(() => view.el.querySelector('.message.chat-error')); err_message.querySelector('.retry').click(); while (_converse.connection.IQ_stanzas.length) { _converse.connection.IQ_stanzas.pop(); } sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle('query[xmlns="urn:xmpp:mam:2"]', iq).length).pop()); queryid = sent_stanza.querySelector('query').getAttribute('queryid'); expect(Strophe.serialize(sent_stanza)).toBe( ``+ ``+ ``+ `urn:xmpp:mam:2`+ `mercutio@montague.lit`+ ``+ `50`+ ``+ ``); const msg1 = $msg({'id':'aeb212', 'to': contact_jid}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid': queryid, 'id':'28482-98726-73623'}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to': contact_jid, 'from': _converse.bare_jid, 'type':'chat' }) .c('body').t("Call me but love, and I'll be new baptized;"); _converse.connection._dataRecv(mock.createRequest(msg1)); const msg2 = $msg({'id':'aeb213', 'to': contact_jid}) .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid': queryid, 'id':'28482-98726-73624'}) .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:18:25Z'}).up() .c('message', { 'xmlns':'jabber:client', 'to': contact_jid, 'from': _converse.bare_jid, 'type':'chat' }) .c('body').t("Henceforth I never will be Romeo."); _converse.connection._dataRecv(mock.createRequest(msg2)); const stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')}) .c('fin', {'xmlns': 'urn:xmpp:mam:2', 'complete': true}) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('first', {'index': '0'}).t('28482-98726-73623').up() .c('last').t('28482-98726-73624').up() .c('count').t('2'); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => view.model.messages.length === 2, 500); err_message = view.el.querySelector('.message.chat-error'); expect(err_message).toBe(null); done(); })); }); });