From 15b22736315839d46ef51c3b490c1c48f8c507f6 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Fri, 3 May 2019 19:47:49 +0200 Subject: [PATCH] `_converse.api.archive.query` now returns a Promise instead of accepting a callback functions. --- CHANGES.md | 1 + dist/converse.js | 402 +++++++++++++------------ spec/mam.js | 124 +++----- src/converse-mam-views.js | 47 ++- src/headless/converse-mam.js | 302 ++++++++++--------- src/headless/dist/converse-headless.js | 369 ++++++++++++----------- 6 files changed, 615 insertions(+), 630 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ad5b5ce15..f078bb318 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ - **Breaking changes**: - Rename `muc_disable_moderator_commands` to [muc_disable_slash_commands](https://conversejs.org/docs/html/configuration.html#muc-disable-slash-commands). +- `_converse.api.archive.query` now returns a Promise instead of accepting a callback functions. ### API changes diff --git a/dist/converse.js b/dist/converse.js index 88149b365..b081b3ada 100644 --- a/dist/converse.js +++ b/dist/converse.js @@ -52121,26 +52121,27 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins } this.addSpinner(); + let result; - _converse.api.archive.query(Object.assign({ - 'groupchat': is_groupchat, - 'before': '', - // Page backwards from the most recent message - 'max': _converse.archived_messages_page_size, - 'with': this.model.get('jid') - }, options), messages => { - // Success - this.clearSpinner(); - - _.each(messages, message_handler); - }, e => { - // Error - this.clearSpinner(); - + try { + result = await _converse.api.archive.query(Object.assign({ + 'groupchat': is_groupchat, + 'before': '', + // Page backwards from the most recent message + 'max': _converse.archived_messages_page_size, + 'with': this.model.get('jid') + }, options)); + } catch (e) { _converse.log("Error or timeout while trying to fetch " + "archived messages", Strophe.LogLevel.ERROR); _converse.log(e, Strophe.LogLevel.ERROR); - }); + } finally { + this.clearSpinner(); + } + + if (result.messages) { + result.messages.forEach(message_handler); + } }, onScroll(ev) { @@ -66323,121 +66324,6 @@ const u = _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].env.utils; const RSM_ATTRIBUTES = ['max', 'first', 'last', 'after', 'before', 'index', 'count']; // XEP-0313 Message Archive Management const MAM_ATTRIBUTES = ['with', 'start', 'end']; - -function queryForArchivedMessages(_converse, options, callback, errback) { - /* Internal function, called by the "archive.query" API method. - */ - let date; - - if (_.isFunction(options)) { - callback = options; - errback = callback; - options = null; - } - - const queryid = _converse.connection.getUniqueId(); - - const attrs = { - 'type': 'set' - }; - - if (options && options.groupchat) { - if (!options['with']) { - // eslint-disable-line dot-notation - throw new Error('You need to specify a "with" value containing ' + 'the chat room JID, when querying groupchat messages.'); - } - - attrs.to = options['with']; // eslint-disable-line dot-notation - } - - const stanza = $iq(attrs).c('query', { - 'xmlns': Strophe.NS.MAM, - 'queryid': queryid - }); - - if (options) { - stanza.c('x', { - 'xmlns': Strophe.NS.XFORM, - 'type': 'submit' - }).c('field', { - 'var': 'FORM_TYPE', - 'type': 'hidden' - }).c('value').t(Strophe.NS.MAM).up().up(); - - if (options['with'] && !options.groupchat) { - // eslint-disable-line dot-notation - stanza.c('field', { - 'var': 'with' - }).c('value').t(options['with']).up().up(); // eslint-disable-line dot-notation - } - - _.each(['start', 'end'], function (t) { - if (options[t]) { - date = moment(options[t]); - - if (date.isValid()) { - stanza.c('field', { - 'var': t - }).c('value').t(date.format()).up().up(); - } else { - throw new TypeError(`archive.query: invalid date provided for: ${t}`); - } - } - }); - - stanza.up(); - - if (options instanceof Strophe.RSM) { - stanza.cnode(options.toXML()); - } else if (_.intersection(RSM_ATTRIBUTES, Object.keys(options)).length) { - stanza.cnode(new Strophe.RSM(options).toXML()); - } - } - - const messages = []; - - const message_handler = _converse.connection.addHandler(message => { - if (options.groupchat && message.getAttribute('from') !== options['with']) { - // eslint-disable-line dot-notation - return true; - } - - const result = message.querySelector('result'); - - if (!_.isNull(result) && result.getAttribute('queryid') === queryid) { - messages.push(message); - } - - return true; - }, Strophe.NS.MAM); - - _converse.api.sendIQ(stanza, _converse.message_archiving_timeout).then(iq => { - _converse.connection.deleteHandler(message_handler); - - if (_.isFunction(callback)) { - const set = iq.querySelector('set'); - let rsm; - - if (!_.isUndefined(set)) { - rsm = new Strophe.RSM({ - xml: set - }); - Object.assign(rsm, _.pick(options, _.concat(MAM_ATTRIBUTES, ['max']))); - } - - callback(messages, rsm); - } - }).catch(e => { - _converse.connection.deleteHandler(message_handler); - - if (_.isFunction(errback)) { - errback.apply(this, arguments); - } - - return; - }); -} - _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam', { dependencies: ['converse-muc'], overrides: { @@ -66613,15 +66499,11 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * * `before` * * `index` * * `count` - * @param {Function} callback A function to call whenever - * we receive query-relevant stanza. - * When the callback is called, a Strophe.RSM object is - * returned on which "next" or "previous" can be called - * before passing it in again to this method, to - * get the next or previous page in the result set. - * @param {Function} errback A function to call when an - * error stanza is received, for example when it - * doesn't support message archiving. + * @throws {Error} An error is thrown if the XMPP server responds with an error. + * @returns {Promise} A promise which resolves to an object which + * will have keys `messages` and `rsm` which contains a Strophe.RSM object + * on which "next" or "previous" can be called before passing it in again + * to this method, to get the next or previous page in the result set. * * @example * // Requesting all archived messages @@ -66629,41 +66511,17 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * // * // The simplest query that can be made is to simply not pass in any parameters. * // Such a query will return all archived messages for the current user. - * // - * // Generally, you'll however always want to pass in a callback method, to receive - * // the returned messages. * - * this._converse.api.archive.query( - * (messages) => { - * // Do something with the messages, like showing them in your webpage. - * }, - * (iq) => { - * // The query was not successful, perhaps inform the user? - * // The IQ stanza returned by the XMPP server is passed in, so that you - * // may inspect it and determine what the problem was. - * } - * ) - * @example - * // Waiting until server support has been determined - * // ================================================ - * // - * // The query method will only work if Converse has been able to determine that - * // the server supports MAM queries, otherwise the following error will be raised: - * // - * // "This server does not support XEP-0313, Message Archive Management" - * // - * // The very first time Converse loads in a browser tab, if you call the query - * // API too quickly, the above error might appear because service discovery has not - * // yet been completed. - * // - * // To work solve this problem, you can first listen for the `serviceDiscovered` event, - * // through which you can be informed once support for MAM has been determined. - * - * _converse.api.listen.on('serviceDiscovered', function (feature) { - * if (feature.get('var') === converse.env.Strophe.NS.MAM) { - * _converse.api.archive.query() - * } - * }); + * let result; + * try { + * result = await _converse.api.archive.query(); + * } catch (e) { + * // The query was not successful, perhaps inform the user? + * // The IQ stanza returned by the XMPP server is passed in, so that you + * // may inspect it and determine what the problem was. + * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); * * @example * // Requesting all archived messages for a particular contact or room @@ -66674,10 +66532,20 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * // room under the `with` key. * * // For a particular user - * this._converse.api.archive.query({'with': 'john@doe.net'}, callback, errback);) + * let result; + * try { + * result = await _converse.api.archive.query({'with': 'john@doe.net'}); + * } catch (e) { + * // The query was not successful + * } * * // For a particular room - * this._converse.api.archive.query({'with': 'discuss@conference.doglovers.net'}, callback, errback);) + * let result; + * try { + * result = await _converse.api.archive.query({'with': 'discuss@conference.doglovers.net'}); + * } catch (e) { + * // The query was not successful + * } * * @example * // Requesting all archived messages before or after a certain date @@ -66692,7 +66560,12 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * 'start': '2010-06-07T00:00:00Z', * 'end': '2010-07-07T13:23:54Z' * }; - * this._converse.api.archive.query(options, callback, errback); + * let result; + * try { + * result = await _converse.api.archive.query(options); + * } catch (e) { + * // The query was not successful + * } * * @example * // Limiting the amount of messages returned @@ -66702,7 +66575,12 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * // By default, the messages are returned from oldest to newest. * * // Return maximum 10 archived messages - * this._converse.api.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback); + * let result; + * try { + * result = await _converse.api.archive.query({'with': 'john@doe.net', 'max':10}); + * } catch (e) { + * // The query was not successful + * } * * @example * // Paging forwards through a set of archived messages @@ -66712,8 +66590,8 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * // repeatedly make a further query to fetch the next batch of messages. * // * // To simplify this usecase for you, the callback method receives not only an array - * // with the returned archived messages, but also a special RSM (*Result Set - * // Management*) object which contains the query parameters you passed in, as well + * // with the returned archived messages, but also a special Strophe.RSM (*Result Set Management*) + * // object which contains the query parameters you passed in, as well * // as two utility methods `next`, and `previous`. * // * // When you call one of these utility methods on the returned RSM object, and then @@ -66721,14 +66599,24 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * // archived messages. Please note, when calling these methods, pass in an integer * // to limit your results. * - * const callback = function (messages, rsm) { - * // Do something with the messages, like showing them in your webpage. - * // ... - * // You can now use the returned "rsm" object, to fetch the next batch of messages: - * _converse.api.archive.query(rsm.next(10), callback, errback)) - * + * let result; + * try { + * result = await _converse.api.archive.query({'with': 'john@doe.net', 'max':10}); + * } catch (e) { + * // The query was not successful + * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); + * + * while (result.rsm) { + * try { + * result = await _converse.api.archive.query(rsm.next(10)); + * } catch (e) { + * // The query was not successful + * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); * } - * _converse.api.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback); * * @example * // Paging backwards through a set of archived messages @@ -66739,22 +66627,142 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * // `before` parameter. If you simply want to page backwards from the most recent * // message, pass in the `before` parameter with an empty string value `''`. * - * _converse.api.archive.query({'before': '', 'max':5}, function (message, rsm) { - * // Do something with the messages, like showing them in your webpage. - * // ... - * // You can now use the returned "rsm" object, to fetch the previous batch of messages: - * rsm.previous(5); // Call previous method, to update the object's parameters, - * // passing in a limit value of 5. - * // Now we query again, to get the previous batch. - * _converse.api.archive.query(rsm, callback, errback); + * let result; + * try { + * result = await _converse.api.archive.query({'before': '', 'max':5}); + * } catch (e) { + * // The query was not successful * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); + * + * // Now we query again, to get the previous batch. + * try { + * result = await _converse.api.archive.query(rsm.previous(5);); + * } catch (e) { + * // The query was not successful + * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); + * */ - 'query': function query(options, callback, errback) { + 'query': async function query(options) { if (!_converse.api.connection.connected()) { throw new Error('Can\'t call `api.archive.query` before having established an XMPP session'); } - return queryForArchivedMessages(_converse, options, callback, errback); + const attrs = { + 'type': 'set' + }; + + if (options && options.groupchat) { + if (!options['with']) { + // eslint-disable-line dot-notation + throw new Error('You need to specify a "with" value containing ' + 'the chat room JID, when querying groupchat messages.'); + } + + attrs.to = options['with']; // eslint-disable-line dot-notation + } + + const jid = attrs.to || _converse.bare_jid; + const supported = await _converse.api.disco.supports(Strophe.NS.MAM, jid); + + if (!supported.length) { + _converse.log(`Did not fetch MAM archive for ${jid} because it doesn't support ${Strophe.NS.MAM}`); + + return { + 'messages': [] + }; + } + + const queryid = _converse.connection.getUniqueId(); + + const stanza = $iq(attrs).c('query', { + 'xmlns': Strophe.NS.MAM, + 'queryid': queryid + }); + + if (options) { + stanza.c('x', { + 'xmlns': Strophe.NS.XFORM, + 'type': 'submit' + }).c('field', { + 'var': 'FORM_TYPE', + 'type': 'hidden' + }).c('value').t(Strophe.NS.MAM).up().up(); + + if (options['with'] && !options.groupchat) { + // eslint-disable-line dot-notation + stanza.c('field', { + 'var': 'with' + }).c('value').t(options['with']).up().up(); // eslint-disable-line dot-notation + } + + ['start', 'end'].forEach(t => { + if (options[t]) { + const date = moment(options[t]); + + if (date.isValid()) { + stanza.c('field', { + 'var': t + }).c('value').t(date.format()).up().up(); + } else { + throw new TypeError(`archive.query: invalid date provided for: ${t}`); + } + } + }); + stanza.up(); + + if (options instanceof Strophe.RSM) { + stanza.cnode(options.toXML()); + } else if (_.intersection(RSM_ATTRIBUTES, Object.keys(options)).length) { + stanza.cnode(new Strophe.RSM(options).toXML()); + } + } + + const messages = []; + + const message_handler = _converse.connection.addHandler(message => { + if (options.groupchat && message.getAttribute('from') !== options['with']) { + // eslint-disable-line dot-notation + return true; + } + + const result = message.querySelector('result'); + + if (!_.isNull(result) && result.getAttribute('queryid') === queryid) { + messages.push(message); + } + + return true; + }, Strophe.NS.MAM); + + let iq; + + try { + iq = await _converse.api.sendIQ(stanza, _converse.message_archiving_timeout); + } catch (e) { + _converse.connection.deleteHandler(message_handler); + + throw e; + } + + _converse.connection.deleteHandler(message_handler); + + const set = iq.querySelector('set'); + let rsm; + + if (!_.isNull(set)) { + rsm = new Strophe.RSM({ + 'xml': set + }); + Object.assign(rsm, _.pick(options, _.concat(MAM_ATTRIBUTES, ['max']))); + } + + return { + messages, + rsm + }; } } }); diff --git a/spec/mam.js b/spec/mam.js index 1b830a614..a22307793 100644 --- a/spec/mam.js +++ b/spec/mam.js @@ -162,18 +162,17 @@ it("can be used to query for all archived messages", mock.initConverse( null, ['discoInitialized'], {}, - function (done, _converse) { + async function (done, _converse) { - let sent_stanza, IQ_id; const sendIQ = _converse.connection.sendIQ; + await test_utils.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); }); - 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}); - } _converse.api.archive.query(); + await test_utils.waitUntil(() => sent_stanza); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``); @@ -185,10 +184,7 @@ null, [], {}, async function (done, _converse) { - const entity = await _converse.api.disco.entities.get(_converse.domain); - if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { - _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); - } + await test_utils.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) { @@ -196,6 +192,7 @@ IQ_id = sendIQ.bind(this)(iq, callback, errback); }); _converse.api.archive.query({'with':'juliet@capulet.lit'}); + await test_utils.waitUntil(() => sent_stanza); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ @@ -218,22 +215,16 @@ null, [], {}, async function (done, _converse) { - const entity = await _converse.api.disco.entities.get(_converse.domain); - if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { - _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); - } - + await test_utils.openAndEnterChatRoom(_converse, 'coven', 'chat.shakespeare.lit', 'nicky', [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 callback = jasmine.createSpy('callback'); - - _converse.api.archive.query({'with': 'coven@chat.shakespeare.lit', 'groupchat': true}, callback); + _converse.api.archive.query({'with': 'coven@chat.shakespeare.lit', 'groupchat': true}); + await test_utils.waitUntil(() => sent_stanza); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); - expect(sent_stanza.toString()).toBe( ``+ ``+ @@ -252,19 +243,15 @@ null, [], {}, async function (done, _converse) { - const entity = await _converse.api.disco.entities.get(_converse.domain); - if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { - _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); - } + await test_utils.openAndEnterChatRoom(_converse, 'coven', 'chat.shakespeare.lit', 'nicky', [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 callback = jasmine.createSpy('callback'); - - _converse.api.archive.query({'with': 'coven@chat.shakespear.lit', 'groupchat': true, 'max':'10'}, callback); + const promise = _converse.api.archive.query({'with': 'coven@chat.shakespear.lit', 'groupchat': true, 'max':'10'}); + await test_utils.waitUntil(() => sent_stanza); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); /* @@ -318,10 +305,8 @@ .c('count').t('16'); _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await test_utils.waitUntil(() => callback.calls.count()); - expect(callback).toHaveBeenCalled(); - const args = callback.calls.argsFor(0); - expect(args[0].length).toBe(0); + const result = await promise; + expect(result.messages.length).toBe(0); done(); })); @@ -330,23 +315,20 @@ null, [], {}, async function (done, _converse) { + await test_utils.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 entities = await _converse.api.disco.entities.get(); - if (!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'; const end = '2010-07-07T13:23:54Z'; _converse.api.archive.query({ 'start': start, 'end': end - }); + await test_utils.waitUntil(() => sent_stanza); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ @@ -373,13 +355,12 @@ null, [], {}, async function (done, _converse) { - const entity = await _converse.api.disco.entities.get(_converse.domain); - if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { - _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); + await test_utils.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')); } - expect(_.partial(_converse.api.archive.query, {'start': 'not a real date'})).toThrow( - new TypeError('archive.query: invalid date provided for: start') - ); done(); })); @@ -388,10 +369,7 @@ null, [], {}, async function (done, _converse) { - const entity = await _converse.api.disco.entities.get(_converse.domain); - if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { - _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); - } + await test_utils.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) { @@ -403,6 +381,7 @@ } const start = '2010-06-07T00:00:00Z'; _converse.api.archive.query({'start': start}); + await test_utils.waitUntil(() => sent_stanza); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ @@ -426,10 +405,7 @@ null, [], {}, async function (done, _converse) { - const entity = await _converse.api.disco.entities.get(_converse.domain); - if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { - _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); - } + await test_utils.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) { @@ -438,6 +414,7 @@ }); const start = '2010-06-07T00:00:00Z'; _converse.api.archive.query({'start': start, 'max':10}); + await test_utils.waitUntil(() => sent_stanza); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ @@ -464,10 +441,7 @@ null, [], {}, async function (done, _converse) { - const entity = await _converse.api.disco.entities.get(_converse.domain); - if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { - _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); - } + await test_utils.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) { @@ -480,6 +454,7 @@ 'after': '09af3-cc343-b409f', 'max':10 }); + await test_utils.waitUntil(() => sent_stanza); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ @@ -506,10 +481,7 @@ null, [], {}, async function (done, _converse) { - const entity = await _converse.api.disco.entities.get(_converse.domain); - if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { - _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); - } + await test_utils.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) { @@ -517,6 +489,7 @@ IQ_id = sendIQ.bind(this)(iq, callback, errback); }); _converse.api.archive.query({'before': '', 'max':10}); + await test_utils.waitUntil(() => sent_stanza); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ @@ -540,10 +513,7 @@ null, [], {}, async function (done, _converse) { - const entity = await _converse.api.disco.entities.get(_converse.domain); - if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { - _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); - } + await test_utils.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) { @@ -558,7 +528,7 @@ rsm['with'] = 'romeo@montague.lit'; // eslint-disable-line dot-notation rsm.start = '2010-06-07T00:00:00Z'; _converse.api.archive.query(rsm); - + await test_utils.waitUntil(() => sent_stanza); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); expect(sent_stanza.toString()).toBe( ``+ @@ -582,24 +552,20 @@ done(); })); - it("accepts a callback function, which it passes the messages and a Strophe.RSM object", + it("returns an object which includes the messages and a Strophe.RSM object", mock.initConverse( null, [], {}, async function (done, _converse) { - const entity = await _converse.api.disco.entities.get(_converse.domain); - if (!entity.features.findWhere({'var': Strophe.NS.MAM})) { - _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); - } + await test_utils.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 callback = jasmine.createSpy('callback'); - - _converse.api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'}, callback); + const promise = _converse.api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'}); + await test_utils.waitUntil(() => sent_stanza); const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); /* @@ -659,17 +625,15 @@ .c('count').t('16'); _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await test_utils.waitUntil(() => callback.calls.count()); - expect(callback).toHaveBeenCalled(); - const args = callback.calls.argsFor(0); - expect(args[0].length).toBe(2); - expect(args[0][0].outerHTML).toBe(msg1.nodeTree.outerHTML); - expect(args[0][1].outerHTML).toBe(msg2.nodeTree.outerHTML); - expect(args[1]['with']).toBe('romeo@capulet.lit'); // eslint-disable-line dot-notation - expect(args[1].max).toBe('10'); - expect(args[1].count).toBe('16'); - expect(args[1].first).toBe('23452-4534-1'); - expect(args[1].last).toBe('09af3-cc343-b409f'); + 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['with']).toBe('romeo@capulet.lit'); // eslint-disable-line dot-notation + expect(result.rsm.max).toBe('10'); + expect(result.rsm.count).toBe('16'); + expect(result.rsm.first).toBe('23452-4534-1'); + expect(result.rsm.last).toBe('09af3-cc343-b409f'); done() })); }); diff --git a/src/converse-mam-views.js b/src/converse-mam-views.js index cbed575f7..dcbeac84b 100644 --- a/src/converse-mam-views.js +++ b/src/converse-mam-views.js @@ -82,10 +82,10 @@ converse.plugins.add('converse-mam-views', { async fetchArchivedMessages (options) { const { _converse } = this.__super__; - if (this.disable_mam) { return; } - + if (this.disable_mam) { + return; + } const is_groupchat = this.model.get('type') === CHATROOMS_TYPE; - let mam_jid, message_handler; if (is_groupchat) { mam_jid = this.model.get('jid'); @@ -94,32 +94,31 @@ converse.plugins.add('converse-mam-views', { mam_jid = _converse.bare_jid; message_handler = _converse.chatboxes.onMessage.bind(_converse.chatboxes) } - const supported = await _converse.api.disco.supports(Strophe.NS.MAM, mam_jid); if (!supported.length) { return; } this.addSpinner(); - _converse.api.archive.query( - Object.assign({ - 'groupchat': is_groupchat, - 'before': '', // Page backwards from the most recent message - 'max': _converse.archived_messages_page_size, - 'with': this.model.get('jid'), - }, options), - - messages => { // Success - this.clearSpinner(); - _.each(messages, message_handler); - }, - e => { // Error - this.clearSpinner(); - _converse.log( - "Error or timeout while trying to fetch "+ - "archived messages", Strophe.LogLevel.ERROR); - _converse.log(e, Strophe.LogLevel.ERROR); - } - ); + let result; + try { + result = await _converse.api.archive.query( + Object.assign({ + 'groupchat': is_groupchat, + 'before': '', // Page backwards from the most recent message + 'max': _converse.archived_messages_page_size, + 'with': this.model.get('jid'), + }, options)); + } catch (e) { + _converse.log( + "Error or timeout while trying to fetch "+ + "archived messages", Strophe.LogLevel.ERROR); + _converse.log(e, Strophe.LogLevel.ERROR); + } finally { + this.clearSpinner(); + } + if (result.messages) { + result.messages.forEach(message_handler); + } }, onScroll (ev) { diff --git a/src/headless/converse-mam.js b/src/headless/converse-mam.js index 90776797f..01d9c47e5 100644 --- a/src/headless/converse-mam.js +++ b/src/headless/converse-mam.js @@ -21,88 +21,6 @@ const RSM_ATTRIBUTES = ['max', 'first', 'last', 'after', 'before', 'index', 'cou const MAM_ATTRIBUTES = ['with', 'start', 'end']; -function queryForArchivedMessages (_converse, options, callback, errback) { - /* Internal function, called by the "archive.query" API method. - */ - let date; - if (_.isFunction(options)) { - callback = options; - errback = callback; - options = null; - } - const queryid = _converse.connection.getUniqueId(); - const attrs = {'type':'set'}; - if (options && options.groupchat) { - if (!options['with']) { // eslint-disable-line dot-notation - throw new Error( - 'You need to specify a "with" value containing '+ - 'the chat room JID, when querying groupchat messages.'); - } - attrs.to = options['with']; // eslint-disable-line dot-notation - } - - const stanza = $iq(attrs).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':queryid}); - if (options) { - stanza.c('x', {'xmlns':Strophe.NS.XFORM, 'type': 'submit'}) - .c('field', {'var':'FORM_TYPE', 'type': 'hidden'}) - .c('value').t(Strophe.NS.MAM).up().up(); - - if (options['with'] && !options.groupchat) { // eslint-disable-line dot-notation - stanza.c('field', {'var':'with'}).c('value') - .t(options['with']).up().up(); // eslint-disable-line dot-notation - } - _.each(['start', 'end'], function (t) { - if (options[t]) { - date = moment(options[t]); - if (date.isValid()) { - stanza.c('field', {'var':t}).c('value').t(date.format()).up().up(); - } else { - throw new TypeError(`archive.query: invalid date provided for: ${t}`); - } - } - }); - stanza.up(); - if (options instanceof Strophe.RSM) { - stanza.cnode(options.toXML()); - } else if (_.intersection(RSM_ATTRIBUTES, Object.keys(options)).length) { - stanza.cnode(new Strophe.RSM(options).toXML()); - } - } - - const messages = []; - const message_handler = _converse.connection.addHandler((message) => { - if (options.groupchat && message.getAttribute('from') !== options['with']) { // eslint-disable-line dot-notation - return true; - } - const result = message.querySelector('result'); - if (!_.isNull(result) && result.getAttribute('queryid') === queryid) { - messages.push(message); - } - return true; - }, Strophe.NS.MAM); - - _converse.api.sendIQ(stanza, _converse.message_archiving_timeout) - .then(iq => { - _converse.connection.deleteHandler(message_handler); - if (_.isFunction(callback)) { - const set = iq.querySelector('set'); - let rsm; - if (!_.isUndefined(set)) { - rsm = new Strophe.RSM({xml: set}); - Object.assign(rsm, _.pick(options, _.concat(MAM_ATTRIBUTES, ['max']))); - } - callback(messages, rsm); - } - }).catch(e => { - _converse.connection.deleteHandler(message_handler); - if (_.isFunction(errback)) { - errback.apply(this, arguments); - } - return; - }); -} - - converse.plugins.add('converse-mam', { dependencies: ['converse-muc'], @@ -260,15 +178,11 @@ converse.plugins.add('converse-mam', { * * `before` * * `index` * * `count` - * @param {Function} callback A function to call whenever - * we receive query-relevant stanza. - * When the callback is called, a Strophe.RSM object is - * returned on which "next" or "previous" can be called - * before passing it in again to this method, to - * get the next or previous page in the result set. - * @param {Function} errback A function to call when an - * error stanza is received, for example when it - * doesn't support message archiving. + * @throws {Error} An error is thrown if the XMPP server responds with an error. + * @returns {Promise} A promise which resolves to an object which + * will have keys `messages` and `rsm` which contains a Strophe.RSM object + * on which "next" or "previous" can be called before passing it in again + * to this method, to get the next or previous page in the result set. * * @example * // Requesting all archived messages @@ -276,41 +190,17 @@ converse.plugins.add('converse-mam', { * // * // The simplest query that can be made is to simply not pass in any parameters. * // Such a query will return all archived messages for the current user. - * // - * // Generally, you'll however always want to pass in a callback method, to receive - * // the returned messages. * - * this._converse.api.archive.query( - * (messages) => { - * // Do something with the messages, like showing them in your webpage. - * }, - * (iq) => { - * // The query was not successful, perhaps inform the user? - * // The IQ stanza returned by the XMPP server is passed in, so that you - * // may inspect it and determine what the problem was. - * } - * ) - * @example - * // Waiting until server support has been determined - * // ================================================ - * // - * // The query method will only work if Converse has been able to determine that - * // the server supports MAM queries, otherwise the following error will be raised: - * // - * // "This server does not support XEP-0313, Message Archive Management" - * // - * // The very first time Converse loads in a browser tab, if you call the query - * // API too quickly, the above error might appear because service discovery has not - * // yet been completed. - * // - * // To work solve this problem, you can first listen for the `serviceDiscovered` event, - * // through which you can be informed once support for MAM has been determined. - * - * _converse.api.listen.on('serviceDiscovered', function (feature) { - * if (feature.get('var') === converse.env.Strophe.NS.MAM) { - * _converse.api.archive.query() - * } - * }); + * let result; + * try { + * result = await _converse.api.archive.query(); + * } catch (e) { + * // The query was not successful, perhaps inform the user? + * // The IQ stanza returned by the XMPP server is passed in, so that you + * // may inspect it and determine what the problem was. + * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); * * @example * // Requesting all archived messages for a particular contact or room @@ -321,10 +211,20 @@ converse.plugins.add('converse-mam', { * // room under the `with` key. * * // For a particular user - * this._converse.api.archive.query({'with': 'john@doe.net'}, callback, errback);) + * let result; + * try { + * result = await _converse.api.archive.query({'with': 'john@doe.net'}); + * } catch (e) { + * // The query was not successful + * } * * // For a particular room - * this._converse.api.archive.query({'with': 'discuss@conference.doglovers.net'}, callback, errback);) + * let result; + * try { + * result = await _converse.api.archive.query({'with': 'discuss@conference.doglovers.net'}); + * } catch (e) { + * // The query was not successful + * } * * @example * // Requesting all archived messages before or after a certain date @@ -339,7 +239,12 @@ converse.plugins.add('converse-mam', { * 'start': '2010-06-07T00:00:00Z', * 'end': '2010-07-07T13:23:54Z' * }; - * this._converse.api.archive.query(options, callback, errback); + * let result; + * try { + * result = await _converse.api.archive.query(options); + * } catch (e) { + * // The query was not successful + * } * * @example * // Limiting the amount of messages returned @@ -349,7 +254,12 @@ converse.plugins.add('converse-mam', { * // By default, the messages are returned from oldest to newest. * * // Return maximum 10 archived messages - * this._converse.api.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback); + * let result; + * try { + * result = await _converse.api.archive.query({'with': 'john@doe.net', 'max':10}); + * } catch (e) { + * // The query was not successful + * } * * @example * // Paging forwards through a set of archived messages @@ -359,8 +269,8 @@ converse.plugins.add('converse-mam', { * // repeatedly make a further query to fetch the next batch of messages. * // * // To simplify this usecase for you, the callback method receives not only an array - * // with the returned archived messages, but also a special RSM (*Result Set - * // Management*) object which contains the query parameters you passed in, as well + * // with the returned archived messages, but also a special Strophe.RSM (*Result Set Management*) + * // object which contains the query parameters you passed in, as well * // as two utility methods `next`, and `previous`. * // * // When you call one of these utility methods on the returned RSM object, and then @@ -368,14 +278,24 @@ converse.plugins.add('converse-mam', { * // archived messages. Please note, when calling these methods, pass in an integer * // to limit your results. * - * const callback = function (messages, rsm) { - * // Do something with the messages, like showing them in your webpage. - * // ... - * // You can now use the returned "rsm" object, to fetch the next batch of messages: - * _converse.api.archive.query(rsm.next(10), callback, errback)) - * + * let result; + * try { + * result = await _converse.api.archive.query({'with': 'john@doe.net', 'max':10}); + * } catch (e) { + * // The query was not successful + * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); + * + * while (result.rsm) { + * try { + * result = await _converse.api.archive.query(rsm.next(10)); + * } catch (e) { + * // The query was not successful + * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); * } - * _converse.api.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback); * * @example * // Paging backwards through a set of archived messages @@ -386,21 +306,107 @@ converse.plugins.add('converse-mam', { * // `before` parameter. If you simply want to page backwards from the most recent * // message, pass in the `before` parameter with an empty string value `''`. * - * _converse.api.archive.query({'before': '', 'max':5}, function (message, rsm) { - * // Do something with the messages, like showing them in your webpage. - * // ... - * // You can now use the returned "rsm" object, to fetch the previous batch of messages: - * rsm.previous(5); // Call previous method, to update the object's parameters, - * // passing in a limit value of 5. - * // Now we query again, to get the previous batch. - * _converse.api.archive.query(rsm, callback, errback); + * let result; + * try { + * result = await _converse.api.archive.query({'before': '', 'max':5}); + * } catch (e) { + * // The query was not successful * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); + * + * // Now we query again, to get the previous batch. + * try { + * result = await _converse.api.archive.query(rsm.previous(5);); + * } catch (e) { + * // The query was not successful + * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); + * */ - 'query': function (options, callback, errback) { + 'query': async function (options) { if (!_converse.api.connection.connected()) { throw new Error('Can\'t call `api.archive.query` before having established an XMPP session'); } - return queryForArchivedMessages(_converse, options, callback, errback); + const attrs = {'type':'set'}; + if (options && options.groupchat) { + if (!options['with']) { // eslint-disable-line dot-notation + throw new Error( + 'You need to specify a "with" value containing '+ + 'the chat room JID, when querying groupchat messages.'); + } + attrs.to = options['with']; // eslint-disable-line dot-notation + } + + const jid = attrs.to || _converse.bare_jid; + const supported = await _converse.api.disco.supports(Strophe.NS.MAM, jid); + if (!supported.length) { + _converse.log(`Did not fetch MAM archive for ${jid} because it doesn't support ${Strophe.NS.MAM}`); + return { + 'messages': [] + }; + } + + const queryid = _converse.connection.getUniqueId(); + const stanza = $iq(attrs).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':queryid}); + if (options) { + stanza.c('x', {'xmlns':Strophe.NS.XFORM, 'type': 'submit'}) + .c('field', {'var':'FORM_TYPE', 'type': 'hidden'}) + .c('value').t(Strophe.NS.MAM).up().up(); + + if (options['with'] && !options.groupchat) { // eslint-disable-line dot-notation + stanza.c('field', {'var':'with'}).c('value') + .t(options['with']).up().up(); // eslint-disable-line dot-notation + } + ['start', 'end'].forEach(t => { + if (options[t]) { + const date = moment(options[t]); + if (date.isValid()) { + stanza.c('field', {'var':t}).c('value').t(date.format()).up().up(); + } else { + throw new TypeError(`archive.query: invalid date provided for: ${t}`); + } + } + }); + stanza.up(); + if (options instanceof Strophe.RSM) { + stanza.cnode(options.toXML()); + } else if (_.intersection(RSM_ATTRIBUTES, Object.keys(options)).length) { + stanza.cnode(new Strophe.RSM(options).toXML()); + } + } + + const messages = []; + const message_handler = _converse.connection.addHandler(message => { + if (options.groupchat && message.getAttribute('from') !== options['with']) { // eslint-disable-line dot-notation + return true; + } + const result = message.querySelector('result'); + if (!_.isNull(result) && result.getAttribute('queryid') === queryid) { + messages.push(message); + } + return true; + }, Strophe.NS.MAM); + + let iq; + try { + iq = await _converse.api.sendIQ(stanza, _converse.message_archiving_timeout) + } catch (e) { + _converse.connection.deleteHandler(message_handler); + throw(e); + } + _converse.connection.deleteHandler(message_handler); + const set = iq.querySelector('set'); + let rsm; + if (!_.isNull(set)) { + rsm = new Strophe.RSM({'xml': set}); + Object.assign(rsm, _.pick(options, _.concat(MAM_ATTRIBUTES, ['max']))); + } + return { + messages, + rsm + } } } }); diff --git a/src/headless/dist/converse-headless.js b/src/headless/dist/converse-headless.js index ff145b261..9dc7f9904 100644 --- a/src/headless/dist/converse-headless.js +++ b/src/headless/dist/converse-headless.js @@ -44571,121 +44571,6 @@ const u = _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].env.utils; const RSM_ATTRIBUTES = ['max', 'first', 'last', 'after', 'before', 'index', 'count']; // XEP-0313 Message Archive Management const MAM_ATTRIBUTES = ['with', 'start', 'end']; - -function queryForArchivedMessages(_converse, options, callback, errback) { - /* Internal function, called by the "archive.query" API method. - */ - let date; - - if (_.isFunction(options)) { - callback = options; - errback = callback; - options = null; - } - - const queryid = _converse.connection.getUniqueId(); - - const attrs = { - 'type': 'set' - }; - - if (options && options.groupchat) { - if (!options['with']) { - // eslint-disable-line dot-notation - throw new Error('You need to specify a "with" value containing ' + 'the chat room JID, when querying groupchat messages.'); - } - - attrs.to = options['with']; // eslint-disable-line dot-notation - } - - const stanza = $iq(attrs).c('query', { - 'xmlns': Strophe.NS.MAM, - 'queryid': queryid - }); - - if (options) { - stanza.c('x', { - 'xmlns': Strophe.NS.XFORM, - 'type': 'submit' - }).c('field', { - 'var': 'FORM_TYPE', - 'type': 'hidden' - }).c('value').t(Strophe.NS.MAM).up().up(); - - if (options['with'] && !options.groupchat) { - // eslint-disable-line dot-notation - stanza.c('field', { - 'var': 'with' - }).c('value').t(options['with']).up().up(); // eslint-disable-line dot-notation - } - - _.each(['start', 'end'], function (t) { - if (options[t]) { - date = moment(options[t]); - - if (date.isValid()) { - stanza.c('field', { - 'var': t - }).c('value').t(date.format()).up().up(); - } else { - throw new TypeError(`archive.query: invalid date provided for: ${t}`); - } - } - }); - - stanza.up(); - - if (options instanceof Strophe.RSM) { - stanza.cnode(options.toXML()); - } else if (_.intersection(RSM_ATTRIBUTES, Object.keys(options)).length) { - stanza.cnode(new Strophe.RSM(options).toXML()); - } - } - - const messages = []; - - const message_handler = _converse.connection.addHandler(message => { - if (options.groupchat && message.getAttribute('from') !== options['with']) { - // eslint-disable-line dot-notation - return true; - } - - const result = message.querySelector('result'); - - if (!_.isNull(result) && result.getAttribute('queryid') === queryid) { - messages.push(message); - } - - return true; - }, Strophe.NS.MAM); - - _converse.api.sendIQ(stanza, _converse.message_archiving_timeout).then(iq => { - _converse.connection.deleteHandler(message_handler); - - if (_.isFunction(callback)) { - const set = iq.querySelector('set'); - let rsm; - - if (!_.isUndefined(set)) { - rsm = new Strophe.RSM({ - xml: set - }); - Object.assign(rsm, _.pick(options, _.concat(MAM_ATTRIBUTES, ['max']))); - } - - callback(messages, rsm); - } - }).catch(e => { - _converse.connection.deleteHandler(message_handler); - - if (_.isFunction(errback)) { - errback.apply(this, arguments); - } - - return; - }); -} - _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam', { dependencies: ['converse-muc'], overrides: { @@ -44861,15 +44746,11 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * * `before` * * `index` * * `count` - * @param {Function} callback A function to call whenever - * we receive query-relevant stanza. - * When the callback is called, a Strophe.RSM object is - * returned on which "next" or "previous" can be called - * before passing it in again to this method, to - * get the next or previous page in the result set. - * @param {Function} errback A function to call when an - * error stanza is received, for example when it - * doesn't support message archiving. + * @throws {Error} An error is thrown if the XMPP server responds with an error. + * @returns {Promise} A promise which resolves to an object which + * will have keys `messages` and `rsm` which contains a Strophe.RSM object + * on which "next" or "previous" can be called before passing it in again + * to this method, to get the next or previous page in the result set. * * @example * // Requesting all archived messages @@ -44877,41 +44758,17 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * // * // The simplest query that can be made is to simply not pass in any parameters. * // Such a query will return all archived messages for the current user. - * // - * // Generally, you'll however always want to pass in a callback method, to receive - * // the returned messages. * - * this._converse.api.archive.query( - * (messages) => { - * // Do something with the messages, like showing them in your webpage. - * }, - * (iq) => { - * // The query was not successful, perhaps inform the user? - * // The IQ stanza returned by the XMPP server is passed in, so that you - * // may inspect it and determine what the problem was. - * } - * ) - * @example - * // Waiting until server support has been determined - * // ================================================ - * // - * // The query method will only work if Converse has been able to determine that - * // the server supports MAM queries, otherwise the following error will be raised: - * // - * // "This server does not support XEP-0313, Message Archive Management" - * // - * // The very first time Converse loads in a browser tab, if you call the query - * // API too quickly, the above error might appear because service discovery has not - * // yet been completed. - * // - * // To work solve this problem, you can first listen for the `serviceDiscovered` event, - * // through which you can be informed once support for MAM has been determined. - * - * _converse.api.listen.on('serviceDiscovered', function (feature) { - * if (feature.get('var') === converse.env.Strophe.NS.MAM) { - * _converse.api.archive.query() - * } - * }); + * let result; + * try { + * result = await _converse.api.archive.query(); + * } catch (e) { + * // The query was not successful, perhaps inform the user? + * // The IQ stanza returned by the XMPP server is passed in, so that you + * // may inspect it and determine what the problem was. + * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); * * @example * // Requesting all archived messages for a particular contact or room @@ -44922,10 +44779,20 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * // room under the `with` key. * * // For a particular user - * this._converse.api.archive.query({'with': 'john@doe.net'}, callback, errback);) + * let result; + * try { + * result = await _converse.api.archive.query({'with': 'john@doe.net'}); + * } catch (e) { + * // The query was not successful + * } * * // For a particular room - * this._converse.api.archive.query({'with': 'discuss@conference.doglovers.net'}, callback, errback);) + * let result; + * try { + * result = await _converse.api.archive.query({'with': 'discuss@conference.doglovers.net'}); + * } catch (e) { + * // The query was not successful + * } * * @example * // Requesting all archived messages before or after a certain date @@ -44940,7 +44807,12 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * 'start': '2010-06-07T00:00:00Z', * 'end': '2010-07-07T13:23:54Z' * }; - * this._converse.api.archive.query(options, callback, errback); + * let result; + * try { + * result = await _converse.api.archive.query(options); + * } catch (e) { + * // The query was not successful + * } * * @example * // Limiting the amount of messages returned @@ -44950,7 +44822,12 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * // By default, the messages are returned from oldest to newest. * * // Return maximum 10 archived messages - * this._converse.api.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback); + * let result; + * try { + * result = await _converse.api.archive.query({'with': 'john@doe.net', 'max':10}); + * } catch (e) { + * // The query was not successful + * } * * @example * // Paging forwards through a set of archived messages @@ -44960,8 +44837,8 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * // repeatedly make a further query to fetch the next batch of messages. * // * // To simplify this usecase for you, the callback method receives not only an array - * // with the returned archived messages, but also a special RSM (*Result Set - * // Management*) object which contains the query parameters you passed in, as well + * // with the returned archived messages, but also a special Strophe.RSM (*Result Set Management*) + * // object which contains the query parameters you passed in, as well * // as two utility methods `next`, and `previous`. * // * // When you call one of these utility methods on the returned RSM object, and then @@ -44969,14 +44846,24 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * // archived messages. Please note, when calling these methods, pass in an integer * // to limit your results. * - * const callback = function (messages, rsm) { - * // Do something with the messages, like showing them in your webpage. - * // ... - * // You can now use the returned "rsm" object, to fetch the next batch of messages: - * _converse.api.archive.query(rsm.next(10), callback, errback)) - * + * let result; + * try { + * result = await _converse.api.archive.query({'with': 'john@doe.net', 'max':10}); + * } catch (e) { + * // The query was not successful + * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); + * + * while (result.rsm) { + * try { + * result = await _converse.api.archive.query(rsm.next(10)); + * } catch (e) { + * // The query was not successful + * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); * } - * _converse.api.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback); * * @example * // Paging backwards through a set of archived messages @@ -44987,22 +44874,142 @@ _converse_core__WEBPACK_IMPORTED_MODULE_2__["default"].plugins.add('converse-mam * // `before` parameter. If you simply want to page backwards from the most recent * // message, pass in the `before` parameter with an empty string value `''`. * - * _converse.api.archive.query({'before': '', 'max':5}, function (message, rsm) { - * // Do something with the messages, like showing them in your webpage. - * // ... - * // You can now use the returned "rsm" object, to fetch the previous batch of messages: - * rsm.previous(5); // Call previous method, to update the object's parameters, - * // passing in a limit value of 5. - * // Now we query again, to get the previous batch. - * _converse.api.archive.query(rsm, callback, errback); + * let result; + * try { + * result = await _converse.api.archive.query({'before': '', 'max':5}); + * } catch (e) { + * // The query was not successful * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); + * + * // Now we query again, to get the previous batch. + * try { + * result = await _converse.api.archive.query(rsm.previous(5);); + * } catch (e) { + * // The query was not successful + * } + * // Do something with the messages, like showing them in your webpage. + * result.messages.forEach(m => this.showMessage(m)); + * */ - 'query': function query(options, callback, errback) { + 'query': async function query(options) { if (!_converse.api.connection.connected()) { throw new Error('Can\'t call `api.archive.query` before having established an XMPP session'); } - return queryForArchivedMessages(_converse, options, callback, errback); + const attrs = { + 'type': 'set' + }; + + if (options && options.groupchat) { + if (!options['with']) { + // eslint-disable-line dot-notation + throw new Error('You need to specify a "with" value containing ' + 'the chat room JID, when querying groupchat messages.'); + } + + attrs.to = options['with']; // eslint-disable-line dot-notation + } + + const jid = attrs.to || _converse.bare_jid; + const supported = await _converse.api.disco.supports(Strophe.NS.MAM, jid); + + if (!supported.length) { + _converse.log(`Did not fetch MAM archive for ${jid} because it doesn't support ${Strophe.NS.MAM}`); + + return { + 'messages': [] + }; + } + + const queryid = _converse.connection.getUniqueId(); + + const stanza = $iq(attrs).c('query', { + 'xmlns': Strophe.NS.MAM, + 'queryid': queryid + }); + + if (options) { + stanza.c('x', { + 'xmlns': Strophe.NS.XFORM, + 'type': 'submit' + }).c('field', { + 'var': 'FORM_TYPE', + 'type': 'hidden' + }).c('value').t(Strophe.NS.MAM).up().up(); + + if (options['with'] && !options.groupchat) { + // eslint-disable-line dot-notation + stanza.c('field', { + 'var': 'with' + }).c('value').t(options['with']).up().up(); // eslint-disable-line dot-notation + } + + ['start', 'end'].forEach(t => { + if (options[t]) { + const date = moment(options[t]); + + if (date.isValid()) { + stanza.c('field', { + 'var': t + }).c('value').t(date.format()).up().up(); + } else { + throw new TypeError(`archive.query: invalid date provided for: ${t}`); + } + } + }); + stanza.up(); + + if (options instanceof Strophe.RSM) { + stanza.cnode(options.toXML()); + } else if (_.intersection(RSM_ATTRIBUTES, Object.keys(options)).length) { + stanza.cnode(new Strophe.RSM(options).toXML()); + } + } + + const messages = []; + + const message_handler = _converse.connection.addHandler(message => { + if (options.groupchat && message.getAttribute('from') !== options['with']) { + // eslint-disable-line dot-notation + return true; + } + + const result = message.querySelector('result'); + + if (!_.isNull(result) && result.getAttribute('queryid') === queryid) { + messages.push(message); + } + + return true; + }, Strophe.NS.MAM); + + let iq; + + try { + iq = await _converse.api.sendIQ(stanza, _converse.message_archiving_timeout); + } catch (e) { + _converse.connection.deleteHandler(message_handler); + + throw e; + } + + _converse.connection.deleteHandler(message_handler); + + const set = iq.querySelector('set'); + let rsm; + + if (!_.isNull(set)) { + rsm = new Strophe.RSM({ + 'xml': set + }); + Object.assign(rsm, _.pick(options, _.concat(MAM_ATTRIBUTES, ['max']))); + } + + return { + messages, + rsm + }; } } });