Browse Source

Support for roster versioning

fixes #1106
JC Brand 7 years ago
parent
commit
bb95375f9c
6 changed files with 67 additions and 15 deletions
  1. 2 2
      dev.html
  2. 26 1
      spec/roster.js
  3. 1 0
      src/converse-core.js
  4. 4 5
      src/converse-disco.js
  5. 22 7
      src/converse-roster.js
  6. 12 0
      tests/mock.js

+ 2 - 2
dev.html

@@ -27,7 +27,7 @@
             //     'prosody@conference.prosody.im',
             //     'jdev@conference.jabber.org'
             // ],
-            websocket_url: 'ws://chat.example.org:5280/xmpp-websocket',
+            // websocket_url: 'ws://chat.example.org:5280/xmpp-websocket',
             view_mode: 'fullscreen',
             archived_messages_page_size: '500',
             allow_public_bookmarks: true,
@@ -35,7 +35,7 @@
                 'discuss@conference.conversejs.org'
             ],
             bosh_service_url: 'http://chat.example.org:5280/http-bind/',
-            bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes
+            // bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes
             message_archiving: 'always',
             debug: true
         });

+ 26 - 1
spec/roster.js

@@ -57,7 +57,7 @@
                     `<iq type="get" id="${stanza.getAttribute('id')}" xmlns="jabber:client">`+
                         `<query xmlns="jabber:iq:roster"/>`+
                     `</iq>`);
-                const result = $iq({
+                let result = $iq({
                     'to': _converse.connection.jid,
                     'type': 'result',
                     'id': stanza.getAttribute('id')
@@ -68,6 +68,31 @@
                   .c('item', {'jid': 'romeo@example.com'})
                 _converse.connection._dataRecv(test_utils.createRequest(result));
                 expect(_converse.roster.data.get('version')).toBe('ver7');
+                expect(_converse.roster.models.length).toBe(2);
+
+                _converse.roster.fetchFromServer();
+                stanza = _converse.connection.IQ_stanzas.pop().nodeTree;
+                expect(stanza.outerHTML).toBe(
+                    `<iq type="get" id="${stanza.getAttribute('id')}" xmlns="jabber:client">`+
+                        `<query xmlns="jabber:iq:roster" ver="ver7"/>`+
+                    `</iq>`);
+
+                result = $iq({
+                    'to': _converse.connection.jid,
+                    'type': 'result',
+                    'id': stanza.getAttribute('id')
+                });
+                _converse.connection._dataRecv(test_utils.createRequest(result));
+
+                const roster_push = $iq({
+                    'to': _converse.connection.jid,
+                    'type': 'set',
+                }).c('query', {'xmlns': 'jabber:iq:roster', 'ver': 'ver34'})
+                    .c('item', {'jid': 'romeo@example.com', 'subscription': 'remove'});
+                _converse.connection._dataRecv(test_utils.createRequest(roster_push));
+                expect(_converse.roster.data.get('version')).toBe('ver34');
+                expect(_converse.roster.models.length).toBe(1);
+                expect(_converse.roster.at(0).get('jid')).toBe('nurse@example.com');
                 done();
             });
         }));

+ 1 - 0
src/converse-core.js

@@ -649,6 +649,7 @@
             _converse.session.id = id; // Appears to be necessary for backbone.browserStorage
             _converse.session.browserStorage = new Backbone.BrowserStorage[_converse.storage](id);
             _converse.session.fetch();
