JC Brand 2 months ago
parent
commit
9f8cbacdac

+ 57 - 54
src/headless/plugins/caps/tests/caps.js

@@ -1,68 +1,71 @@
 /*global mock */
 
-
 const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
 
-describe("A sent presence stanza", function () {
-
+describe('A sent presence stanza', function () {
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
     beforeEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000));
     afterEach(() => (jasmine.DEFAULT_TIMEOUT_INTERVAL = original_timeout));
 
-    it("includes an entity capabilities node",
-            mock.initConverse([], {}, async (_converse) => {
-
-        await mock.waitForRoster(_converse, 'current', 0);
-        _converse.api.disco.own.identities.clear();
-        _converse.api.disco.own.features.clear();
+    it(
+        'includes an entity capabilities node',
+        mock.initConverse([], {}, async (_converse) => {
+            await mock.waitForRoster(_converse, 'current', 0);
+            _converse.api.disco.own.identities.clear();
+            _converse.api.disco.own.features.clear();
 
-        _converse.api.disco.own.identities.add("client", "pc", "Exodus 0.9.1");
-        _converse.api.disco.own.features.add("http://jabber.org/protocol/caps");
-        _converse.api.disco.own.features.add("http://jabber.org/protocol/disco#info");
-        _converse.api.disco.own.features.add("http://jabber.org/protocol/disco#items");
-        _converse.api.disco.own.features.add("http://jabber.org/protocol/muc");
+            _converse.api.disco.own.identities.add('client', 'pc', 'Exodus 0.9.1');
+            _converse.api.disco.own.features.add('http://jabber.org/protocol/caps');
+            _converse.api.disco.own.features.add('http://jabber.org/protocol/disco#info');
+            _converse.api.disco.own.features.add('http://jabber.org/protocol/disco#items');
+            _converse.api.disco.own.features.add('http://jabber.org/protocol/muc');
 
-        const { profile } = _converse.state;
+            const { profile } = _converse.state;
 
-        const presence = await profile.constructPresence();
-        expect(presence.toLocaleString()).toBe(
-            `<presence xmlns="jabber:client">`+
-                `<priority>0</priority>`+
-                `<c hash="sha-1" node="https://conversejs.org" ver="QgayPKawpkPSDYmwT/WM94uAlu0=" xmlns="http://jabber.org/protocol/caps"/>`+
-            `</presence>`)
-    }));
+            const presence = await profile.constructPresence();
+            expect(presence.node).toEqualStanza(stx`
+            <presence xmlns="jabber:client">
+                <priority>0</priority>
+                <c hash="sha-1"
+                    node="https://conversejs.org"
+                    ver="QgayPKawpkPSDYmwT/WM94uAlu0="
+                    xmlns="http://jabber.org/protocol/caps"/>
+            </presence>`);
+        })
+    );
 
-    it("has a given priority", mock.initConverse(['statusInitialized'], {}, async (_converse) => {
-        const { api } = _converse;
-        const { profile } = _converse.state;
-        let pres = await profile.constructPresence('online', null, 'Hello world');
-        expect(pres.toLocaleString()).toBe(
-            `<presence xmlns="jabber:client">`+
-                `<status>Hello world</status>`+
-                `<priority>0</priority>`+
-                `<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
-            `</presence>`
-        );
+    it(
+        'has a given priority',
+        mock.initConverse(['statusInitialized'], {}, async (_converse) => {
+            const { api } = _converse;
+            const { profile } = _converse.state;
+            let pres = await profile.constructPresence('online', null, 'Hello world');
+            expect(pres.node).toEqualStanza(stx`
+            <presence xmlns="jabber:client">
+                <status>Hello world</status>
+                <priority>0</priority>
+                <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+            </presence>`);
 
-        api.settings.set('priority', 2);
-        pres = await profile.constructPresence('away', null, 'Going jogging');
-        expect(pres.toLocaleString()).toBe(
-            `<presence xmlns="jabber:client">`+
-                `<show>away</show>`+
-                `<status>Going jogging</status>`+
-                `<priority>2</priority>`+
-                `<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
-            `</presence>`
-        );
+            api.settings.set('priority', 2);
+            pres = await profile.constructPresence('away', null, 'Going jogging');
+            expect(pres.node).toEqualStanza(stx`
+            <presence xmlns="jabber:client">
+                <show>away</show>
+                <status>Going jogging</status>
+                <priority>2</priority>
+                <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+            </presence>`);
 
-        api.settings.set('priority', undefined);
-        pres = await profile.constructPresence('dnd', null, 'Doing taxes');
-        expect(pres.toLocaleString()).toBe(
-            `<presence xmlns="jabber:client">`+
-                `<show>dnd</show>`+
-                `<status>Doing taxes</status>`+
-                `<priority>0</priority>`+
-                `<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
-            `</presence>`
-        );
-    }));
+            api.settings.set('priority', undefined);
+            pres = await profile.constructPresence('dnd', null, 'Doing taxes');
+            expect(pres.node).toEqualStanza(stx`
+            <presence xmlns="jabber:client">
+                <show>dnd</show>
+                <status>Doing taxes</status>
+                <priority>0</priority>
+                <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+            </presence>`);
+        })
+    );
 });

+ 27 - 42
src/headless/plugins/status/profile.js

@@ -6,7 +6,7 @@ import ModelWithVCard from '../../shared/model-with-vcard';
 import ColorAwareModel from '../../shared/color.js';
 import { isIdle, getIdleSeconds } from './utils.js';
 
-const { Strophe, $pres } = converse.env;
+const { Stanza, Strophe, stx } = converse.env;
 
 export default class Profile extends ModelWithVCard(ColorAwareModel(Model)) {
     defaults() {
@@ -69,58 +69,43 @@ export default class Profile extends ModelWithVCard(ColorAwareModel(Model)) {
         return this.vcard?.get('nickname') || api.settings.get('nickname');
     }
 
-    /** Constructs a presence stanza
-     * @param {string} [type]
-     * @param {string} [to] - The JID to which this presence should be sent
-     * @param {string} [status_message]
+    /**
+     * Constructs a presence stanza
+     * @param {import('./types').presence_attrs} [attrs={}]
      */
-    async constructPresence(type, to = null, status_message) {
-        type = typeof type === 'string' ? type : this.get('status') || api.settings.get('default_state');
-        status_message = typeof status_message === 'string' ? status_message : this.get('status_message');
-
-        let presence;
-
-        if (type === 'subscribe') {
-            presence = $pres({ to, type });
-            const { profile } = _converse.state;
-            const nick = profile.getNickname();
-            if (nick) presence.c('nick', { 'xmlns': Strophe.NS.NICK }).t(nick).up();
-        } else if (
-            type === 'unavailable' ||
-            type === 'probe' ||
-            type === 'error' ||
-            type === 'unsubscribe' ||
-            type === 'unsubscribed' ||
-            type === 'subscribed'
-        ) {
-            presence = $pres({ to, type });
-        } else if (type === 'offline') {
-            presence = $pres({ to, type: 'unavailable' });
-        } else if (type === 'online') {
-            presence = $pres({ to });
-        } else {
-            presence = $pres({ to }).c('show').t(type).up();
-        }
-
-        if (status_message) presence.c('status').t(status_message).up();
+    async constructPresence(attrs = {}) {
+        debugger;
+        const type =
+            typeof attrs.type === 'string' ? attrs.type : this.get('status') || api.settings.get('default_state');
+        const status = typeof attrs.status === 'string' ? attrs.status : this.get('status_message');
+        const include_nick = status === 'subscribe';
+        const { show, to } = attrs;
 
+        const { profile } = _converse.state;
+        const nick = include_nick ? profile.getNickname() : null;
         const priority = api.settings.get('priority');
-        presence
-            .c('priority')
-            .t(Number.isNaN(Number(priority)) ? 0 : priority)
-            .up();
 
+        let idle_since;
         if (isIdle()) {
-            const idle_since = new Date();
+            idle_since = new Date();
             idle_since.setSeconds(idle_since.getSeconds() - getIdleSeconds());
-            presence.c('idle', { xmlns: Strophe.NS.IDLE, since: idle_since.toISOString() });
         }
 
+        const presence = stx`
+            <presence ${to ? Stanza.unsafeXML(`to="${Strophe.xmlescape(to)}"`) : ''}
+                    ${type ? Stanza.unsafeXML(`type="${Strophe.xmlescape(type)}"`) : ''}
+                    xmlns="jabber:client">
+                ${nick ? stx`<nick xmlns="${Strophe.NS.NICK}">${nick}</nick>` : ''}
+                ${show ? stx`<show>${show}</show>` : ''}
+                ${status ? stx`<status>${status}</status>` : ''}
+                <priority>${Number.isNaN(Number(priority)) ? 0 : priority}</priority>
+                ${idle_since ? stx`<idle xmlns="${Strophe.NS.IDLE}" since="${idle_since.toISOString()}"></idle>` : ''}
+            </presence>`;
+
         /**
          * *Hook* which allows plugins to modify a presence stanza
          * @event _converse#constructedPresence
          */
-        presence = await api.hook('constructedPresence', null, presence);
-        return presence;
+        return await api.hook('constructedPresence', null, presence);
     }
 }

+ 17 - 0
src/headless/plugins/status/types.ts

@@ -0,0 +1,17 @@
+
+export type presence_attrs = {
+    type?: presence_type
+    to?: string
+    status?: string
+    show?: string
+}
+
+export type presence_type =
+    | 'error'
+    | 'offline'
+    | 'online'
+    | 'probe'
+    | 'subscribe'
+    | 'unavailable'
+    | 'unsubscribe'
+    | 'unsubscribed';

+ 3 - 0
src/headless/plugins/status/utils.js

@@ -7,6 +7,9 @@ import { ACTIVE, INACTIVE } from '../../shared/constants.js';
 
 const { Strophe, $build } = converse.env;
 
+/**
+ * @param {boolean} reconnecting
+ */
 function onStatusInitialized(reconnecting) {
     /**
      * Triggered when the user's own chat status has been initialized.

+ 2 - 2
src/headless/shared/api/presence.js

@@ -21,7 +21,7 @@ export default {
         /**
          * Send out a presence stanza
          * @method _converse.api.user.presence.send
-         * @param {String} [type]
+         * @param {import('../../plugins/status/types').presence_type} [type]
          * @param {String} [to]
          * @param {String} [status] - An optional status message
          * @param {Array<Element>|Array<Builder>|Element|Builder} [nodes]
@@ -36,7 +36,7 @@ export default {
             }
 
             const model = /** @type {Profile} */(_converse.state.profile);
-            const presence = await model.constructPresence(type, to, status);
+            const presence = await model.constructPresence({ type, to, status });
             children.map(c => c?.tree() ?? c).forEach(c => presence.cnode(c).up());
             send(presence);
 

+ 57 - 49
src/plugins/muc-views/tests/probes.js

@@ -1,77 +1,85 @@
 /*global mock, converse */
 
-const { Strophe, stx, u }  = converse.env;
+const { Strophe, stx, u } = converse.env;
 
-describe("Groupchats", function () {
-    describe("when muc_send_probes is true", function () {
+describe('Groupchats', function () {
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
 
-        it("sends presence probes when muc_send_probes is true",
-                mock.initConverse([], {'muc_send_probes': true}, async function (_converse) {
+    describe('when muc_send_probes is true', function () {
+        it(
+            'sends presence probes when muc_send_probes is true',
+            mock.initConverse([], { 'muc_send_probes': true }, async function (_converse) {
+                const muc_jid = 'lounge@montague.lit';
+                await mock.openAndEnterMUC(_converse, muc_jid, 'romeo');
 
-            const muc_jid = 'lounge@montague.lit';
-            await mock.openAndEnterMUC(_converse, muc_jid, 'romeo');
-
-            let stanza = stx`<message xmlns="jabber:client" to="${_converse.jid}" type="groupchat" from="${muc_jid}/ralphm">
+                let stanza = stx`<message xmlns="jabber:client" to="${_converse.jid}" type="groupchat" from="${muc_jid}/ralphm">
                     <body>This message will trigger a presence probe</body>
                 </message>`;
-            _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
-            const view = _converse.chatboxviews.get(muc_jid);
+                _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
+                const view = _converse.chatboxviews.get(muc_jid);
 
-            await u.waitUntil(() => view.model.messages.length);
-            let occupant = view.model.messages.at(0)?.occupant;
-            expect(occupant).toBeDefined();
-            expect(occupant.get('nick')).toBe('ralphm');
-            expect(occupant.get('affiliation')).toBeUndefined();
-            expect(occupant.get('role')).toBeUndefined();
+                await u.waitUntil(() => view.model.messages.length);
+                let occupant = view.model.messages.at(0)?.occupant;
+                expect(occupant).toBeDefined();
+                expect(occupant.get('nick')).toBe('ralphm');
+                expect(occupant.get('affiliation')).toBeUndefined();
+                expect(occupant.get('role')).toBeUndefined();
 
-            const sent_stanzas = _converse.api.connection.get().sent_stanzas;
-            let probe = await u.waitUntil(() => sent_stanzas.filter(s => s.matches('presence[type="probe"]')).pop());
-            expect(Strophe.serialize(probe)).toBe(
-                `<presence to="${muc_jid}/ralphm" type="probe" xmlns="jabber:client">`+
-                    `<priority>0</priority>`+
-                    `<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
-                `</presence>`);
+                const sent_stanzas = _converse.api.connection.get().sent_stanzas;
+                let probe = await u.waitUntil(() =>
+                    sent_stanzas.filter((s) => s.matches('presence[type="probe"]')).pop()
+                );
+                expect(probe).toEqualStanza(
+                    stx`<presence to="${muc_jid}/ralphm" type="probe" xmlns="jabber:client">
+                        <priority>0</priority>
+                        <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+                    </presence>`
+                );
 
-            let presence = stx`<presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/ralphm">
+                let presence = stx`<presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/ralphm">
                     <x xmlns="http://jabber.org/protocol/muc#user">
                         <item affiliation="member" jid="ralph@example.org/Conversations.ZvLu" role="participant"/>
                     </x>
                 </presence>`;
-            _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
+                _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
 
-            await u.waitUntil(() => occupant.get('affiliation') === 'member');
-            expect(occupant.get('role')).toBe('participant');
+                await u.waitUntil(() => occupant.get('affiliation') === 'member');
+                expect(occupant.get('role')).toBe('participant');
 
-            // Check that unavailable but affiliated occupants don't get destroyed
-            stanza = stx`<message xmlns="jabber:client" to="${_converse.jid}" type="groupchat" from="${muc_jid}/gonePhising">
+                // Check that unavailable but affiliated occupants don't get destroyed
+                stanza = stx`<message xmlns="jabber:client" to="${_converse.jid}" type="groupchat" from="${muc_jid}/gonePhising">
                     <body>This message from an unavailable user will trigger a presence probe</body>
                 </message>`;
-            _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
+                _converse.api.connection.get()._dataRecv(mock.createRequest(stanza));
 
-            await u.waitUntil(() => view.model.messages.length === 2);
-            occupant = view.model.messages.at(1)?.occupant;
-            expect(occupant).toBeDefined();
-            expect(occupant.get('nick')).toBe('gonePhising');
-            expect(occupant.get('affiliation')).toBeUndefined();
-            expect(occupant.get('role')).toBeUndefined();
+                await u.waitUntil(() => view.model.messages.length === 2);
+                occupant = view.model.messages.at(1)?.occupant;
+                expect(occupant).toBeDefined();
+                expect(occupant.get('nick')).toBe('gonePhising');
+                expect(occupant.get('affiliation')).toBeUndefined();
+                expect(occupant.get('role')).toBeUndefined();
 
-            probe = await u.waitUntil(() => sent_stanzas.filter(s => s.matches(`presence[to="${muc_jid}/gonePhising"]`)).pop());
-            expect(Strophe.serialize(probe)).toBe(
-                `<presence to="${muc_jid}/gonePhising" type="probe" xmlns="jabber:client">`+
-                    `<priority>0</priority>`+
-                    `<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
-                `</presence>`);
+                probe = await u.waitUntil(() =>
+                    sent_stanzas.filter((s) => s.matches(`presence[to="${muc_jid}/gonePhising"]`)).pop()
+                );
+                expect(probe).toEqualStanza(
+                    stx`<presence to="${muc_jid}/gonePhising" type="probe" xmlns="jabber:client">
+                        <priority>0</priority>
+                        <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+                    </presence>`
+                );
 
-            presence = stx`<presence xmlns="jabber:client" type="unavailable" to="${_converse.jid}" from="${muc_jid}/gonePhising">
+                presence = stx`<presence xmlns="jabber:client" type="unavailable" to="${_converse.jid}" from="${muc_jid}/gonePhising">
                     <x xmlns="http://jabber.org/protocol/muc#user">
                         <item affiliation="member" jid="gonePhishing@example.org/d34dBEEF" role="participant"/>
                     </x>
                 </presence>`;
-            _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
+                _converse.api.connection.get()._dataRecv(mock.createRequest(presence));
 
-            expect(view.model.occupants.length).toBe(3);
-            await u.waitUntil(() => occupant.get('affiliation') === 'member');
-            expect(occupant.get('role')).toBe('participant');
-        }));
+                expect(view.model.occupants.length).toBe(3);
+                await u.waitUntil(() => occupant.get('affiliation') === 'member');
+                expect(occupant.get('role')).toBe('participant');
+            })
+        );
     });
 });