Parcourir la source

Include XEP-153 update element in outgoing presences

JC Brand il y a 2 mois
Parent
commit
0fd2c74df4

+ 7 - 3
src/headless/plugins/caps/tests/caps.js

@@ -26,6 +26,7 @@ describe('A sent presence stanza', function () {
             expect(presence.node).toEqualStanza(stx`
             <presence xmlns="jabber:client">
                 <priority>0</priority>
+                <x xmlns="vcard-temp:x:update"/>
                 <c hash="sha-1"
                     node="https://conversejs.org"
                     ver="QgayPKawpkPSDYmwT/WM94uAlu0="
@@ -44,7 +45,8 @@ describe('A sent presence stanza', function () {
             <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"/>
+                <x xmlns="vcard-temp:x:update"/>
+                <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
             </presence>`);
 
             api.settings.set('priority', 2);
@@ -54,7 +56,8 @@ describe('A sent presence stanza', function () {
                 <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"/>
+                <x xmlns="vcard-temp:x:update"/>
+                <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
             </presence>`);
 
             api.settings.set('priority', undefined);
@@ -64,7 +67,8 @@ describe('A sent presence stanza', function () {
                 <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"/>
+                <x xmlns="vcard-temp:x:update"/>
+                <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
             </presence>`);
         })
     );

+ 7 - 13
src/headless/plugins/caps/utils.js

@@ -5,7 +5,7 @@ import _converse from '../../shared/_converse.js';
 import converse from '../../shared/api/public.js';
 import { arrayBufferToBase64, stringToArrayBuffer  } from '../../utils/arraybuffer.js';
 
-const { Strophe, $build } = converse.env;
+const { Strophe, stx } = converse.env;
 
 function propertySort (array, property) {
     return array.sort((a, b) => { return a[property] > b[property] ? -1 : 1 });
@@ -29,22 +29,16 @@ async function generateVerificationString () {
     return arrayBufferToBase64(ab);
 }
 
-async function createCapsNode () {
-    return $build("c", {
-        'xmlns': Strophe.NS.CAPS,
-        'hash': "sha-1",
-        'node': "https://conversejs.org",
-        'ver': await generateVerificationString()
-    }).tree();
-}
-
-
 /**
  * Given a stanza, adds a XEP-0115 CAPS element
  * @param {Strophe.Builder} stanza
  */
 export async function addCapsNode (stanza) {
-    const caps_el = await createCapsNode();
-    stanza.root().cnode(caps_el).up();
+    const node = stx`<c
+        xmlns="${Strophe.NS.CAPS}"
+        hash="sha-1"
+        node="https://conversejs.org"
+        ver="${await generateVerificationString()}"></c>`;
+    stanza.root().cnode(node).up();
     return stanza;
 }

+ 5 - 4
src/headless/plugins/muc/tests/muc.js

@@ -58,7 +58,7 @@ describe("Groupchats", function () {
                 <presence from="${_converse.jid}" id="${pres.getAttribute('id')}" to="${muc_jid}/romeo" xmlns="jabber:client">
                     <x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>
                     <show>away</show>
-                    <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+                    <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
                 </presence>`);
 
             expect(muc.getOwnOccupant().get('show')).toBe('away');
@@ -72,7 +72,8 @@ describe("Groupchats", function () {
                 <presence to="${muc_jid}/romeo" xmlns="jabber:client">
                     <show>xa</show>
                     <priority>0</priority>
-                    <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+                    <x xmlns="vcard-temp:x:update"/>
+                    <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
                 </presence>`)
 
             profile.set({ show: 'dnd', status_message: 'Do not disturb' });
@@ -87,7 +88,7 @@ describe("Groupchats", function () {
                     <x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>
                     <show>dnd</show>
                     <status>Do not disturb</status>
-                    <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+                    <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
                 </presence>`);
 
             expect(muc2.getOwnOccupant().get('show')).toBe('dnd');
@@ -143,7 +144,7 @@ describe("Groupchats", function () {
             expect(Strophe.serialize(pres)).toBe(
                 `<presence from="${_converse.jid}" id="${pres.getAttribute('id')}" to="coven@chat.shakespeare.lit/romeo" xmlns="jabber:client">`+
                     `<x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>`+
-                    `<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
+                    `<c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>`+
                 `</presence>`);
         }));
     });

+ 2 - 2
src/headless/plugins/smacks/tests/smacks.js

