diff --git a/src/headless/headless.js b/src/headless/headless.js index cd89a308c..8794578e4 100644 --- a/src/headless/headless.js +++ b/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"; diff --git a/src/headless/plugins/vcard.js b/src/headless/plugins/vcard.js deleted file mode 100644 index a5c4e4205..000000000 --- a/src/headless/plugins/vcard.js +++ /dev/null @@ -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(` - - ${data.fn} - ${data.nickname} - ${data.url} - ${data.role} - ${data.email} - - ${data.image_type} - ${data.image} - - `, 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); - } - } - }); - } -}); diff --git a/src/headless/plugins/vcard/api.js b/src/headless/plugins/vcard/api.js new file mode 100644 index 000000000..46dda7e35 --- /dev/null +++ b/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(` + + ${data.fn} + ${data.nickname} + ${data.url} + ${data.role} + ${data.email} + + ${data.image_type} + ${data.image} + + `, 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); + } + } +} diff --git a/src/headless/plugins/vcard/index.js b/src/headless/plugins/vcard/index.js new file mode 100644 index 000000000..19b62f75f --- /dev/null +++ b/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); + } +}); diff --git a/src/headless/plugins/vcard/utils.js b/src/headless/plugins/vcard/utils.js new file mode 100644 index 000000000..6eb4f5685 --- /dev/null +++ b/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); +} diff --git a/src/headless/plugins/vcard/vcard.js b/src/headless/plugins/vcard/vcard.js new file mode 100644 index 000000000..72d19d27f --- /dev/null +++ b/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;