diff --git a/.eslintrc.json b/.eslintrc.json
index f9c59d290..62fc37723 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -18,7 +18,7 @@
"lodash/prefer-lodash-method": [2, {
"ignoreMethods": [
"find", "endsWith", "startsWith", "filter", "reduce",
- "map", "replace", "toLower", "split", "trim", "forEach"
+ "map", "replace", "toLower", "split", "trim", "forEach", "toUpperCase"
]
}],
"lodash/prefer-startswith": "off",
diff --git a/spec/disco.js b/spec/disco.js
index fa8146cec..03794b076 100644
--- a/spec/disco.js
+++ b/spec/disco.js
@@ -8,8 +8,161 @@
} (this, function (jasmine, $, converse, mock, test_utils) {
"use strict";
var Strophe = converse.env.Strophe;
+ var $iq = converse.env.$iq;
+ var _ = converse.env._;
describe("Service Discovery", function () {
+
+ describe("Whenever converse.js queries a server for its features", function () {
+ it("stores the features it receives", mock.initConverseWithAsync(function (done, _converse) {
+ var IQ_stanzas = _converse.connection.IQ_stanzas;
+ var IQ_ids = _converse.connection.IQ_ids;
+ test_utils.waitUntil(function () {
+ return _.filter(IQ_stanzas, function (iq) {
+ return iq.nodeTree.querySelector('query[xmlns="http://jabber.org/protocol/disco#info"]');
+ }).length > 0;
+ }, 300).then(function () {
+ /*
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ var info_IQ_id = IQ_ids[0];
+ var stanza = $iq({
+ 'type': 'result',
+ 'from': 'localhost',
+ 'to': 'dummy@localhost/resource',
+ 'id': info_IQ_id
+ }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
+ .c('identity', {
+ 'category': 'conference',
+ 'type': 'text',
+ 'name': 'Play-Specific Chatrooms'}).up()
+ .c('identity', {
+ 'category': 'directory',
+ 'type': 'chatroom',
+ 'name': 'Play-Specific Chatrooms'}).up()
+ .c('feature', {
+ 'var': 'http://jabber.org/protocol/disco#info'}).up()
+ .c('feature', {
+ 'var': 'http://jabber.org/protocol/disco#items'}).up()
+ .c('feature', {
+ 'var': 'jabber:iq:register'}).up()
+ .c('feature', {
+ 'var': 'jabber:iq:time'}).up()
+ .c('feature', {
+ 'var': 'jabber:iq:version'});
+ _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+ var entities = _converse.disco_entities;
+ expect(entities.length).toBe(1);
+ expect(entities.get('localhost').features.length).toBe(5);
+ expect(entities.get('localhost').features.where({'var': 'jabber:iq:version'}).length).toBe(1);
+ expect(entities.get('localhost').features.where({'var': 'jabber:iq:time'}).length).toBe(1);
+ expect(entities.get('localhost').features.where({'var': 'jabber:iq:register'}).length).toBe(1);
+ expect(entities.get('localhost').features.where(
+ {'var': 'http://jabber.org/protocol/disco#items'}).length).toBe(1);
+ expect(entities.get('localhost').features.where(
+ {'var': 'http://jabber.org/protocol/disco#info'}).length).toBe(1);
+
+
+ test_utils.waitUntil(function () {
+ // Converse.js sees that the entity has a disco#items feature,
+ // so it will make a query for it.
+ return _.filter(IQ_stanzas, function (iq) {
+ return iq.nodeTree.querySelector('query[xmlns="http://jabber.org/protocol/disco#items"]');
+ }).length > 0;
+ }, 300).then(function () {
+ /*
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ var items_IQ_id = IQ_ids.pop();
+ stanza = $iq({
+ 'type': 'result',
+ 'from': 'localhost',
+ 'to': 'dummy@localhost/resource',
+ 'id': items_IQ_id
+ }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'})
+ .c('item', {
+ 'jid': 'people.shakespeare.lit',
+ 'name': 'Directory of Characters'}).up()
+ .c('item', {
+ 'jid': 'plays.shakespeare.lit',
+ 'name': 'Play-Specific Chatrooms'}).up()
+ .c('item', {
+ 'jid': 'words.shakespeare.lit',
+ 'name': 'Gateway to Marlowe IM'}).up()
+ .c('item', {
+ 'jid': 'localhost',
+ 'name': 'Shakespearean Lexicon'}).up()
+
+ .c('item', {
+ 'jid': 'localhost',
+ 'node': 'books',
+ 'name': 'Books by and about Shakespeare'}).up()
+ .c('item', {
+ 'node': 'localhost',
+ 'name': 'Wear your literary taste with pride'}).up()
+ .c('item', {
+ 'jid': 'localhost',
+ 'node': 'music',
+ 'name': 'Music from the time of Shakespeare'
+ });
+ _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+ entities = _converse.disco_entities;
+ expect(entities.length).toBe(4);
+ expect(entities.get(_converse.domain).features.length).toBe(5);
+ expect(entities.get(_converse.domain).identities.length).toBe(2);
+ expect(entities.get(_converse.domain).identities.where({'category': 'conference'}).length).toBe(1);
+ expect(entities.get(_converse.domain).identities.where({'category': 'directory'}).length).toBe(1);
+ done();
+ });
+ });
+ }));
+ });
+
describe("Whenever converse.js discovers a new server feature", function () {
it("emits the serviceDiscovered event",
mock.initConverseWithPromises(
@@ -19,6 +172,7 @@
sinon.spy(_converse, 'emit');
_converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM});
expect(_converse.emit.called).toBe(true);
+ expect(_converse.emit.args[0][0]).toBe('serviceDiscovered');
expect(_converse.emit.args[0][1].get('var')).toBe(Strophe.NS.MAM);
done();
}));
diff --git a/spec/mam.js b/spec/mam.js
index 240f45635..d0e916f1c 100644
--- a/spec/mam.js
+++ b/spec/mam.js
@@ -381,7 +381,7 @@
'var': Strophe.NS.MAM
});
spyOn(feature, 'save').and.callFake(feature.set); // Save will complain about a url not being set
- _converse.disco_entities.get(_converse.domain).features.onFeatureAdded(feature);
+ _converse.disco_entities.get(_converse.domain).onFeatureAdded(feature);
expect(_converse.connection.sendIQ).toHaveBeenCalled();
expect(sent_stanza.toLocaleString()).toBe(
diff --git a/src/converse-disco.js b/src/converse-disco.js
index 645a30a46..16d9b0556 100644
--- a/src/converse-disco.js
+++ b/src/converse-disco.js
@@ -22,19 +22,90 @@
*/
const { _converse } = this;
+ function onDiscoItems (stanza) {
+ _.each(stanza.querySelectorAll('query item'), (item) => {
+ if (item.getAttribute("node")) {
+ // XXX: ignore nodes for now.
+ // See: https://xmpp.org/extensions/xep-0030.html#items-nodes
+ return;
+ }
+ const jid = item.getAttribute('jid');
+ const entities = _converse.disco_entities;
+ if (_.isUndefined(entities.get(jid))) {
+ entities.create({'jid': jid});
+ }
+ });
+ }
+
// Promises exposed by this plugin
_converse.api.promises.add('discoInitialized');
_converse.DiscoEntity = Backbone.Model.extend({
/* A Disco Entity is a JID addressable entity that can be queried
* for features.
+ *
* See XEP-0030: https://xmpp.org/extensions/xep-0030.html
*/
- initialize (settings) {
- if (_.isNil(settings.jid)) {
- throw new Error('DiscoEntity must be instantiated with a JID');
+ idAttribute: 'jid',
+
+ initialize () {
+ this.features = new Backbone.Collection();
+ this.features.browserStorage = new Backbone.BrowserStorage[_converse.storage](
+ b64_sha1(`converse.features-${this.get('jid')}`)
+ );
+ this.features.on('add', this.onFeatureAdded);
+
+ this.identities = new Backbone.Collection();
+ this.identities.browserStorage = new Backbone.BrowserStorage[_converse.storage](
+ b64_sha1(`converse.identities-${this.get('jid')}`)
+ );
+ this.fetchFeatures();
+ },
+
+ onFeatureAdded (feature) {
+ _converse.emit('serviceDiscovered', feature);
+ },
+
+ fetchFeatures () {
+ if (this.features.browserStorage.records.length === 0) {
+ this.queryInfo();
+ } else {
+ this.features.fetch({add: true});
+ this.identities.fetch({add: true});
}
- this.features = new _converse.Features({'jid': settings.jid});
+ },
+
+ queryInfo () {
+ _converse.connection.disco.info(this.get('jid'), null, this.onInfo.bind(this));
+ },
+
+ queryForItems () {
+ if (_.isEmpty(this.identities.where({'category': 'server'})) &&
+ _.isEmpty(this.identities.where({'category': 'conference'}))) {
+ // Don't fetch features and items if this is not a
+ // server or a conference component.
+ return;
+ }
+ _converse.connection.disco.items(this.get('jid'), null, onDiscoItems);
+ },
+
+ onInfo (stanza) {
+ _.forEach(stanza.querySelectorAll('identity'), (identity) => {
+ this.identities.create({
+ 'category': identity.getAttribute('category'),
+ 'type': stanza.getAttribute('type'),
+ 'name': stanza.getAttribute('name')
+ });
+ });
+ if (stanza.querySelector('feature[var="'+Strophe.NS.DISCO_ITEMS+'"]')) {
+ this.queryForItems();
+ }
+ _.forEach(stanza.querySelectorAll('feature'), (feature) => {
+ this.features.create({
+ 'var': feature.getAttribute('var'),
+ 'from': stanza.getAttribute('from')
+ });
+ });
}
});
@@ -56,14 +127,8 @@
this.fetch({
add: true,
success: function (collection) {
- if (collection.length === 0) {
- /* The sessionStorage is empty */
- // TODO: check for domain in collection even if
- // not empty
- this.create({
- 'id': _converse.domain,
- 'jid': _converse.domain
- });
+ if (collection.length === 0 || !collection.get(_converse.domain)) {
+ this.create({'jid': _converse.domain});
}
resolve();
}.bind(this),
@@ -75,95 +140,29 @@
}
});
- _converse.Features = Backbone.Collection.extend({
- /* Service Discovery
- * -----------------
- * This collection stores Feature Models, representing features
- * provided by available XMPP entities (e.g. servers)
- * See XEP-0030 for more details: http://xmpp.org/extensions/xep-0030.html
- * All features are shown here: http://xmpp.org/registrar/disco-features.html
- */
- model: Backbone.Model,
+ function addClientFeatures () {
+ /* The strophe.disco.js plugin keeps a list of features which
+ * it will advertise to any #info queries made to it.
+ *
+ * See: http://xmpp.org/extensions/xep-0030.html#info
+ */
- initialize (settings) {
- const jid = settings.jid;
- if (_.isNil(jid)) {
- throw new Error('DiscoEntity must be instantiated with a JID');
- }
- this.addClientIdentities().addClientFeatures();
- this.browserStorage = new Backbone.BrowserStorage[_converse.storage](
- b64_sha1(`converse.features-${jid}`)
- );
- this.on('add', this.onFeatureAdded, this);
- this.fetchFeatures(jid);
- },
+ // See http://xmpp.org/registrar/disco-categories.html
+ _converse.connection.disco.addIdentity('client', 'web', 'Converse.js');
- fetchFeatures (jid) {
- if (this.browserStorage.records.length === 0) {
- // browserStorage is empty, so we've likely never queried this
- // domain for features yet
- _converse.connection.disco.info(jid, null, this.onInfo.bind(this));
- _converse.connection.disco.items(jid, null, this.onItems.bind(this));
- } else {
- this.fetch({add:true});
- }
- },
-
- onFeatureAdded (feature) {
- _converse.emit('serviceDiscovered', feature);
- },
-
- addClientIdentities () {
- /* See http://xmpp.org/registrar/disco-categories.html
- */
- _converse.connection.disco.addIdentity('client', 'web', 'Converse.js');
- return this;
- },
-
- addClientFeatures () {
- /* The strophe.disco.js plugin keeps a list of features which
- * it will advertise to any #info queries made to it.
- *
- * See: http://xmpp.org/extensions/xep-0030.html#info
- */
- _converse.connection.disco.addFeature(Strophe.NS.BOSH);
- _converse.connection.disco.addFeature(Strophe.NS.CHATSTATES);
- _converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO);
- _converse.connection.disco.addFeature(Strophe.NS.ROSTERX); // Limited support
- if (_converse.message_carbons) {
- _converse.connection.disco.addFeature(Strophe.NS.CARBONS);
- }
- _converse.emit('addClientFeatures');
- return this;
- },
-
- onItems (stanza) {
- _.each(stanza.querySelectorAll('query item'), (item) => {
- _converse.connection.disco.info(
- item.getAttribute('jid'),
- null,
- this.onInfo.bind(this));
- });
- },
-
- onInfo (stanza) {
- if ((sizzle('identity[category=server][type=im]', stanza).length === 0) &&
- (sizzle('identity[category=conference][type=text]', stanza).length === 0)) {
- // This isn't an IM server component
- return;
- }
- _.forEach(stanza.querySelectorAll('feature'), (feature) => {
- const namespace = feature.getAttribute('var');
- this[namespace] = true;
- this.create({
- 'var': namespace,
- 'from': stanza.getAttribute('from')
- });
- });
+ _converse.connection.disco.addFeature(Strophe.NS.BOSH);
+ _converse.connection.disco.addFeature(Strophe.NS.CHATSTATES);
+ _converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO);
+ _converse.connection.disco.addFeature(Strophe.NS.ROSTERX); // Limited support
+ if (_converse.message_carbons) {
+ _converse.connection.disco.addFeature(Strophe.NS.CARBONS);
}
- });
+ _converse.emit('addClientFeatures');
+ return this;
+ }
function initializeDisco () {
+ addClientFeatures();
_converse.disco_entities = new _converse.DiscoEntities();
}
_converse.api.listen.on('reconnected', initializeDisco);
diff --git a/tests/mock.js b/tests/mock.js
index 13b2f5253..5d12cb51f 100644
--- a/tests/mock.js
+++ b/tests/mock.js
@@ -49,6 +49,17 @@
return function () {
Strophe.Bosh.prototype._processRequest = function () {}; // Don't attempt to send out stanzas
var c = new Strophe.Connection('jasmine tests');
+ var sendIQ = c.sendIQ;
+
+ c.IQ_stanzas = [];
+ c.IQ_ids = [];
+ c.sendIQ = function (iq, callback, errback) {
+ this.IQ_stanzas.push(iq);
+ var id = sendIQ.bind(this)(iq, callback, errback);
+ this.IQ_ids.push(id);
+ return id;
+ }
+
c.vcard = {
'get': function (callback, jid) {
var fullname;
@@ -111,18 +122,15 @@
return function (done) {
var _converse = initConverse(settings, spies);
var promises = _.map(promise_names, _converse.api.waitUntil);
- Promise.all(promises).then(_.partial(func, done, _converse));
+ Promise.all(promises)
+ .then(_.partial(func, done, _converse))
+ .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
}
};
mock.initConverseWithConnectionSpies = function (spies, settings, func) {
- if (_.isFunction(settings)) {
- var _func = settings;
- settings = func;
- func = _func;
- }
- return function () {
- return func(initConverse(settings, spies));
+ return function (done) {
+ return func(done, initConverse(settings, spies));
};
};