@@ -101,8 +101,8 @@ describe("XEP-0198 Stream Management", function () {
         expect(_converse.session.get('unacked_stanzas')[1]).toBe(Strophe.serialize(IQ_stanzas[3]));
         expect(_converse.session.get('unacked_stanzas')[2]).toBe(Strophe.serialize(IQ_stanzas[4]));
         expect(_converse.session.get('unacked_stanzas')[3]).toBe(
-            `<presence xmlns="jabber:client"><priority>0</priority>`+
-                `<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
+            `<presence xmlns="jabber:client"><priority>0</priority><x xmlns="vcard-temp:x:update"/>`+
+                `<c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>`+
             `</presence>`);
 
         r = u.toStanza(`<r xmlns="urn:xmpp:sm:3"/>`);

+ 3 - 2
src/headless/plugins/status/tests/status.js

@@ -1,6 +1,6 @@
 /*global mock, converse */
 
-const u = converse.env.utils;
+const { u, sizzle } = converse.env;
 
 describe("The Profile model", function () {
 
@@ -12,7 +12,8 @@ describe("The Profile model", function () {
         _converse.api.user.status.message.set("I'm also happy!");
 
         const stanza = await u.waitUntil(() => sent_stanzas.filter(s => s.matches('presence')).pop());
-        expect(stanza.childNodes.length).toBe(3);
+        expect(stanza.childNodes.length).toBe(4);
+        expect(sizzle(`x[xmlns="${Strophe.NS.VCARD_UPDATE}"]`, stanza).length).toBe(1);
         expect(stanza.querySelectorAll('status').length).toBe(1);
         expect(stanza.querySelector('status').textContent).toBe("I'm also happy!");
         expect(stanza.querySelectorAll('show').length).toBe(0);

+ 31 - 21
src/headless/plugins/vcard/api.js

@@ -7,18 +7,7 @@ import api from '../../shared/api/index.js';
 import converse from "../../shared/api/public.js";
 import { createStanza, fetchVCard } from './utils.js';
 
-const { dayjs, u } = converse.env;
-
-/**
- * @typedef {Object} VCardData
- * @property {string} [VCardData.fn]
- * @property {string} [VCardData.nickname]
- * @property {string} [VCardData.role]
- * @property {string} [VCardData.email]
- * @property {string} [VCardData.url]
- * @property {string} [VCardData.image_type]
- * @property {string} [VCardData.image]
- */
+const { Strophe, dayjs, u, stx } = converse.env;
 
 export default {
     /**
@@ -39,8 +28,7 @@ export default {
          *
          * @method _converse.api.vcard.set
          * @param {string} jid The JID for which the VCard should be set
-         * @param {VCardData} data A map of VCard keys and values
-         *
+         * @param {import("./types").VCardData} data A map of VCard keys and values
          * @example
          * let jid = _converse.bare_jid;
          * _converse.api.vcard.set( jid, {
@@ -53,13 +41,26 @@ export default {
          * }).
          */
         async set (jid, data) {
+            if (!jid) throw Error("No jid provided for the VCard data");
+            debugger;
             api.waitUntil('VCardsInitialized');
 
-            if (!jid) {
-                throw Error("No jid provided for the VCard data");
+            let vcard = _converse.state.vcards.get(jid);
+            const old_vcard_attrs = vcard?.attributes ?? null;
+            if (vcard && old_vcard_attrs.image !== data.image) {
+                // Optimistically update the vcard with image data. Otherwise some servers (e.g. Ejabberd)
+                // could send a XEP-0153 vcard:update presence which would cause us to refetch the vcard again.
+                const buffer = u.base64ToArrayBuffer(data.image);
+                const hash_ab = await crypto.subtle.digest('SHA-1', buffer);
+                vcard.save({
+                    image: data.image,
+                    image_type: data.image_type,
+                    image_hash: u.arrayBufferToHex(hash_ab),
+                });
             }
-            const div = document.createElement('div');
-            const vcard_el = u.toStanza(`
+
+            let result;
+            const vcard_el = stx`
                 <vCard xmlns="vcard-temp">
                     <FN>${data.fn ?? ''}</FN>
                     <NICKNAME>${data.nickname ?? ''}</NICKNAME>
@@ -70,14 +71,22 @@ export default {
                         <TYPE>${data.image_type ?? ''}</TYPE>
                         <BINVAL>${data.image ?? ''}</BINVAL>
                     </PHOTO>
-                </vCard>`, div);
-            let result;
+                </vCard>`;
             try {
                 result = await api.sendIQ(createStanza("set", jid, vcard_el));
             } catch (e) {
+                if (old_vcard_attrs) vcard.save(old_vcard_attrs);
                 throw (e);
             }
-            await api.vcard.update(jid, true);
+
+            vcard = await api.vcard.update(jid, true);
+            if (u.isOwnJID(jid)) {
+                // Send out a XEP-0153 presence with the image hash
+                const node = stx`<x xmlns="${Strophe.NS.VCARD_UPDATE}">
+                    <photo>${vcard.get('image_hash') ?? ''}</photo>
+                </x>`;
+                api.user.presence.send({}, node);
+            }
             return result;
         },
 
@@ -179,6 +188,7 @@ export default {
                 delete data['stanza']
                 u.safeSave(model, data);
             }
+            return model;
         }
     }
 }

+ 30 - 0
src/headless/plugins/vcard/parsers.js

@@ -0,0 +1,30 @@
+import converse from '../../shared/api/public.js';
+
+const { u } = converse.env;
+
+/**
+ * @param {Element} iq
+ * @returns {Promise<import("./types").VCardResult>}
+ */
+export async function parseVCardResultStanza(iq) {
+    const result = {
+        email: iq.querySelector(':scope > vCard EMAIL USERID')?.textContent,
+        fullname: iq.querySelector(':scope > vCard FN')?.textContent,
+        image: iq.querySelector(':scope > vCard PHOTO BINVAL')?.textContent,
+        image_type: iq.querySelector(':scope > vCard PHOTO TYPE')?.textContent,
+        nickname: iq.querySelector(':scope > vCard NICKNAME')?.textContent,
+        role: iq.querySelector(':scope > vCard ROLE')?.textContent,
+        stanza: iq, // TODO: remove?
+        url: iq.querySelector(':scope > vCard URL')?.textContent,
+        vcard_updated: new Date().toISOString(),
+        error: undefined,
+        vcard_error: undefined,
+        image_hash: undefined,
+    };
+    if (result.image) {
+        const buffer = u.base64ToArrayBuffer(result.image);
+        const ab = await crypto.subtle.digest('SHA-1', buffer);
+        result['image_hash'] = u.arrayBufferToHex(ab);
+    }
+    return result;
+}

+ 6 - 1
src/headless/plugins/vcard/plugin.js

@@ -14,6 +14,7 @@ import {
     onOccupantAvatarChanged,
     registerPresenceHandler,
     unregisterPresenceHandler,
+    updatePresence,
 } from './utils.js';
 
 const { Strophe } = converse.env;
@@ -45,7 +46,10 @@ converse.plugins.add('converse-vcard', {
             }
         );
 
-        api.listen.on('addClientFeatures', () => api.disco.own.features.add(Strophe.NS.VCARD));
+        api.listen.on('addClientFeatures', () => {
+            api.disco.own.features.add(Strophe.NS.VCARD);
+            api.disco.own.features.add(Strophe.NS.VCARD_UPDATE);
+        });
         api.listen.on('clearSession', () => clearVCardsSession());
 
         api.listen.on('visibilityChanged', ({ el }) => {
@@ -61,5 +65,6 @@ converse.plugins.add('converse-vcard', {
 
         api.listen.on('presencesInitialized', () => registerPresenceHandler());
         api.listen.on('beforeTearDown', () => unregisterPresenceHandler());
+        api.listen.on('constructedPresence', (_, p) => updatePresence(p));
     },
 });

+ 142 - 12
src/headless/plugins/vcard/tests/update.js

@@ -1,8 +1,8 @@
-describe('A VCard', function () {
+describe('An incoming presence with a XEP-0153 vcard:update element', function () {
     beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
 
     it(
-        'is replaced when a XEP-0153 presence update is received',
+        'will cause a VCard avatar to be replaced',
         mock.initConverse(['chatBoxesFetched'], { no_vcard_mocks: true }, async function (_converse) {
             const { api } = _converse;
             const { u, sizzle } = _converse.env;
@@ -24,7 +24,10 @@ describe('A VCard', function () {
                         </presence>`
                 )
             );
