Преглед на файлове

`_converse.api.archive.query` now returns a Promise

instead of accepting a callback functions.
JC Brand преди 6 години
родител
ревизия
15b2273631
променени са 6 файла, в които са добавени 612 реда и са изтрити 627 реда
  1. 1 0
      CHANGES.md
  2. 204 196
      dist/converse.js
  3. 44 80
      spec/mam.js
  4. 23 24
      src/converse-mam-views.js
  5. 153 147
      src/headless/converse-mam.js
  6. 187 180
      src/headless/dist/converse-headless.js

+ 1 - 0
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
 

+ 204 - 196
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<Object>} 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
+          };
         }
       }
     });

+ 44 - 80
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(
                     `<iq id="${IQ_id}" type="set" xmlns="jabber:client"><query queryid="${queryid}" xmlns="urn:xmpp:mam:2"/></iq>`);
@@ -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(
                     `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
@@ -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(
                     `<iq id="${IQ_id}" to="coven@chat.shakespeare.lit" type="set" xmlns="jabber:client">`+
                         `<query queryid="${queryid}" xmlns="urn:xmpp:mam:2">`+
@@ -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');
 
                 /* <message id='iasd207' from='coven@chat.shakespeare.lit' to='hag66@shakespeare.lit/pda'>
@@ -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(
                     `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
@@ -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(
                     `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
@@ -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(
                     `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
@@ -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(
                     `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
@@ -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(
                     `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
@@ -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(
                     `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
@@ -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');
 
                 /*  <message id='aeb213' to='juliet@capulet.lit/chamber'>
@@ -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()
            }));
         });

+ 23 - 24
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) {

+ 153 - 147
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<Object>} 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
+                    }
                 }
             }
         });

+ 187 - 180
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<Object>} 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
+          };
         }
       }
     });