Просмотр исходного кода

Split vcard plugin up into multiple files

JC Brand 3 лет назад
Родитель
Сommit
bdac6f1b47

+ 1 - 1
src/headless/headless.js

@@ -18,7 +18,7 @@ import "./plugins/pubsub.js";           // XEP-0060 Pubsub
 import "./plugins/roster/index.js";     // RFC-6121 Contacts Roster
 import "./plugins/smacks/index.js";     // XEP-0198 Stream Management
 import "./plugins/status/index.js";
-import "./plugins/vcard.js";            // XEP-0054 VCard-temp
+import "./plugins/vcard/index.js";      // XEP-0054 VCard-temp
 /* END: Removable components */
 
 import { converse } from "./core.js";

+ 0 - 394
src/headless/plugins/vcard.js

@@ -1,394 +0,0 @@
-/**
- * @module converse-vcard
- * @copyright The Converse.js contributors
- * @license Mozilla Public License (MPLv2)
- */
-import "./status";
-import log from "@converse/headless/log";
-import { Collection } from "@converse/skeletor/src/collection";
-import { Model } from '@converse/skeletor/src/model.js';
-import { _converse, api, converse } from "../core.js";
-import { initStorage } from '@converse/headless/utils/storage.js';
-
-const { Strophe, $iq, dayjs } = converse.env;
-const u = converse.env.utils;
-
-
-converse.plugins.add('converse-vcard', {
-
-    dependencies: ["converse-status", "converse-roster"],
-
-    overrides: {
-        XMPPStatus: {
-            getNickname () {
-                const { _converse } = this.__super__;
-                const nick = this.__super__.getNickname.apply(this);
-                if (!nick && _converse.xmppstatus.vcard) {
-                    return _converse.xmppstatus.vcard.get('nickname');
-                } else {
-                    return nick;
-                }
-            },
-
-            getFullname () {
-                const { _converse } = this.__super__;
-                const fullname = this.__super__.getFullname.apply(this);
-                if (!fullname && _converse.xmppstatus.vcard) {
-                    return _converse.xmppstatus.vcard.get('fullname');
-                } else {
-                    return fullname;
-                }
-            }
-        },
-
-        RosterContact: {
-            getDisplayName () {
-                if (!this.get('nickname') && this.vcard) {
-                    return this.vcard.getDisplayName();
-                } else {
-                    return this.__super__.getDisplayName.apply(this);
-                }
-            },
-            getFullname () {
-                if (this.vcard) {
-                    return this.vcard.get('fullname');
-                } else {
-                    return this.__super__.getFullname.apply(this);
-                }
-            }
-        }
-    },
-
-    initialize () {
-        /* The initialize function gets called as soon as the plugin is
-         * loaded by converse.js's plugin machinery.
-         */
-        api.promises.add('VCardsInitialized');
-
-
-        /**
-         * Represents a VCard
-         * @class
-         * @namespace _converse.VCard
-         * @memberOf _converse
-         */
-        _converse.VCard = Model.extend({
-            defaults: {
-                'image': _converse.DEFAULT_IMAGE,
-                'image_type': _converse.DEFAULT_IMAGE_TYPE
-            },
-
-            set (key, val, options) {
-                // Override Model.prototype.set to make sure that the
-                // default `image` and `image_type` values are maintained.
-                let attrs;
-                if (typeof key === 'object') {
-                    attrs = key;
-                    options = val;
-                } else {
-                    (attrs = {})[key] = val;
-                }
-                if ('image' in attrs && !attrs['image']) {
-                    attrs['image'] = _converse.DEFAULT_IMAGE;
-                    attrs['image_type'] = _converse.DEFAULT_IMAGE_TYPE;
-                    return Model.prototype.set.call(this, attrs, options);
-                } else {
-                    return Model.prototype.set.apply(this, arguments);
-                }
-            },
-
-            getDisplayName () {
-                return this.get('nickname') || this.get('fullname') || this.get('jid');
-            }
-        });
-
-
-        _converse.VCards = Collection.extend({
-            model: _converse.VCard,
-
-            initialize () {
-                this.on('add', vcard => (vcard.get('jid') && api.vcard.update(vcard)));
-            }
-        });
-
-
-        async function onVCardData (jid, iq) {
-            const vcard = iq.querySelector('vCard');
-            let result = {};
-            if (vcard !== null) {
-                result = {
-                    'stanza': iq,
-                    'fullname': vcard.querySelector('FN')?.textContent,
-                    'nickname': vcard.querySelector('NICKNAME')?.textContent,
-                    'image': vcard.querySelector('PHOTO BINVAL')?.textContent,
-                    'image_type': vcard.querySelector('PHOTO TYPE')?.textContent,
-                    'url': vcard.querySelector('URL')?.textContent,
-                    'role': vcard.querySelector('ROLE')?.textContent,
-                    'email': vcard.querySelector('EMAIL USERID')?.textContent,
-                    'vcard_updated': (new Date()).toISOString(),
-                    'vcard_error': 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;
-        }
-
-
-        function createStanza (type, jid, vcard_el) {
-            const iq = $iq(jid ? {'type': type, 'to': jid} : {'type': type});
-            if (!vcard_el) {
-                iq.c("vCard", {'xmlns': Strophe.NS.VCARD});
-            } else {
-                iq.cnode(vcard_el);
-            }
-            return iq;
-        }
-
-
-        async function getVCard (_converse, jid) {
-            const to = Strophe.getBareJidFromJid(jid) === _converse.bare_jid ? null : jid;
-            let iq;
-            try {
-                iq = await api.sendIQ(createStanza("get", to))
-            } catch (iq) {
-                return {
-                    'stanza': iq,
-                    'jid': jid,
-                    'vcard_error': (new Date()).toISOString()
-                }
-            }
-            return onVCardData(jid, iq);
-        }
-
-
-        async function setVCardOnModel (model) {
-            let jid;
-            if (model instanceof _converse.Message) {
-                if (model.get('type') === 'error') {
-                    return;
-                }
-                jid = model.get('from');
-            } else {
-                jid = model.get('jid');
-            }
-            await api.waitUntil('VCardsInitialized');
-            model.vcard = _converse.vcards.findWhere({'jid': jid});
-            if (!model.vcard) {
-                model.vcard = _converse.vcards.create({'jid': jid});
-            }
-            model.vcard.on('change', () => model.trigger('vcard:change'));
-            model.trigger('vcard:add');
-        }
-
-
-        function getVCardForChatroomOccupant (message) {
-            const chatbox = message?.collection?.chatbox;
-            const nick = Strophe.getResourceFromJid(message.get('from'));
-
-            if (chatbox && chatbox.get('nick') === nick) {
-                return _converse.xmppstatus.vcard;
-            } else {
-                const jid = message.occupant && message.occupant.get('jid') || message.get('from');
-                if (jid) {
-                    return _converse.vcards.findWhere({jid}) || _converse.vcards.create({jid});
-                } else {
-                    log.error(`Could not assign VCard for message because no JID found! msgid: ${message.get('msgid')}`);
-                    return;
-                }
-            }
-        }
-
-
-        async function setVCardOnMUCMessage (message) {
-            await api.waitUntil('VCardsInitialized');
-            if (['error', 'info'].includes(message.get('type'))) {
-                return;
-            } else {
-                message.vcard = getVCardForChatroomOccupant(message);
-                message.vcard.on('change', () => message.trigger('vcard:change'));
-                message.trigger('vcard:add');
-            }
-        }
-
-
-        async function initVCardCollection () {
-            _converse.vcards = new _converse.VCards();
-            const id = `${_converse.bare_jid}-converse.vcards`;
-            initStorage(_converse.vcards, id);
-            await new Promise(resolve => {
-                _converse.vcards.fetch({
-                    'success': resolve,
-                    'error': resolve
-                }, {'silent': true});
-            });
-            const vcards = _converse.vcards;
-            if (_converse.session) {
-                const jid = _converse.session.get('bare_jid');
-                const status = _converse.xmppstatus;
-                status.vcard = vcards.findWhere({'jid': jid}) || vcards.create({'jid': jid});
-                if (status.vcard) {
-                    status.vcard.on('change', () => status.trigger('vcard:change'));
-                    status.trigger('vcard:add');
-                }
-            }
-            /**
-             * Triggered as soon as the `_converse.vcards` collection has been initialized and populated from cache.
-             * @event _converse#VCardsInitialized
-             */
-            api.trigger('VCardsInitialized');
-        }
-
-
-        function clearVCardsSession () {
-            if (_converse.shouldClearCache()) {
-                api.promises.add('VCardsInitialized');
-                if (_converse.vcards) {
-                    _converse.vcards.clearStore();
-                    delete _converse.vcards;
-                }
-            }
-        }
-
-
-        /************************ BEGIN Event Handlers ************************/
-
-        api.listen.on('chatBoxInitialized', m => setVCardOnModel(m));
-        api.listen.on('chatRoomInitialized', m => setVCardOnModel(m));
-        api.listen.on('chatRoomMessageInitialized', m => setVCardOnMUCMessage(m));
-        api.listen.on('addClientFeatures', () => api.disco.own.features.add(Strophe.NS.VCARD));
-        api.listen.on('clearSession', () => clearVCardsSession());
-        api.listen.on('messageInitialized', m => setVCardOnModel(m));
-        api.listen.on('rosterContactInitialized', m => setVCardOnModel(m));
-        api.listen.on('statusInitialized', initVCardCollection);
-
-
-        /************************ BEGIN API ************************/
-        Object.assign(_converse.api, {
-            /**
-             * The XEP-0054 VCard API
-             *
-             * This API lets you access and update user VCards
-             *
-             * @namespace _converse.api.vcard
-             * @memberOf _converse.api
-             */
-            'vcard': {
-                /**
-                 * Enables setting new values for a VCard.
-                 *
-                 * Sends out an IQ stanza to set the user's VCard and if
-                 * successful, it updates the {@link _converse.VCard}
-                 * for the passed in JID.
-                 *
-                 * @method _converse.api.vcard.set
-                 * @param {string} jid The JID for which the VCard should be set
-                 * @param {object} data A map of VCard keys and values
-                 * @example
-                 * let jid = _converse.bare_jid;
-                 * _converse.api.vcard.set( jid, {
-                 *     'fn': 'John Doe',
-                 *     'nickname': 'jdoe'
-                 * }).then(() => {
-                 *     // Succes
-                 * }).catch((e) => {
-                 *     // Failure, e is your error object
-                 * }).
-                 */
-                async set (jid, data) {
-                    if (!jid) {
-                        throw Error("No jid provided for the VCard data");
-                    }
-                    const div = document.createElement('div');
-                    const vcard_el = u.toStanza(`
-                        <vCard xmlns="vcard-temp">
-                            <FN>${data.fn}</FN>
-                            <NICKNAME>${data.nickname}</NICKNAME>
-                            <URL>${data.url}</URL>
-                            <ROLE>${data.role}</ROLE>
-                            <EMAIL><INTERNET/><PREF/><USERID>${data.email}</USERID></EMAIL>
-                            <PHOTO>
-                                <TYPE>${data.image_type}</TYPE>
-                                <BINVAL>${data.image}</BINVAL>
-                            </PHOTO>
-                        </vCard>`, div);
-                    let result;
-                    try {
-                        result = await api.sendIQ(createStanza("set", jid, vcard_el));
-                    } catch (e) {
-                        throw (e);
-                    }
-                    await api.vcard.update(jid, true);
-                    return result;
-                },
-
-                /**
-                 * @method _converse.api.vcard.get
-                 * @param {Model|string} model Either a `Model` instance, or a string JID.
-                 *     If a `Model` instance is passed in, then it must have either a `jid`
-                 *     attribute or a `muc_jid` attribute.
-                 * @param {boolean} [force] A boolean indicating whether the vcard should be
-                 *     fetched even if it's been fetched before.
-                 * @returns {promise} A Promise which resolves with the VCard data for a particular JID or for
-                 *     a `Model` instance which represents an entity with a JID (such as a roster contact,
-                 *     chat or chatroom occupant).
-                 *
-                 * @example
-                 * _converse.api.waitUntil('rosterContactsFetched').then(() => {
-                 *     _converse.api.vcard.get('someone@example.org').then(
-                 *         (vcard) => {
-                 *             // Do something with the vcard...
-                 *         }
-                 *     );
-                 * });
-                 */
-                 get (model, force) {
-                    if (typeof model === 'string') {
-                        return getVCard(_converse, model);
-                    } else if (force ||
-                            !model.get('vcard_updated') ||
-                            !dayjs(model.get('vcard_error')).isSame(new Date(), "day")) {
-
-                        const jid = model.get('jid');
-                        if (!jid) {
-                            log.error("No JID to get vcard for");
-                        }
-                        return getVCard(_converse, jid);
-                    } else {
-                        return Promise.resolve({});
-                    }
-                },
-
-                /**
-                 * Fetches the VCard associated with a particular `Model` instance
-                 * (by using its `jid` or `muc_jid` attribute) and then updates the model with the
-                 * returned VCard data.
-                 *
-                 * @method _converse.api.vcard.update
-                 * @param {Model} model A `Model` instance
-                 * @param {boolean} [force] A boolean indicating whether the vcard should be
-                 *     fetched again even if it's been fetched before.
-                 * @returns {promise} A promise which resolves once the update has completed.
-                 * @example
-                 * _converse.api.waitUntil('rosterContactsFetched').then(async () => {
-                 *     const chatbox = await _converse.chatboxes.getChatBox('someone@example.org');
-                 *     _converse.api.vcard.update(chatbox);
-                 * });
-                 */
-                async update (model, force) {
-                    const data = await this.get(model, force);
-                    model = typeof model === 'string' ? _converse.vcards.findWhere({'jid': model}) : model;
-                    if (!model) {
-                        log.error(`Could not find a VCard model for ${model}`);
-                        return;
-                    }
-                    delete data['stanza']
-                    model.save(data);
-                }
-            }
-        });
-    }
-});

+ 129 - 0
src/headless/plugins/vcard/api.js

@@ -0,0 +1,129 @@
+import log from "@converse/headless/log";
+import { _converse, api, converse } from "../../core.js";
+import { createStanza, getVCard } from './utils.js';
+
+const { dayjs, u } = converse.env;
+
+export default {
+    /**
+     * The XEP-0054 VCard API
+     *
+     * This API lets you access and update user VCards
+     *
+     * @namespace _converse.api.vcard
+     * @memberOf _converse.api
+     */
+    'vcard': {
+        /**
+         * Enables setting new values for a VCard.
+         *
+         * Sends out an IQ stanza to set the user's VCard and if
+         * successful, it updates the {@link _converse.VCard}
+         * for the passed in JID.
+         *
+         * @method _converse.api.vcard.set
+         * @param {string} jid The JID for which the VCard should be set
+         * @param {object} data A map of VCard keys and values
+         * @example
+         * let jid = _converse.bare_jid;
+         * _converse.api.vcard.set( jid, {
+         *     'fn': 'John Doe',
+         *     'nickname': 'jdoe'
+         * }).then(() => {
+         *     // Succes
+         * }).catch((e) => {
+         *     // Failure, e is your error object
+         * }).
+         */
+        async set (jid, data) {
+            if (!jid) {
+                throw Error("No jid provided for the VCard data");
+            }
+            const div = document.createElement('div');
+            const vcard_el = u.toStanza(`
+                <vCard xmlns="vcard-temp">
+                    <FN>${data.fn}</FN>
+                    <NICKNAME>${data.nickname}</NICKNAME>
+                    <URL>${data.url}</URL>
+                    <ROLE>${data.role}</ROLE>
+                    <EMAIL><INTERNET/><PREF/><USERID>${data.email}</USERID></EMAIL>
+                    <PHOTO>
+                        <TYPE>${data.image_type}</TYPE>
+                        <BINVAL>${data.image}</BINVAL>
+                    </PHOTO>
+                </vCard>`, div);
+            let result;
+            try {
+                result = await api.sendIQ(createStanza("set", jid, vcard_el));
+            } catch (e) {
+                throw (e);
+            }
+            await api.vcard.update(jid, true);
+            return result;
+        },
+
+        /**
+         * @method _converse.api.vcard.get
+         * @param {Model|string} model Either a `Model` instance, or a string JID.
+         *     If a `Model` instance is passed in, then it must have either a `jid`
+         *     attribute or a `muc_jid` attribute.
+         * @param {boolean} [force] A boolean indicating whether the vcard should be
+         *     fetched even if it's been fetched before.
+         * @returns {promise} A Promise which resolves with the VCard data for a particular JID or for
+         *     a `Model` instance which represents an entity with a JID (such as a roster contact,
+         *     chat or chatroom occupant).
+         *
+         * @example
+         * _converse.api.waitUntil('rosterContactsFetched').then(() => {
+         *     _converse.api.vcard.get('someone@example.org').then(
+         *         (vcard) => {
+         *             // Do something with the vcard...
+         *         }
+         *     );
+         * });
+         */
+         get (model, force) {
+            if (typeof model === 'string') {
+                return getVCard(_converse, model);
+            } else if (force ||
+                    !model.get('vcard_updated') ||
+                    !dayjs(model.get('vcard_error')).isSame(new Date(), "day")) {
+
+                const jid = model.get('jid');
+                if (!jid) {
+                    log.error("No JID to get vcard for");
+                }
+                return getVCard(_converse, jid);
+            } else {
+                return Promise.resolve({});
+            }
+        },
+
+        /**
+         * Fetches the VCard associated with a particular `Model` instance
+         * (by using its `jid` or `muc_jid` attribute) and then updates the model with the
+         * returned VCard data.
+         *
+         * @method _converse.api.vcard.update
+         * @param {Model} model A `Model` instance
+         * @param {boolean} [force] A boolean indicating whether the vcard should be
+         *     fetched again even if it's been fetched before.
+         * @returns {promise} A promise which resolves once the update has completed.
+         * @example
+         * _converse.api.waitUntil('rosterContactsFetched').then(async () => {
+         *     const chatbox = await _converse.chatboxes.getChatBox('someone@example.org');
+         *     _converse.api.vcard.update(chatbox);
+         * });
+         */
+        async update (model, force) {
+            const data = await this.get(model, force);
+            model = typeof model === 'string' ? _converse.vcards.findWhere({'jid': model}) : model;
+            if (!model) {
+                log.error(`Could not find a VCard model for ${model}`);
+                return;
+            }
+            delete data['stanza']
+            model.save(data);
+        }
+    }
+}

+ 96 - 0
src/headless/plugins/vcard/index.js

@@ -0,0 +1,96 @@
+/**
+ * @copyright The Converse.js contributors
+ * @license Mozilla Public License (MPLv2)
+ */
+import "../status";
+import VCard from './vcard.js';
+import vcard_api from './api.js';
+import { Collection } from "@converse/skeletor/src/collection";
+import { _converse, api, converse } from "../../core.js";
+import {
+    clearVCardsSession,
+    initVCardCollection,
+    setVCardOnMUCMessage,
+    setVCardOnModel,
+    setVCardOnOccupant,
+} from './utils.js';
+
+const { Strophe } = converse.env;
+
+
+converse.plugins.add('converse-vcard', {
+
+    dependencies: ["converse-status", "converse-roster"],
+
+    overrides: {
+        XMPPStatus: {
+            getNickname () {
+                const { _converse } = this.__super__;
+                const nick = this.__super__.getNickname.apply(this);
+                if (!nick && _converse.xmppstatus.vcard) {
+                    return _converse.xmppstatus.vcard.get('nickname');
+                } else {
+                    return nick;
+                }
+            },
+
+            getFullname () {
+                const { _converse } = this.__super__;
+                const fullname = this.__super__.getFullname.apply(this);
+                if (!fullname && _converse.xmppstatus.vcard) {
+                    return _converse.xmppstatus.vcard.get('fullname');
+                } else {
+                    return fullname;
+                }
+            }
+        },
+
+        RosterContact: {
+            getDisplayName () {
+                if (!this.get('nickname') && this.vcard) {
+                    return this.vcard.getDisplayName();
+                } else {
+                    return this.__super__.getDisplayName.apply(this);
+                }
+            },
+            getFullname () {
+                if (this.vcard) {
+                    return this.vcard.get('fullname');
+                } else {
+                    return this.__super__.getFullname.apply(this);
+                }
+            }
+        }
+    },
+
+    initialize () {
+        /* The initialize function gets called as soon as the plugin is
+         * loaded by converse.js's plugin machinery.
+         */
+        api.promises.add('VCardsInitialized');
+
+        _converse.VCard = VCard;
+
+        _converse.VCards = Collection.extend({
+            model: _converse.VCard,
+            initialize () {
+                this.on('add', vcard => (vcard.get('jid') && api.vcard.update(vcard)));
+            }
+        });
+
+        api.listen.on('chatRoomInitialized', m => {
+            setVCardOnModel(m)
+            m.occupants.forEach(setVCardOnOccupant);
+            m.listenTo(m.occupants, 'add', setVCardOnOccupant);
+        });
+        api.listen.on('chatBoxInitialized', m => setVCardOnModel(m));
+        api.listen.on('chatRoomMessageInitialized', m => setVCardOnMUCMessage(m));
+        api.listen.on('addClientFeatures', () => api.disco.own.features.add(Strophe.NS.VCARD));
+        api.listen.on('clearSession', () => clearVCardsSession());
+        api.listen.on('messageInitialized', m => setVCardOnModel(m));
+        api.listen.on('rosterContactInitialized', m => setVCardOnModel(m));
+        api.listen.on('statusInitialized', initVCardCollection);
+
+        Object.assign(_converse.api, vcard_api);
+    }
+});

+ 152 - 0
src/headless/plugins/vcard/utils.js

@@ -0,0 +1,152 @@
+import log from "@converse/headless/log";
+import { _converse, api, converse } from "../../core.js";
+import { initStorage } from '@converse/headless/utils/storage.js';
+
+const { Strophe, $iq, u } = converse.env;
+
+
+async function onVCardData (jid, iq) {
+    const vcard = iq.querySelector('vCard');
+    let result = {};
+    if (vcard !== null) {
+        result = {
+            'stanza': iq,
+            'fullname': vcard.querySelector('FN')?.textContent,
+            'nickname': vcard.querySelector('NICKNAME')?.textContent,
+            'image': vcard.querySelector('PHOTO BINVAL')?.textContent,
+            'image_type': vcard.querySelector('PHOTO TYPE')?.textContent,
+            'url': vcard.querySelector('URL')?.textContent,
+            'role': vcard.querySelector('ROLE')?.textContent,
+            'email': vcard.querySelector('EMAIL USERID')?.textContent,
+            'vcard_updated': (new Date()).toISOString(),
+            'vcard_error': 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;
+}
+
+
+export function createStanza (type, jid, vcard_el) {
+    const iq = $iq(jid ? {'type': type, 'to': jid} : {'type': type});
+    if (!vcard_el) {
+        iq.c("vCard", {'xmlns': Strophe.NS.VCARD});
+    } else {
+        iq.cnode(vcard_el);
+    }
+    return iq;
+}
+
+
+export async function setVCardOnModel (model) {
+    let jid;
+    if (model instanceof _converse.Message) {
+        if (model.get('type') === 'error') {
+            return;
+        }
+        jid = model.get('from');
+    } else {
+        jid = model.get('jid');
+    }
+    await api.waitUntil('VCardsInitialized');
+    model.vcard = _converse.vcards.findWhere({'jid': jid});
+    if (!model.vcard) {
+        model.vcard = _converse.vcards.create({'jid': jid});
+    }
+    model.vcard.on('change', () => model.trigger('vcard:change'));
+    model.trigger('vcard:add');
+}
+
+
+function getVCardForChatroomOccupant (message) {
+    const chatbox = message?.collection?.chatbox;
+    const nick = Strophe.getResourceFromJid(message.get('from'));
+
+    if (chatbox && chatbox.get('nick') === nick) {
+        return _converse.xmppstatus.vcard;
+    } else {
+        const jid = message.occupant && message.occupant.get('jid') || message.get('from');
+        if (jid) {
+            return _converse.vcards.findWhere({jid}) || _converse.vcards.create({jid});
+        } else {
+            log.error(`Could not assign VCard for message because no JID found! msgid: ${message.get('msgid')}`);
+            return;
+        }
+    }
+}
+
+export async function setVCardOnOccupant (occupant) {
+    await api.waitUntil('VCardsInitialized');
+    occupant.vcard = getVCardForChatroomOccupant(occupant);
+    occupant.vcard.on('change', () => occupant.trigger('vcard:change'));
+    occupant.trigger('vcard:add');
+}
+
+export async function setVCardOnMUCMessage (message) {
+    if (['error', 'info'].includes(message.get('type'))) {
+        return;
+    } else {
+        await api.waitUntil('VCardsInitialized');
+        message.vcard = getVCardForChatroomOccupant(message);
+        message.vcard.on('change', () => message.trigger('vcard:change'));
+        message.trigger('vcard:add');
+    }
+}
+
+
+export async function initVCardCollection () {
+    _converse.vcards = new _converse.VCards();
+    const id = `${_converse.bare_jid}-converse.vcards`;
+    initStorage(_converse.vcards, id);
+    await new Promise(resolve => {
+        _converse.vcards.fetch({
+            'success': resolve,
+            'error': resolve
+        }, {'silent': true});
+    });
+    const vcards = _converse.vcards;
+    if (_converse.session) {
+        const jid = _converse.session.get('bare_jid');
+        const status = _converse.xmppstatus;
+        status.vcard = vcards.findWhere({'jid': jid}) || vcards.create({'jid': jid});
+        if (status.vcard) {
+            status.vcard.on('change', () => status.trigger('vcard:change'));
+            status.trigger('vcard:add');
+        }
+    }
+    /**
+        * Triggered as soon as the `_converse.vcards` collection has been initialized and populated from cache.
+        * @event _converse#VCardsInitialized
+        */
+    api.trigger('VCardsInitialized');
+}
+
+
+export function clearVCardsSession () {
+    if (_converse.shouldClearCache()) {
+        api.promises.add('VCardsInitialized');
+        if (_converse.vcards) {
+            _converse.vcards.clearStore();
+            delete _converse.vcards;
+        }
+    }
+}
+
+export async function getVCard (_converse, jid) {
+    const to = Strophe.getBareJidFromJid(jid) === _converse.bare_jid ? null : jid;
+    let iq;
+    try {
+        iq = await api.sendIQ(createStanza("get", to))
+    } catch (iq) {
+        return {
+            'stanza': iq,
+            'jid': jid,
+            'vcard_error': (new Date()).toISOString()
+        }
+    }
+    return onVCardData(jid, iq);
+}

+ 40 - 0
src/headless/plugins/vcard/vcard.js

@@ -0,0 +1,40 @@
+import { Model } from '@converse/skeletor/src/model.js';
+import { _converse } from "../../core.js";
+
+ /**
+  * Represents a VCard
+  * @class
+  * @namespace _converse.VCard
+  * @memberOf _converse
+  */
+ const VCard = Model.extend({
+     defaults: {
+         'image': _converse.DEFAULT_IMAGE,
+         'image_type': _converse.DEFAULT_IMAGE_TYPE
+     },
+
+     set (key, val, options) {
+         // Override Model.prototype.set to make sure that the
+         // default `image` and `image_type` values are maintained.
+         let attrs;
+         if (typeof key === 'object') {
+             attrs = key;
+             options = val;
+         } else {
+             (attrs = {})[key] = val;
+         }
+         if ('image' in attrs && !attrs['image']) {
+             attrs['image'] = _converse.DEFAULT_IMAGE;
+             attrs['image_type'] = _converse.DEFAULT_IMAGE_TYPE;
+             return Model.prototype.set.call(this, attrs, options);
+         } else {
+             return Model.prototype.set.apply(this, arguments);
+         }
+     },
+
+     getDisplayName () {
+         return this.get('nickname') || this.get('fullname') || this.get('jid');
+     }
+ });
+
+export default VCard;