-            const sent_stanza = await u.waitUntil(() => IQ_stanzas.filter((s) => sizzle('vCard', s).length).pop(), 1000);
+            const sent_stanza = await u.waitUntil(
+                () => IQ_stanzas.filter((s) => sizzle('vCard', s).length).pop(),
+                1000
+            );
             expect(sent_stanza).toEqualStanza(stx`
                 <iq type="get"
                         to="mercutio@montague.lit"
@@ -49,16 +52,16 @@ describe('A VCard', function () {
                     <vCard xmlns='vcard-temp'>
                         <BDAY>1476-06-09</BDAY>
                         <ADR>
-                        <CTRY>Italy</CTRY>
-                        <LOCALITY>Verona</LOCALITY>
-                        <HOME/>
+                            <CTRY>Italy</CTRY>
+                            <LOCALITY>Verona</LOCALITY>
+                            <HOME/>
                         </ADR>
                         <NICKNAME/>
                         <N><GIVEN>Mercutio</GIVEN><FAMILY>Capulet</FAMILY></N>
                         <EMAIL><USERID>mercutio@shakespeare.lit</USERID></EMAIL>
                         <PHOTO>
-                        <TYPE>${blob.type}</TYPE>
-                        <BINVAL>${base64Image}</BINVAL>
+                            <TYPE>${blob.type}</TYPE>
+                            <BINVAL>${base64Image}</BINVAL>
                         </PHOTO>
                     </vCard>
                 </iq>`)