+            _converse.emit('sessionInitialized');
         };
 
         this.clearSession = function () {

+ 4 - 5
src/converse-disco.js

@@ -219,12 +219,12 @@
 
             function initStreamFeatures () {
                 _converse.stream_features = new Backbone.Collection();
-                _converse.stream_features.browserStorage = new Backbone.BrowserStorage[_converse.storage](
+                _converse.stream_features.browserStorage = new Backbone.BrowserStorage.session(
                     b64_sha1(`converse.stream-features-${_converse.bare_jid}`)
                 );
                 _converse.stream_features.fetch({
                     success (collection) {
-                        if (collection.length === 0) {
+                        if (collection.length === 0 && _converse.connection.features) {
                             _.forEach(
                                 _converse.connection.features.childNodes,
                                 (feature) => {
@@ -240,11 +240,10 @@
 
             function initializeDisco () {
                 addClientFeatures();
-                initStreamFeatures();
                 _converse.connection.addHandler(onDiscoInfoRequest, Strophe.NS.DISCO_INFO, 'iq', 'get', null, null);
 
                 _converse.disco_entities = new _converse.DiscoEntities();
-                _converse.disco_entities.browserStorage = new Backbone.BrowserStorage[_converse.storage](
+                _converse.disco_entities.browserStorage = new Backbone.BrowserStorage.session(
                     b64_sha1(`converse.disco-entities-${_converse.bare_jid}`)
                 );
 
@@ -253,12 +252,12 @@
                         // If we don't have an entity for our own XMPP server,
                         // create one.
                         _converse.disco_entities.create({'jid': _converse.domain});
-                        initStreamFeatures();
                     }
                     _converse.emit('discoInitialized');
                 }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
             }
 
+            _converse.api.listen.on('sessionInitialized', initStreamFeatures);
             _converse.api.listen.on('reconnected', initializeDisco);
             _converse.api.listen.on('connected', initializeDisco);
 

+ 22 - 7
src/converse-roster.js

@@ -382,12 +382,14 @@
                      * Returns a promise which resolves once the contacts have been
                      * fetched.
                      */
+                    const that = this;
                     return new Promise((resolve, reject) => {
                         this.fetch({
                             'add': true,
                             'silent': true,
                             success (collection) {
-                                if (collection.length === 0) {
+                                if (collection.length === 0 || 
+                                        (that.rosterVersioningSupported() && !_converse.session.get('roster_fetched'))) {
                                     _converse.send_initial_presence = true;
                                     _converse.roster.fetchFromServer().then(resolve).catch(reject);
                                 } else {
@@ -530,7 +532,11 @@
                         return;
                     }
                     _converse.connection.send($iq({type: 'result', id, from: _converse.connection.jid}));
-                    const items = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"] item`, iq);
+
+                    const query = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"]`, iq).pop();
+                    this.data.save('version', query.getAttribute('ver'));
+
+                    const items = sizzle(`item`, query);
                     if (items.length > 1) {
                         _converse.log(iq, Strophe.LogLevel.ERROR);
                         throw new Error('Roster push query may not contain more than one "item" element.');
@@ -545,6 +551,10 @@
                     return;
                 },
 
+                rosterVersioningSupported () {
+                    return _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver') && this.data.get('version');
+                },
+
                 fetchFromServer () {
                     /* Fetch the roster from the XMPP server */
                     return new Promise((resolve, reject) => {
@@ -552,7 +562,9 @@
                             'type': 'get',
                             'id': _converse.connection.getUniqueId('roster')
                         }).c('query', {xmlns: Strophe.NS.ROSTER});
-
+                        if (this.rosterVersioningSupported()) {
+                            iq.attrs({'ver': this.data.get('version')});
+                        }
                         const callback = _.flow(this.onReceivedFromServer.bind(this), resolve);
                         const errback = function (iq) {
                             const errmsg = "Error while trying to fetch roster from the server";
@@ -567,10 +579,13 @@
                     /* An IQ stanza containing the roster has been received from
                      * the XMPP server.
                      */
-                    const query = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"]`, iq).pop(),
-                          items = sizzle(`item`, query);
-                    _.each(items, (item) => this.updateContact(item));
-                    this.data.save('version', query.getAttribute('ver'));
+                    const query = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"]`, iq).pop();
+                    if (query) {
+                        const items = sizzle(`item`, query);
+                        _.each(items, (item) => this.updateContact(item));
+                        this.data.save('version', query.getAttribute('ver'));
+                        _converse.session.save('roster_fetched', true);
+                    }
                     _converse.emit('roster', iq);
                 },
 

+ 12 - 0
tests/mock.js

@@ -63,6 +63,18 @@
                 this.IQ_ids.push(id);
                 return id;
             }
+            c.features = Strophe.xmlHtmlNode(
+                '<stream:features xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client">'+
+                    '<ver xmlns="urn:xmpp:features:rosterver"/>'+
+                    '<csi xmlns="urn:xmpp:csi:0"/>'+
+                    '<c xmlns="http://jabber.org/protocol/caps" ver="UwBpfJpEt3IoLYfWma/o/p3FFRo=" hash="sha-1" node="http://prosody.im"/>'+
+                    '<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">'+
+                        '<required/>'+
+                    '</bind>'+
+                    '<session xmlns="urn:ietf:params:xml:ns:xmpp-session">'+
+                        '<optional/>'+
+                    '</session>'+
+                '</stream:features>').firstChild;
 
             c._proto._connect = function () {
                 c.authenticated = true;