Browse Source

New config option `enable_roster_versioning`

JC Brand 2 months ago
parent
commit
e72825e2af

+ 2 - 1
CHANGES.md

@@ -77,10 +77,11 @@
 - Add new themes 'Cyberpunk' and 'Nordic' and remove the old 'Concord' theme.
 - Add new themes 'Cyberpunk' and 'Nordic' and remove the old 'Concord' theme.
 - Improved accessibility.
 - Improved accessibility.
 - New "getOccupantActionButtons" hook, so that plugins can add actions on MUC occupants.
 - New "getOccupantActionButtons" hook, so that plugins can add actions on MUC occupants.
+- Update the "Add MUC" modal to add validation and to allow specifying only the MUC name and not the whole address.
 - MUC occupants badges: displays short labels, with full label as title.
 - MUC occupants badges: displays short labels, with full label as title.
 - New config option [stanza_timeout](https://conversejs.org/docs/html/configuration.html#show-background)
 - New config option [stanza_timeout](https://conversejs.org/docs/html/configuration.html#show-background)
 - New config option [lazy_load_vcards](https://conversejs.org/docs/html/configuration.html#lazy-load-vcards)
 - New config option [lazy_load_vcards](https://conversejs.org/docs/html/configuration.html#lazy-load-vcards)
-- Update the "Add MUC" modal to add validation and to allow specifying only the MUC name and not the whole address.
+- New config option [enable_roster_versioning](https://conversejs.org/docs/html/configuration.html#enable-roster-versioning)
 
 
 ### Default config changes
 ### Default config changes
 - Make `fullscreen` the default `view_mode`.
 - Make `fullscreen` the default `view_mode`.

+ 20 - 0
docs/source/configuration.rst

@@ -920,6 +920,16 @@ The app servers are specified with the `push_app_servers`_ option.
     Registering a push app server against a MUC domain is not (yet) standardized
     Registering a push app server against a MUC domain is not (yet) standardized
     and this feature should be considered experimental.
     and this feature should be considered experimental.
 
 
+
+enable_roster_versioning
+------------------------
+
+* Default: ``true``
+
+Determines support for `roster versioning <https://xmpp.org/rfcs/rfc6121.html#roster-versioning>`_.
+If set to ``false``, the full roster will always be fetched.
+
+
 enable_smacks
 enable_smacks
 -------------
 -------------
 
 
@@ -2097,6 +2107,16 @@ everywhere.
 This warning isn't applicable to all deployments of Converse and can therefore
 This warning isn't applicable to all deployments of Converse and can therefore
 be turned off by setting this config variable to ``false``.
 be turned off by setting this config variable to ``false``.
 
 
+
+show_self_in_roster
+-------------------
+
+* Default: ``true``
+
+If true, you'll see yourself in your list of contacts (aka the "roster") and
+can open a chat with yourself.
+
+
 show_send_button
 show_send_button
 ----------------
 ----------------
 
 

+ 8 - 4
src/headless/plugins/roster/contacts.js

@@ -278,15 +278,19 @@ class RosterContacts extends Collection {
         /**
         /**
          * When the roster receives a push event from server (i.e. new entry in your contacts roster).
          * When the roster receives a push event from server (i.e. new entry in your contacts roster).
          * @event _converse#rosterPush
          * @event _converse#rosterPush
-         * @type { Element }
+         * @type {Element}
          * @example _converse.api.listen.on('rosterPush', iq => { ... });
          * @example _converse.api.listen.on('rosterPush', iq => { ... });
          */
          */
         api.trigger('rosterPush', iq);
         api.trigger('rosterPush', iq);
         return;
         return;
     }
     }
 
 
-    rosterVersioningSupported() {
-        return api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver') && this.data.get('version');
+    shouldUseRosterVersioning() {
+        return (
+            api.settings.get('enable_roster_versioning') &&
+            this.data.get('version') &&
+            api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver')
+        );
     }
     }
 
 
     /**
     /**
@@ -298,7 +302,7 @@ class RosterContacts extends Collection {
         const stanza = stx`
         const stanza = stx`
             <iq type="get" id="${u.getUniqueId('roster')}" xmlns="jabber:client">
             <iq type="get" id="${u.getUniqueId('roster')}" xmlns="jabber:client">
                 <query xmlns="${Strophe.NS.ROSTER}"
                 <query xmlns="${Strophe.NS.ROSTER}"
-                    ${this.rosterVersioningSupported() ? Stanza.unsafeXML(`ver="${this.data.get('version')}"`) : ''}>
+                    ${this.shouldUseRosterVersioning() ? Stanza.unsafeXML(`ver="${this.data.get('version')}"`) : ''}>
                 </query>
                 </query>
             </iq>`;
             </iq>`;
 
 

+ 2 - 1
src/headless/plugins/roster/plugin.js

@@ -26,9 +26,10 @@ converse.plugins.add('converse-roster', {
 
 
     initialize () {
     initialize () {
         api.settings.extend({
         api.settings.extend({
-            show_self_in_roster: true,
             allow_contact_requests: true,
             allow_contact_requests: true,
             auto_subscribe: false,
             auto_subscribe: false,
+            enable_roster_versioning: true,
+            show_self_in_roster: true,
             synchronize_availability: true
             synchronize_availability: true
         });
         });
 
 

+ 1 - 1
src/headless/types/plugins/roster/contacts.d.ts

@@ -64,7 +64,7 @@ declare class RosterContacts extends Collection {
      * @param {Element} iq - The IQ stanza received from the XMPP server.
      * @param {Element} iq - The IQ stanza received from the XMPP server.
      */
      */
     onRosterPush(iq: Element): void;
     onRosterPush(iq: Element): void;
-    rosterVersioningSupported(): any;
+    shouldUseRosterVersioning(): any;
     /**
     /**
      * Fetches the roster from the XMPP server and updates the local state
      * Fetches the roster from the XMPP server and updates the local state
      * @emits _converse#roster
      * @emits _converse#roster

+ 90 - 31
src/plugins/rosterview/tests/roster.js

@@ -65,7 +65,7 @@ describe("The Contacts Roster", function () {
     it("is populated once we have registered a presence handler", mock.initConverse([], {}, async function (_converse) {
     it("is populated once we have registered a presence handler", mock.initConverse([], {}, async function (_converse) {
         const IQs = _converse.api.connection.get().IQ_stanzas;
         const IQs = _converse.api.connection.get().IQ_stanzas;
         const stanza = await u.waitUntil(
         const stanza = await u.waitUntil(
-            () => IQs.filter(iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop());
+            () => IQs.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
 
 
         expect(stanza).toEqualStanza(
         expect(stanza).toEqualStanza(
             stx`<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">
             stx`<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">
@@ -83,15 +83,20 @@ describe("The Contacts Roster", function () {
     }));
     }));
 
 
     it("supports roster versioning", mock.initConverse([], {}, async function (_converse) {
     it("supports roster versioning", mock.initConverse([], {}, async function (_converse) {
-        const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
+        const { IQ_stanzas } = _converse.api.connection.get();
         let stanza = await u.waitUntil(
         let stanza = await u.waitUntil(
-            () => IQ_stanzas.filter(iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop()
-        );
-        expect(_converse.roster.data.get('version')).toBeUndefined();
-        expect(Strophe.serialize(stanza)).toBe(
-            `<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
-                `<query xmlns="jabber:iq:roster"/>`+
-            `</iq>`);
+            () => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
+
+        const { roster } = _converse.state;
+
+        expect(roster.data.get('version')).toBeUndefined();
+        expect(stanza).toEqualStanza(stx`
+            <iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">
+                <query xmlns="jabber:iq:roster"/>
+            </iq>`);
+
+        while (IQ_stanzas.length) IQ_stanzas.pop();
+
         let result = stx`
         let result = stx`
             <iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
             <iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
                 <query xmlns="jabber:iq:roster" ver="ver7">
                 <query xmlns="jabber:iq:roster" ver="ver7">
@@ -101,16 +106,17 @@ describe("The Contacts Roster", function () {
             </iq>`;
             </iq>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(result));
         _converse.api.connection.get()._dataRecv(mock.createRequest(result));
 
 
-        await u.waitUntil(() => _converse.roster.models.length > 1);
-        expect(_converse.roster.data.get('version')).toBe('ver7');
-        expect(_converse.roster.models.length).toBe(2);
+        await u.waitUntil(() => roster.models.length > 1);
+        expect(roster.data.get('version')).toBe('ver7');
+        expect(roster.models.length).toBe(2);
 
 
-        _converse.roster.fetchFromServer();
-        stanza = _converse.api.connection.get().IQ_stanzas.pop();
-        expect(Strophe.serialize(stanza)).toBe(
-            `<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">`+
-                `<query ver="ver7" xmlns="jabber:iq:roster"/>`+
-            `</iq>`);
+        roster.fetchFromServer();
+        stanza = await u.waitUntil(
+            () => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
+        expect(stanza).toEqualStanza(
+            stx`<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">
+                <query ver="ver7" xmlns="jabber:iq:roster"/>
+            </iq>`);
 
 
         result = stx`
         result = stx`
             <iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
             <iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
@@ -124,16 +130,68 @@ describe("The Contacts Roster", function () {
                 </query>
                 </query>
             </iq>`;
             </iq>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(roster_push));
         _converse.api.connection.get()._dataRecv(mock.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');
+        expect(roster.data.get('version')).toBe('ver34');
+        expect(roster.models.length).toBe(1);
+        expect(roster.at(0).get('jid')).toBe('nurse@example.com');
+    }));
+
+    it("can ignore roster versioning", mock.initConverse([], { enable_roster_versioning: false }, async function (_converse) {
+        const { IQ_stanzas } = _converse.api.connection.get();
+        let stanza = await u.waitUntil(
+            () => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
+
+        const { roster } = _converse.state;
+
+        expect(roster.data.get('version')).toBeUndefined();
+        expect(stanza).toEqualStanza(stx`
+            <iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">
+                <query xmlns="jabber:iq:roster"/>
+            </iq>`);
+
+        while (IQ_stanzas.length) IQ_stanzas.pop();
+
+        let result = stx`
+            <iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
+                <query xmlns="jabber:iq:roster" ver="ver7">
+                    <item jid="nurse@example.com"/>
+                    <item jid="romeo@example.com"/>
+                </query>
+            </iq>`;
+        _converse.api.connection.get()._dataRecv(mock.createRequest(result));
+
+        await u.waitUntil(() => roster.models.length > 1);
+        expect(roster.data.get('version')).toBe('ver7');
+        expect(roster.models.length).toBe(2);
+
+        roster.fetchFromServer();
+        stanza = await u.waitUntil(
+            () => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
+        expect(stanza).toEqualStanza(
+            stx`<iq id="${stanza.getAttribute('id')}" type="get" xmlns="jabber:client">
+                <query xmlns="jabber:iq:roster"/>
+            </iq>`);
+
+        result = stx`
+            <iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
+                <query xmlns="jabber:iq:roster" ver="ver8">
+                    <item jid="nurse@example.com"/>
+                    <item jid='romeo@example.com' subscription='remove'/>
+                </query>
+            </iq>`;
+        _converse.api.connection.get()._dataRecv(mock.createRequest(result));
+
+        await u.waitUntil(() => roster.data.get('version') === 'ver8');
+        expect(roster.models.length).toBe(1);
+        expect(roster.at(0).get('jid')).toBe('nurse@example.com');
     }));
     }));
 
 
     it("also contains contacts with subscription of none", mock.initConverse(
     it("also contains contacts with subscription of none", mock.initConverse(
         [], {}, async function (_converse) {
         [], {}, async function (_converse) {
 
 
-        const sent_IQs = _converse.api.connection.get().IQ_stanzas;
-        const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop());
+        const { IQ_stanzas } = _converse.api.connection.get();
+        let stanza = await u.waitUntil(
+            () => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
+
         _converse.api.connection.get()._dataRecv(mock.createRequest(stx`
         _converse.api.connection.get()._dataRecv(mock.createRequest(stx`
             <iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
             <iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
                 <query xmlns="jabber:iq:roster">
                 <query xmlns="jabber:iq:roster">
@@ -150,7 +208,7 @@ describe("The Contacts Roster", function () {
             </iq>
             </iq>
         `));
         `));
 
 
-        while (sent_IQs.length) sent_IQs.pop();
+        while (IQ_stanzas.length) IQ_stanzas.pop();
 
 
         await u.waitUntil(() => _converse.roster.length === 3);
         await u.waitUntil(() => _converse.roster.length === 3);
         expect(_converse.roster.pluck('jid')).toEqual(['juliet@example.net', 'mercutio@example.net', 'lord.capulet@example.net']);
         expect(_converse.roster.pluck('jid')).toEqual(['juliet@example.net', 'mercutio@example.net', 'lord.capulet@example.net']);
@@ -160,9 +218,9 @@ describe("The Contacts Roster", function () {
     it("can be refreshed if loglevel is set to debug", mock.initConverse(
     it("can be refreshed if loglevel is set to debug", mock.initConverse(
         [], {loglevel: 'debug'}, async function (_converse) {
         [], {loglevel: 'debug'}, async function (_converse) {
 
 
-        const sent_IQs = _converse.api.connection.get().IQ_stanzas;
+        const { IQ_stanzas } = _converse.api.connection.get();
         let stanza = await u.waitUntil(
         let stanza = await u.waitUntil(
-            () => sent_IQs.filter(iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop());
+            () => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
 
 
         _converse.api.connection.get()._dataRecv(mock.createRequest(stx`
         _converse.api.connection.get()._dataRecv(mock.createRequest(stx`
             <iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
             <iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
@@ -177,7 +235,7 @@ describe("The Contacts Roster", function () {
             </iq>
             </iq>
         `));
         `));
 
 
-        while (sent_IQs.length) sent_IQs.pop();
+        while (IQ_stanzas.length) IQ_stanzas.pop();
 
 
         await u.waitUntil(() => _converse.roster.length === 2);
         await u.waitUntil(() => _converse.roster.length === 2);
         expect(_converse.roster.pluck('jid')).toEqual(['juliet@example.net', 'mercutio@example.net']);
         expect(_converse.roster.pluck('jid')).toEqual(['juliet@example.net', 'mercutio@example.net']);
@@ -192,8 +250,7 @@ describe("The Contacts Roster", function () {
         sync_button.click();
         sync_button.click();
 
 
         stanza = await u.waitUntil(
         stanza = await u.waitUntil(
-            () => sent_IQs.filter(iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop()
-        );
+            () => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
 
 
         _converse.api.connection.get()._dataRecv(mock.createRequest(stx`
         _converse.api.connection.get()._dataRecv(mock.createRequest(stx`
             <iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
             <iq to="${_converse.api.connection.get().jid}" type="result" id="${stanza.getAttribute('id')}" xmlns="jabber:client">
@@ -1292,8 +1349,10 @@ describe("The Contacts Roster", function () {
 
 
             await mock.openControlBox(_converse);
             await mock.openControlBox(_converse);
 
 
-            const sent_IQs = _converse.api.connection.get().IQ_stanzas;
-            const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop());
+            const { IQ_stanzas } = _converse.api.connection.get();
+            const stanza = await u.waitUntil(
+                () => IQ_stanzas.filter(iq => sizzle('iq query[xmlns="jabber:iq:roster"]', iq).length).pop());
+
             // Taken from the spec
             // Taken from the spec
             // https://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3
             // https://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3
             const result = stx`
             const result = stx`