@@ -90,7 +93,7 @@ describe('A VCard', function () {
     );
 
     it(
-        'is removed when a XEP-0153 presence update is received',
+        'will cause a VCard avatar to be removed',
         mock.initConverse(['chatBoxesFetched'], { no_vcard_mocks: true }, async function (_converse) {
             const { api } = _converse;
             const { u, sizzle } = _converse.env;
@@ -100,9 +103,7 @@ describe('A VCard', function () {
             const own_jid = _converse.session.get('jid');
 
             const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
-            let sent_stanza = await u.waitUntil(() =>
-                IQ_stanzas.filter((s) => sizzle(`vCard`, s).length).pop()
-            , 500);
+            let sent_stanza = await u.waitUntil(() => IQ_stanzas.filter((s) => sizzle(`vCard`, s).length).pop(), 500);
             _converse.api.connection.get()._dataRecv(
                 mock.createRequest(stx`
                 <iq from='${own_jid}'
@@ -202,3 +203,132 @@ describe('A VCard', function () {
         })
     );
 });
+
+describe('An outgoing presence with a XEP-0153 vcard:update element', function () {
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
+    it(
+        'is sent when the user updates their VCard avatar',
+        mock.initConverse(['chatBoxesFetched'], { no_vcard_mocks: true }, async function (_converse) {
+            const { api } = _converse;
+            const { u, sizzle } = _converse.env;
+            await mock.waitForRoster(_converse, 'current', 0);
+            mock.openControlBox(_converse);
+            const own_jid = _converse.session.get('jid');
+            const own_bare_jid = _converse.session.get('bare_jid');
+
+            const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
+            let sent_stanza = await u.waitUntil(() => IQ_stanzas.filter((s) => sizzle(`vCard`, s).length).pop(), 500);
+            _converse.api.connection.get()._dataRecv(
+                mock.createRequest(stx`
+                <iq from='${own_jid}'
+                        xmlns="jabber:client"
+                        to='${_converse.session.get('jid')}'
+                        type='result'
+                        id='${sent_stanza.getAttribute('id')}'>
+                    <vCard xmlns='vcard-temp'></vCard>
+                </iq>`)
+            );
+
+            const vcard = await u.waitUntil(() => _converse.state.vcards.get(own_bare_jid));
+            expect(vcard.get('image_hash')).not.toBeDefined();
+            const vcard_updated = await u.waitUntil(() => vcard.get('vcard_updated'));
+
+            while (IQ_stanzas.length) IQ_stanzas.pop();
+
+            const response = await fetch('/base/logo/conversejs-filled-192.png');
+            const blob = await response.blob();
+            const arrayBuffer = await blob.arrayBuffer();
+            const byteArray = new Uint8Array(arrayBuffer);
+            const hash_ab = await crypto.subtle.digest('SHA-1', byteArray);
+            const image_hash = u.arrayBufferToHex(hash_ab);
+            const base64Image = btoa(String.fromCharCode(...byteArray));
+
+            // Call the API to set a new avatar
+            api.vcard.set(own_bare_jid, {
+                image: base64Image,
+                image_type: blob.type,
+            });
+
+            sent_stanza = await u.waitUntil(() => IQ_stanzas.filter((s) => sizzle(`vCard`, s).length).pop(), 500);
+            expect(sent_stanza).toEqualStanza(stx`
+                <iq type="set" to="${own_bare_jid}" xmlns="jabber:client" id="${sent_stanza.getAttribute('id')}">
+                    <vCard xmlns="vcard-temp">
+                        <FN/><NICKNAME/><URL/><ROLE/><EMAIL><INTERNET/><PREF/><USERID/></EMAIL><PHOTO><TYPE>image/png</TYPE>
+                        <BINVAL>${base64Image}</BINVAL></PHOTO>
+                    </vCard>
+                </iq>`);
+
+            // Check optimistic save
+            expect(vcard.get('image')).toBe(base64Image);
+            expect(vcard.get('image_hash')).toBe(image_hash);
+            expect(vcard.get('image_type')).toBe(blob.type);
+            expect(vcard.get('vcard_updated')).toBe(vcard_updated); // didn't change
+
+            while (IQ_stanzas.length) IQ_stanzas.pop();
+
+            // Some servers send their own update presence, before sending the
+            // return IQ. This should not create a new VCard get request
+            _converse.api.connection.get()._dataRecv(
+                mock.createRequest(
+                    stx`<presence xmlns="jabber:client"
+                                to="${_converse.session.get('jid')}"
+                                from="${own_jid}">
+                            <x xmlns='vcard-temp:x:update'>
+                                <photo>${image_hash}</photo>
+                            </x>
+                        </presence>`
+                )
+            );
+
+            _converse.api.connection.get()._dataRecv(
+                mock.createRequest(stx`
+                <iq from='${own_bare_jid}'
+                        xmlns="jabber:client"
+                        to='${own_jid}'
+                        type='result'
+                        id='${sent_stanza.getAttribute('id')}'></iq>`)
+            );
+
+            // A new get IQ is sent out to fetch the latest VCard
+            sent_stanza = await u.waitUntil(
+                () => IQ_stanzas.filter((s) => sizzle('vCard', s).length).pop(),
+                1000
+            );
+            expect(sent_stanza).toEqualStanza(stx`
+                <iq type="get" xmlns="jabber:client" id="${sent_stanza.getAttribute('id')}">
+                    <vCard xmlns="vcard-temp"/>
+                </iq>`);
+
+            _converse.api.connection.get()._dataRecv(
+                mock.createRequest(stx`
+                <iq from='${own_bare_jid}'
+                        xmlns="jabber:client"
+                        to='${own_jid}'
+                        type='result'
+                        id='${sent_stanza.getAttribute('id')}'>
+                    <vCard xmlns='vcard-temp'>
+                        <BDAY>1476-06-09</BDAY>
+                        <ADR>
+                            <CTRY>Italy</CTRY>
+                            <LOCALITY>Verona</LOCALITY>
+                            <HOME/>
+                        </ADR>
+                        <NICKNAME/>
+                        <N><GIVEN>Romeo</GIVEN><FAMILY>Montague</FAMILY></N>
+                        <EMAIL><USERID>romeo@shakespeare.lit</USERID></EMAIL>
+                        <PHOTO>
+                            <TYPE>${blob.type}</TYPE>
+                            <BINVAL>${base64Image}</BINVAL>
+                        </PHOTO>
+                    </vCard>
+                </iq>`)
+            );
+
+            await u.waitUntil(() => vcard.get('vcard_updated'));
+            expect(vcard.get('image')).toBe(base64Image);
+            expect(vcard.get('image_hash')).toBe(image_hash);
+            expect(vcard.get('image_type')).toBe(blob.type);
+        })
+    );
+});

+ 10 - 0
src/headless/plugins/vcard/types.ts

@@ -18,3 +18,13 @@ export interface VCardResult {
     vcard_error?: string;
     vcard_updated?: string;
 }
+
+export type VCardData = {
+    fn?: string;
+    nickname?: string;
+    role?: string;
+    email?: string;
+    url?: string;
+    image_type?: string;
+    image?: string;
+};

+ 15 - 30
src/headless/plugins/vcard/utils.js

@@ -14,38 +14,12 @@ import log from '@converse/log';
 import { shouldClearCache } from '../../utils/session.js';
 import { isElement } from '../../utils/html.js';
 import { parseErrorStanza } from '../../shared/parsers.js';
+import {parseVCardResultStanza} from './parsers.js';
 
-const { Strophe, $iq, u, sizzle } = converse.env;
+const { Strophe, $iq, sizzle, stx } = converse.env;
 
 Strophe.addNamespace('VCARD_UPDATE', 'vcard-temp:x:update');
 
-/**
- * @param {Element} iq
- * @returns {Promise<import("./types").VCardResult>}
- */
-export async function onVCardData(iq) {
-    const result = {
-        email: iq.querySelector(':scope > vCard EMAIL USERID')?.textContent,
-        fullname: iq.querySelector(':scope > vCard FN')?.textContent,
-        image: iq.querySelector(':scope > vCard PHOTO BINVAL')?.textContent,
-        image_type: iq.querySelector(':scope > vCard PHOTO TYPE')?.textContent,
-        nickname: iq.querySelector(':scope > vCard NICKNAME')?.textContent,
-        role: iq.querySelector(':scope > vCard ROLE')?.textContent,
-        stanza: iq, // TODO: remove?
-        url: iq.querySelector(':scope > vCard URL')?.textContent,
-        vcard_updated: new Date().toISOString(),
-        error: undefined,
-        vcard_error: undefined,
-        image_hash: undefined,
-    };
-    if (result.image) {
-        const buffer = u.base64ToArrayBuffer(result.image);
-        const ab = await crypto.subtle.digest('SHA-1', buffer);
-        result['image_hash'] = u.arrayBufferToHex(ab);
-    }
-    return result;
-}
-
 /**
  * @param {"get"|"set"|"result"} type
  * @param {string} jid
@@ -192,7 +166,7 @@ export async function fetchVCard(jid) {
             vcard_error: new Date().toISOString(),
         };
     }
-    return onVCardData(iq);
+    return parseVCardResultStanza(iq);
 }
 
 /**
@@ -222,7 +196,7 @@ export function unregisterPresenceHandler() {
 }
 
 export function registerPresenceHandler() {
-    unregisterPresenceHandler();
+    // unregisterPresenceHandler();
     const connection = api.connection.get();
     presence_ref = connection.addHandler(
         /** @param {Element} pres */
@@ -239,3 +213,14 @@ export function registerPresenceHandler() {
         null
     );
 }
+
+/**
+ * @param {import('strophe.js').Builder} stanza
+ */
+export function updatePresence(stanza) {
+    if (sizzle(`x[xmlns=${Strophe.NS.VCARD_UPDATE}"]`, stanza.root()).length === 0) {
+        const node = stx`<x xmlns="${Strophe.NS.VCARD_UPDATE}"></x>`;
+        stanza.root().cnode(node).up();
+    }
+    return stanza;
+}

+ 1 - 0
src/headless/types/plugins/muc/occupant.d.ts

@@ -278,6 +278,7 @@ declare class MUCOccupant extends MUCOccupant_base {
     initialize(): Promise<void>;
     defaults(): {
         hats: any[];
+        presence: string;
         show: any;
         states: any[];
         hidden: boolean;

+ 2 - 12
src/headless/types/plugins/vcard/api.d.ts

@@ -9,8 +9,7 @@ declare namespace _default {
          *
          * @method _converse.api.vcard.set
          * @param {string} jid The JID for which the VCard should be set
-         * @param {VCardData} data A map of VCard keys and values
-         *
+         * @param {import("./types").VCardData} data A map of VCard keys and values
          * @example
          * let jid = _converse.bare_jid;
          * _converse.api.vcard.set( jid, {
@@ -22,7 +21,7 @@ declare namespace _default {
          *     // Failure, e is your error object
          * }).
          */
-        function set(jid: string, data: VCardData): Promise<any>;
+        function set(jid: string, data: import("./types").VCardData): Promise<any>;
         /**
          * @method _converse.api.vcard.get
          * @param {Model|string} model Either a `Model` instance, or a string JID.
@@ -67,13 +66,4 @@ declare namespace _default {
 }
 export default _default;
 export type Model = import("@converse/skeletor").Model;
-export type VCardData = {
-    fn?: string;
-    nickname?: string;
-    role?: string;
-    email?: string;
-    url?: string;
-    image_type?: string;
-    image?: string;
-};
 //# sourceMappingURL=api.d.ts.map

+ 6 - 0
src/headless/types/plugins/vcard/parsers.d.ts

@@ -0,0 +1,6 @@
+/**
+ * @param {Element} iq
+ * @returns {Promise<import("./types").VCardResult>}
+ */
+export function parseVCardResultStanza(iq: Element): Promise<import("./types").VCardResult>;
+//# sourceMappingURL=parsers.d.ts.map

+ 9 - 0
src/headless/types/plugins/vcard/types.d.ts

@@ -16,4 +16,13 @@ export interface VCardResult {
     vcard_error?: string;
     vcard_updated?: string;
 }
+export type VCardData = {
+    fn?: string;
+    nickname?: string;
+    role?: string;
+    email?: string;
+    url?: string;
+    image_type?: string;
+    image?: string;
+};
 //# sourceMappingURL=types.d.ts.map

+ 5 - 6
src/headless/types/plugins/vcard/utils.d.ts

@@ -1,8 +1,3 @@
-/**
- * @param {Element} iq
- * @returns {Promise<import("./types").VCardResult>}
- */
-export function onVCardData(iq: Element): Promise<import("./types").VCardResult>;
 /**
  * @param {"get"|"set"|"result"} type
  * @param {string} jid
@@ -29,7 +24,7 @@ export function clearVCardsSession(): void;
 /**
  * @param {string} jid
  */
-export function fetchVCard(jid: string): Promise<import("./types").VCardResult | {
+export function fetchVCard(jid: string): Promise<import("./types.js").VCardResult | {
     jid: string;
     stanza: any;
     error: any;
@@ -37,6 +32,10 @@ export function fetchVCard(jid: string): Promise<import("./types").VCardResult |
 }>;
 export function unregisterPresenceHandler(): void;
 export function registerPresenceHandler(): void;
+/**
+ * @param {import('strophe.js').Builder} stanza
+ */
+export function updatePresence(stanza: import("strophe.js").Builder): import("strophe.js").Builder;
 export type MUCMessage = import("../../plugins/muc/message").default;
 export type Profile = import("../../plugins/status/profile").default;
 export type VCards = import("../../plugins/vcard/vcards").default;

+ 1 - 1
src/plugins/bookmark-views/tests/bookmarks.js

@@ -278,7 +278,7 @@ describe("Bookmarks", function () {
             <c xmlns="http://jabber.org/protocol/caps"
                hash="sha-1"
                node="https://conversejs.org"
-               ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ="/>
+               ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s="/>
             </presence>`);
     }));
 });

+ 1 - 1
src/plugins/muc-views/modals/config.js

@@ -8,7 +8,7 @@ const { sizzle, u } = converse.env;
 
 export default class MUCConfigModal extends BaseModal {
     /**
-     * @typedef {import('@converse/headless/types/plugins/vcard/api').VCardData} VCardData
+     * @typedef {import('@converse/headless/types/plugins/vcard/types').VCardData} VCardData
      */
 
     constructor (options) {

+ 3 - 3
src/plugins/muc-views/tests/nickname.js

@@ -391,7 +391,7 @@ describe("A MUC", function () {
                         <history maxstanzas="0"/>
                     </x>
                     <c xmlns="http://jabber.org/protocol/caps" hash="sha-1" node="https://conversejs.org"
-                        ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ="/>
+                        ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s="/>
                 </presence>`);
 
             while (IQ_stanzas.length) IQ_stanzas.pop();
@@ -423,7 +423,7 @@ describe("A MUC", function () {
                         <history maxstanzas="0"/>
                     </x>
                     <c xmlns="http://jabber.org/protocol/caps" hash="sha-1" node="https://conversejs.org"
-                        ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ="/>
+                        ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s="/>
                 </presence>`);
 
             while (IQ_stanzas.length) IQ_stanzas.pop();
@@ -455,7 +455,7 @@ describe("A MUC", function () {
                         <history maxstanzas="0"/>
                     </x>
                     <c xmlns="http://jabber.org/protocol/caps" hash="sha-1" node="https://conversejs.org"
-                        ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ="/>
+                        ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s="/>
                 </presence>`);
         }));
 

+ 4 - 2
src/plugins/muc-views/tests/probes.js

@@ -32,7 +32,8 @@ describe('Groupchats', function () {
                 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"/>
+                        <x xmlns="vcard-temp:x:update"/>
+                        <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
                     </presence>`
                 );
 
@@ -65,7 +66,8 @@ describe('Groupchats', function () {
                 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"/>
+                        <x xmlns="vcard-temp:x:update"/>
+                        <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
                     </presence>`
                 );
 

+ 51 - 43
src/plugins/muc-views/tests/rai.js

@@ -5,6 +5,8 @@ const { Strophe, u, stx, sizzle } = converse.env;
 
 describe("XEP-0437 Room Activity Indicators", function () {
 
+    beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
+
     it("will be activated for a MUC that becomes hidden",
         mock.initConverse(
             [], {
@@ -73,31 +75,33 @@ describe("XEP-0437 Room Activity Indicators", function () {
         view.model.save({'hidden': true});
         await u.waitUntil(() => sent_stanzas.length === 4);
 
-        expect(Strophe.serialize(sent_stanzas[0])).toBe(
-            `<message to="lounge@montague.lit" type="groupchat" xmlns="jabber:client">`+
-                `<inactive xmlns="http://jabber.org/protocol/chatstates"/>`+
-                `<no-store xmlns="urn:xmpp:hints"/>`+
-                `<no-permanent-store xmlns="urn:xmpp:hints"/>`+
-            `</message>`
+        expect(sent_stanzas[0]).toEqualStanza(stx`
+            <message to="lounge@montague.lit" type="groupchat" xmlns="jabber:client">
+                <inactive xmlns="http://jabber.org/protocol/chatstates"/>
+                <no-store xmlns="urn:xmpp:hints"/>
+                <no-permanent-store xmlns="urn:xmpp:hints"/>
+            </message>`
         );
 
-        expect(Strophe.serialize(sent_stanzas[1])).toBe(
-            `<message from="${_converse.jid}" id="${sent_stanzas[1].getAttribute('id')}" to="lounge@montague.lit" type="groupchat" xmlns="jabber:client">`+
-                `<received id="${last_msg_id}" xmlns="urn:xmpp:chat-markers:0"/>`+
-            `</message>`
+        expect(sent_stanzas[1]).toEqualStanza(stx`
+            <message from="${_converse.jid}" id="${sent_stanzas[1].getAttribute('id')}" to="lounge@montague.lit" type="groupchat" xmlns="jabber:client">
+                <received id="${last_msg_id}" xmlns="urn:xmpp:chat-markers:0"/>
+            </message>`
         );
-        expect(Strophe.serialize(sent_stanzas[2])).toBe(
-            `<presence to="${muc_jid}/romeo" type="unavailable" xmlns="jabber:client">`+
-                `<priority>0</priority>`+
-                `<c hash="sha-1" node="https://conversejs.org" ver="/5ng/Bnz6MXvkSDu6hjAlgQ8C60=" xmlns="http://jabber.org/protocol/caps"/>`+
-            `</presence>`
+        expect(sent_stanzas[2]).toEqualStanza(stx`
+            <presence to="${muc_jid}/romeo" type="unavailable" xmlns="jabber:client">
+                <priority>0</priority>
+                <x xmlns="${Strophe.NS.VCARD_UPDATE}"></x>
+                <c hash="sha-1" node="https://conversejs.org" ver="1zMU0D2z+vTfsHt07Z7AyaxbtR8=" xmlns="http://jabber.org/protocol/caps"/>
+            </presence>`
         );
-        expect(Strophe.serialize(sent_stanzas[3])).toBe(
-            `<presence to="montague.lit" xmlns="jabber:client">`+
-                `<priority>0</priority>`+
-                `<c hash="sha-1" node="https://conversejs.org" ver="/5ng/Bnz6MXvkSDu6hjAlgQ8C60=" xmlns="http://jabber.org/protocol/caps"/>`+
-                `<rai xmlns="urn:xmpp:rai:0"/>`+
-            `</presence>`
+        expect(sent_stanzas[3]).toEqualStanza(stx`
+            <presence to="montague.lit" xmlns="jabber:client">
+                <priority>0</priority>
+                <x xmlns="${Strophe.NS.VCARD_UPDATE}"></x>
+                <c hash="sha-1" node="https://conversejs.org" ver="1zMU0D2z+vTfsHt07Z7AyaxbtR8=" xmlns="http://jabber.org/protocol/caps"/>
+                <rai xmlns="urn:xmpp:rai:0"/>
+            </presence>`
         );
 
         await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED);
@@ -149,18 +153,20 @@ describe("XEP-0437 Room Activity Indicators", function () {
         await u.waitUntil(() => getSentPresences().length === 3, 500);
         const sent_presences = getSentPresences();
 
-        expect(Strophe.serialize(sent_presences[1])).toBe(
-            `<presence to="${muc_jid}/romeo" type="unavailable" xmlns="jabber:client">`+
-                `<priority>0</priority>`+
-                `<c hash="sha-1" node="https://conversejs.org" ver="/5ng/Bnz6MXvkSDu6hjAlgQ8C60=" xmlns="http://jabber.org/protocol/caps"/>`+
-            `</presence>`
+        expect(sent_presences[1]).toEqualStanza(stx`
+            <presence to="${muc_jid}/romeo" type="unavailable" xmlns="jabber:client">
+                <priority>0</priority>
+                <x xmlns="${Strophe.NS.VCARD_UPDATE}"></x>
+                <c hash="sha-1" node="https://conversejs.org" ver="1zMU0D2z+vTfsHt07Z7AyaxbtR8=" xmlns="http://jabber.org/protocol/caps"/>
+            </presence>`
         );
-        expect(Strophe.serialize(sent_presences[2])).toBe(
-            `<presence to="montague.lit" xmlns="jabber:client">`+
-                `<priority>0</priority>`+
-                `<c hash="sha-1" node="https://conversejs.org" ver="/5ng/Bnz6MXvkSDu6hjAlgQ8C60=" xmlns="http://jabber.org/protocol/caps"/>`+
-                `<rai xmlns="urn:xmpp:rai:0"/>`+
-            `</presence>`
+        expect(sent_presences[2]).toEqualStanza(stx`
+            <presence to="montague.lit" xmlns="jabber:client">
+                <priority>0</priority>
+                <x xmlns="${Strophe.NS.VCARD_UPDATE}"></x>
+                <c hash="sha-1" node="https://conversejs.org" ver="1zMU0D2z+vTfsHt07Z7AyaxbtR8=" xmlns="http://jabber.org/protocol/caps"/>
+                <rai xmlns="urn:xmpp:rai:0"/>
+            </presence>`
         );
 
         await u.waitUntil(() => model.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED);
@@ -203,18 +209,20 @@ describe("XEP-0437 Room Activity Indicators", function () {
         await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName === 'presence').length === 2);
 
         const sent_presences = sent_stanzas.filter(s => s.nodeName === 'presence');
-        expect(Strophe.serialize(sent_presences[0])).toBe(
-            `<presence to="${muc_jid}/romeo" type="unavailable" xmlns="jabber:client">`+
-                `<priority>0</priority>`+
-                `<c hash="sha-1" node="https://conversejs.org" ver="/5ng/Bnz6MXvkSDu6hjAlgQ8C60=" xmlns="http://jabber.org/protocol/caps"/>`+
-            `</presence>`
+        expect(sent_presences[0]).toEqualStanza(stx`
+            <presence to="${muc_jid}/romeo" type="unavailable" xmlns="jabber:client">
+                <priority>0</priority>
+                <x xmlns="${Strophe.NS.VCARD_UPDATE}"></x>
+                <c hash="sha-1" node="https://conversejs.org" ver="1zMU0D2z+vTfsHt07Z7AyaxbtR8=" xmlns="http://jabber.org/protocol/caps"/>
+            </presence>`
         );
-        expect(Strophe.serialize(sent_presences[1])).toBe(
-            `<presence to="montague.lit" xmlns="jabber:client">`+
-                `<priority>0</priority>`+
-                `<c hash="sha-1" node="https://conversejs.org" ver="/5ng/Bnz6MXvkSDu6hjAlgQ8C60=" xmlns="http://jabber.org/protocol/caps"/>`+
-                `<rai xmlns="urn:xmpp:rai:0"/>`+
-            `</presence>`
+        expect(sent_presences[1]).toEqualStanza(stx`
+            <presence to="montague.lit" xmlns="jabber:client">
+                <priority>0</priority>
+                <x xmlns="${Strophe.NS.VCARD_UPDATE}"></x>
+                <c hash="sha-1" node="https://conversejs.org" ver="1zMU0D2z+vTfsHt07Z7AyaxbtR8=" xmlns="http://jabber.org/protocol/caps"/>
+                <rai xmlns="urn:xmpp:rai:0"/>
+            </presence>`
         );
         // If an error presence with "resource-constraint" is returned, we rejoin
         const activity_stanza = stx`

+ 1 - 1
src/plugins/profile/modals/profile.js

@@ -11,7 +11,7 @@ import './styles/profile.scss';
 
 export default class ProfileModal extends BaseModal {
     /**
-     * @typedef {import('@converse/headless/types/plugins/vcard/api').VCardData} VCardData
+     * @typedef {import('@converse/headless/types/plugins/vcard/types').VCardData} VCardData
      * @typedef {import("@converse/headless").Profile} Profile
      */
 

+ 1 - 1
src/plugins/profile/statusview.js

@@ -7,7 +7,7 @@ import './styles/profile.scss';
 
 class Profile extends CustomElement {
     initialize () {
-        this.model = _converse.state.xmppstatus;
+        this.model = _converse.state.profile;
         this.listenTo(this.model, "change", () => this.requestUpdate());
         this.listenTo(this.model, "vcard:add", () => this.requestUpdate());
         this.listenTo(this.model, "vcard:change", () => this.requestUpdate());

+ 9 - 7
src/plugins/profile/tests/status.js

@@ -33,7 +33,8 @@ describe("The Controlbox", function () {
                 <presence xmlns="jabber:client">
                     <show>dnd</show>
                     <priority>0</priority>
-                    <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+                    <x xmlns="${Strophe.NS.VCARD_UPDATE}"></x>
+                    <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
                 </presence>`);
             const view = await u.waitUntil(() => document.querySelector('converse-user-profile'));
             const first_child = view.querySelector('.xmpp-status span:first-child');
@@ -56,12 +57,13 @@ describe("The Controlbox", function () {
             modal.querySelector('[type="submit"]').click();
             const sent_stanzas = _converse.api.connection.get().sent_stanzas;
             const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop());
-            expect(Strophe.serialize(sent_presence)).toBe(
-                `<presence xmlns="jabber:client">`+
-                    `<status>I am happy</status>`+
-                    `<priority>0</priority>`+
-                    `<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
-                `</presence>`);
+            expect(sent_presence).toEqualStanza(stx`
+                <presence xmlns="jabber:client">
+                    <status>I am happy</status>
+                    <priority>0</priority>
+                    <x xmlns="${Strophe.NS.VCARD_UPDATE}"></x>
+                    <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
+                </presence>`);
 
             const view = await u.waitUntil(() => document.querySelector('converse-user-profile'));
             const first_child = view.querySelector('.xmpp-status span:first-child');

+ 4 - 2
src/plugins/rosterview/tests/presence.js

@@ -30,7 +30,8 @@ describe("A sent presence stanza", function () {
             <presence xmlns="jabber:client">
                 <status>My custom status</status>
                 <priority>0</priority>
-                <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+                <x xmlns="${Strophe.NS.VCARD_UPDATE}"></x>
+                <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
             </presence>`)
         await u.waitUntil(() => modal.getAttribute('aria-hidden') === "true");
         await u.waitUntil(() => !u.isVisible(modal));
@@ -49,7 +50,8 @@ describe("A sent presence stanza", function () {
                     <show>dnd</show>
                     <status>My custom status</status>
                     <priority>0</priority>
-                    <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+                    <x xmlns="${Strophe.NS.VCARD_UPDATE}"></x>
+                    <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
                 </presence>`)
     }));
 });

+ 2 - 1
src/plugins/rosterview/tests/protocol.js

@@ -144,7 +144,8 @@ describe("Presence subscriptions", function () {
                 <presence to="contact@example.org" type="subscribe" xmlns="jabber:client">
                     <nick xmlns="http://jabber.org/protocol/nick">Romeo</nick>
                     <priority>0</priority>
-                    <c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>
+                    <x xmlns="${Strophe.NS.VCARD_UPDATE}"></x>
+                    <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
                 </presence>
             `);
 

+ 6 - 0
src/shared/tests/mock.js

@@ -767,11 +767,13 @@ function getMockVcardFetcher (settings) {
 }
 
 const theme = ['dracula', 'classic', 'cyberpunk', 'nordic'][Math.floor(Math.random()*4)];
+let originalVCardGet;
 
 async function _initConverse (settings) {
     clearStores();
     await clearIndexedDB();
 
+
     _converse = await converse.initialize(Object.assign({
         animate: false,
         auto_subscribe: false,
@@ -793,8 +795,12 @@ async function _initConverse (settings) {
 
     window._converse = _converse;
 
+    originalVCardGet = originalVCardGet || _converse.api.vcard.get;
+
     if (!settings?.no_vcard_mocks && _converse.api.vcard) {
         _converse.api.vcard.get = getMockVcardFetcher(settings);
+    } else {
+        _converse.api.vcard.get = originalVCardGet;
     }
 
     if (settings?.auto_login !== false) {

+ 1 - 1
src/types/plugins/muc-views/modals/config.d.ts

@@ -1,6 +1,6 @@
 export default class MUCConfigModal extends BaseModal {
     /**
-     * @typedef {import('@converse/headless/types/plugins/vcard/api').VCardData} VCardData
+     * @typedef {import('@converse/headless/types/plugins/vcard/types').VCardData} VCardData
      */
     constructor(options: any);
     addListeners(): void;

+ 2 - 2
src/types/plugins/profile/modals/profile.d.ts

@@ -1,6 +1,6 @@
 export default class ProfileModal extends BaseModal {
     /**
-     * @typedef {import('@converse/headless/types/plugins/vcard/api').VCardData} VCardData
+     * @typedef {import('@converse/headless/types/plugins/vcard/types').VCardData} VCardData
      * @typedef {import("@converse/headless").Profile} Profile
      */
     static properties: {
@@ -16,7 +16,7 @@ export default class ProfileModal extends BaseModal {
     /**
      * @param {VCardData} data
      */
-    setVCard(data: import("@converse/headless/types/plugins/vcard/api").VCardData): Promise<boolean>;
+    setVCard(data: import("@converse/headless/types/plugins/vcard/types").VCardData): Promise<boolean>;
     /**
      * @param {SubmitEvent} ev
      */