Split vcard plugin up into multiple files

This commit is contained in:
JC Brand 2021-11-19 13:30:20 +01:00
parent c76e6fb86b
commit bdac6f1b47
6 changed files with 418 additions and 395 deletions

View File

@ -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";

View File

@ -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);
}
}
});
}
});

View File

@ -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);
}
}
}

View File

@ -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);
}
});

View File

@ -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);
}

View File

@ -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;