Avoid race-condition that destroys vcards

VCards were being created before `fetch` was completed, so once fetch
was done those VCards were unset from their collection.

Add a new event and promise `VCardsInitialized` that triggers after
successful fetching and wait for it before creating VCards.
This commit is contained in:
JC Brand 2019-10-24 17:55:20 +02:00
parent 1fa203c990
commit 17dfa3d7ba
10 changed files with 118 additions and 104 deletions

View File

@ -722,10 +722,8 @@
var view = _converse.chatboxviews.get(sender_jid); var view = _converse.chatboxviews.get(sender_jid);
expect(view).toBeDefined(); expect(view).toBeDefined();
await u.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1]) const event = await u.waitUntil(() => view.el.querySelector('.chat-state-notification'));
// Check that the notification appears inside the chatbox in the DOM expect(event.textContent).toEqual(mock.cur_names[1] + ' is typing');
let events = view.el.querySelectorAll('.chat-state-notification');
expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
// Check that it doesn't appear twice // Check that it doesn't appear twice
msg = $msg({ msg = $msg({
@ -735,7 +733,7 @@
id: (new Date()).getTime() id: (new Date()).getTime()
}).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await _converse.chatboxes.onMessage(msg); await _converse.chatboxes.onMessage(msg);
events = view.el.querySelectorAll('.chat-state-notification'); const events = view.el.querySelectorAll('.chat-state-notification');
expect(events.length).toBe(1); expect(events.length).toBe(1);
expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing'); expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
done(); done();
@ -778,7 +776,8 @@
expect(msg_obj.get('sender')).toEqual('me'); expect(msg_obj.get('sender')).toEqual('me');
expect(msg_obj.get('is_delayed')).toEqual(false); expect(msg_obj.get('is_delayed')).toEqual(false);
const chat_content = chatboxview.el.querySelector('.chat-content'); const chat_content = chatboxview.el.querySelector('.chat-content');
const status_text = chat_content.querySelector('.chat-info.chat-state-notification').textContent; const el = await u.waitUntil(() => chat_content.querySelector('.chat-info.chat-state-notification'));
const status_text = el.textContent;
expect(status_text).toBe('Typing from another device'); expect(status_text).toBe('Typing from another device');
done(); done();
})); }));
@ -863,7 +862,7 @@
await _converse.chatboxes.onMessage(msg); await _converse.chatboxes.onMessage(msg);
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
await u.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1]) await u.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1])
var event = view.el.querySelector('.chat-info.chat-state-notification'); const event = await u.waitUntil(() => view.el.querySelector('.chat-state-notification'));
expect(event.textContent).toEqual(mock.cur_names[1] + ' has stopped typing'); expect(event.textContent).toEqual(mock.cur_names[1] + ' has stopped typing');
done(); done();
})); }));
@ -904,9 +903,8 @@
const msg_obj = chatbox.messages.models[0]; const msg_obj = chatbox.messages.models[0];
expect(msg_obj.get('sender')).toEqual('me'); expect(msg_obj.get('sender')).toEqual('me');
expect(msg_obj.get('is_delayed')).toEqual(false); expect(msg_obj.get('is_delayed')).toEqual(false);
const chat_content = chatboxview.el.querySelector('.chat-content'); const el = await u.waitUntil(() => chatboxview.el.querySelector('.chat-info.chat-state-notification'));
const status_text = chat_content.querySelector('.chat-info.chat-state-notification').textContent; expect(el.textContent).toBe('Stopped typing on the other device');
expect(status_text).toBe('Stopped typing on the other device');
done(); done();
})); }));
}); });
@ -1045,7 +1043,7 @@
.c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up() .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
.tree(); .tree();
await _converse.chatboxes.onMessage(msg); await _converse.chatboxes.onMessage(msg);
await u.waitUntil(() => view.model.messages.length); await u.waitUntil(() => view.el.querySelector('.chat-state-notification'));
expect(view.el.querySelectorAll('.chat-state-notification').length).toBe(1); expect(view.el.querySelectorAll('.chat-state-notification').length).toBe(1);
msg = $msg({ msg = $msg({
from: sender_jid, from: sender_jid,
@ -1056,7 +1054,7 @@
await _converse.chatboxes.onMessage(msg); await _converse.chatboxes.onMessage(msg);
await u.waitUntil(() => (view.model.messages.length > 1)); await u.waitUntil(() => (view.model.messages.length > 1));
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
expect(view.el.querySelectorAll('.chat-state-notification').length).toBe(0); await u.waitUntil(() => view.el.querySelectorAll('.chat-state-notification').length === 0);
done(); done();
})); }));
}); });
@ -1084,7 +1082,7 @@
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
const view = _converse.chatboxviews.get(sender_jid); const view = _converse.chatboxviews.get(sender_jid);
await u.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1]); await u.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1]);
const event = view.el.querySelector('.chat-state-notification'); const event = await u.waitUntil(() => view.el.querySelector('.chat-state-notification'));
expect(event.textContent).toEqual(mock.cur_names[1] + ' has gone away'); expect(event.textContent).toEqual(mock.cur_names[1] + ' has gone away');
done(); done();
})); }));

View File

@ -165,7 +165,7 @@
// A contact should now have been created // A contact should now have been created
expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy(); expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy();
expect(contact.get('jid')).toBe('contact@example.org'); expect(contact.get('jid')).toBe('contact@example.org');
expect(_converse.api.vcard.get).toHaveBeenCalled(); await u.waitUntil(() => contact.initialized);
/* To subscribe to the contact's presence information, /* To subscribe to the contact's presence information,
* the user's client MUST send a presence stanza of * the user's client MUST send a presence stanza of

View File

@ -601,7 +601,7 @@
it("do not have a header if there aren't any", it("do not have a header if there aren't any",
mock.initConverse( mock.initConverse(
['rosterGroupsFetched'], {}, ['rosterGroupsFetched', 'VCardsInitialized'], {},
async function (done, _converse) { async function (done, _converse) {
await test_utils.openControlBox(_converse); await test_utils.openControlBox(_converse);
@ -613,16 +613,15 @@
ask: 'subscribe', ask: 'subscribe',
fullname: name fullname: name
}); });
spyOn(window, 'confirm').and.returnValue(true);
await u.waitUntil(() => { await u.waitUntil(() => {
const el = _converse.rosterview.get('Pending contacts').el; const el = _converse.rosterview.get('Pending contacts').el;
return u.isVisible(el) && _.filter(el.querySelectorAll('li'), li => u.isVisible(li)).length; return u.isVisible(el) && _.filter(el.querySelectorAll('li'), li => u.isVisible(li)).length;
}, 700) }, 700)
spyOn(_converse.connection, 'sendIQ').and.callThrough(); const remove_el = await u.waitUntil(() => sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, _converse.rosterview.el).pop());
sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, _converse.rosterview.el).pop().click(); spyOn(window, 'confirm').and.returnValue(true);
remove_el.click();
expect(window.confirm).toHaveBeenCalled(); expect(window.confirm).toHaveBeenCalled();
expect(_converse.connection.sendIQ).toHaveBeenCalled();
const iq = _converse.connection.IQ_stanzas.pop(); const iq = _converse.connection.IQ_stanzas.pop();
expect(Strophe.serialize(iq)).toBe( expect(Strophe.serialize(iq)).toBe(
@ -739,7 +738,7 @@
ask: null, ask: null,
fullname: name fullname: name
}); });
return u.waitUntil(() => contact.vcard.get('fullname')); return u.waitUntil(() => contact.initialized);
})); }));
await u.waitUntil(() => sizzle('li', _converse.rosterview.el).length, 600); await u.waitUntil(() => sizzle('li', _converse.rosterview.el).length, 600);
// Check that they are sorted alphabetically // Check that they are sorted alphabetically
@ -1041,7 +1040,7 @@
requesting: true, requesting: true,
nickname: name nickname: name
}); });
return u.waitUntil(() => contact.vcard.get('fullname')); return u.waitUntil(() => contact.initialized);
})); }));
await u.waitUntil(() => _converse.rosterview.get('Contact requests').el.querySelectorAll('li').length, 700); await u.waitUntil(() => _converse.rosterview.get('Contact requests').el.querySelectorAll('li').length, 700);
expect(_converse.rosterview.update).toHaveBeenCalled(); expect(_converse.rosterview.update).toHaveBeenCalled();

View File

@ -701,6 +701,7 @@ converse.plugins.add('converse-chatview', {
* @param { _converse.Message } message - The message object * @param { _converse.Message } message - The message object
*/ */
async showMessage (message) { async showMessage (message) {
await message.initialized;
const view = this.add(message.get('id'), new _converse.MessageView({'model': message})); const view = this.add(message.get('id'), new _converse.MessageView({'model': message}));
await view.render(); await view.render();
// Clear chat state notifications // Clear chat state notifications

View File

@ -310,7 +310,7 @@ converse.plugins.add('converse-profile', {
/******************** Event Handlers ********************/ /******************** Event Handlers ********************/
_converse.api.listen.on('controlBoxPaneInitialized', async view => { _converse.api.listen.on('controlBoxPaneInitialized', async view => {
await _converse.api.waitUntil('statusInitialized'); await _converse.api.waitUntil('VCardsInitialized');
_converse.xmppstatusview = new _converse.XMPPStatusView({'model': _converse.xmppstatus}); _converse.xmppstatusview = new _converse.XMPPStatusView({'model': _converse.xmppstatus});
view.el.insertAdjacentElement('afterBegin', _converse.xmppstatusview.render().el); view.el.insertAdjacentElement('afterBegin', _converse.xmppstatusview.render().el);
}); });

View File

@ -336,7 +336,8 @@ converse.plugins.add('converse-rosterview', {
"click .remove-xmpp-contact": "removeContact" "click .remove-xmpp-contact": "removeContact"
}, },
initialize () { async initialize () {
await this.model.initialized;
this.listenTo(this.model, "change", this.render); this.listenTo(this.model, "change", this.render);
this.listenTo(this.model, "highlight", this.highlight); this.listenTo(this.model, "highlight", this.highlight);
this.listenTo(this.model, "destroy", this.remove); this.listenTo(this.model, "destroy", this.remove);
@ -345,6 +346,7 @@ converse.plugins.add('converse-rosterview', {
this.listenTo(this.model.presence, "change:show", this.render); this.listenTo(this.model.presence, "change:show", this.render);
this.listenTo(this.model.vcard, 'change:fullname', this.render); this.listenTo(this.model.vcard, 'change:fullname', this.render);
this.render();
}, },
render () { render () {
@ -978,7 +980,7 @@ converse.plugins.add('converse-rosterview', {
}); });
function initRoster () { function initRosterView () {
/* Create an instance of RosterView once the RosterGroups /* Create an instance of RosterView once the RosterGroups
* collection has been created (in @converse/headless/converse-core.js) * collection has been created (in @converse/headless/converse-core.js)
*/ */
@ -996,8 +998,8 @@ converse.plugins.add('converse-rosterview', {
*/ */
_converse.api.trigger('rosterViewInitialized'); _converse.api.trigger('rosterViewInitialized');
} }
_converse.api.listen.on('rosterInitialized', initRoster); _converse.api.listen.on('rosterInitialized', initRosterView);
_converse.api.listen.on('rosterReadyAfterReconnection', initRoster); _converse.api.listen.on('rosterReadyAfterReconnection', initRosterView);
_converse.api.listen.on('afterTearDown', () => { _converse.api.listen.on('afterTearDown', () => {
if (converse.rosterview) { if (converse.rosterview) {

View File

@ -123,11 +123,11 @@ converse.plugins.add('converse-chatboxes', {
}; };
}, },
initialize () { async initialize () {
this.initialized = u.getResolveablePromise();
ModelWithContact.prototype.initialize.apply(this, arguments); ModelWithContact.prototype.initialize.apply(this, arguments);
if (this.get('type') === 'chat') { if (this.get('type') === 'chat') {
this.setVCard();
this.setRosterContact(Strophe.getBareJidFromJid(this.get('from'))); this.setRosterContact(Strophe.getBareJidFromJid(this.get('from')));
} }
@ -137,6 +137,8 @@ converse.plugins.add('converse-chatboxes', {
if (this.isEphemeral()) { if (this.isEphemeral()) {
window.setTimeout(this.safeDestroy.bind(this), 10000); window.setTimeout(this.safeDestroy.bind(this), 10000);
} }
await _converse.api.trigger('messageInitialized', this, {'Synchronous': true});
this.initialized.resolve();
}, },
safeDestroy () { safeDestroy () {
@ -147,19 +149,6 @@ converse.plugins.add('converse-chatboxes', {
} }
}, },
setVCard () {
if (!_converse.vcards) {
// VCards aren't supported
return;
}
if (this.get('type') === 'error') {
return;
} else {
const jid = Strophe.getBareJidFromJid(this.get('from'));
this.vcard = _converse.vcards.findWhere({'jid': jid}) || _converse.vcards.create({'jid': jid});
}
},
isOnlyChatStateNotification () { isOnlyChatStateNotification () {
return u.isOnlyChatStateNotification(this); return u.isOnlyChatStateNotification(this);
}, },

View File

@ -340,10 +340,10 @@ converse.plugins.add('converse-muc', {
} }
}, },
setVCard () { async setVCard () {
await _converse.api.waitUntil('VCardsInitialized');
if (!_converse.vcards) { if (!_converse.vcards) {
// VCards aren't supported return; // VCards aren't supported
return;
} }
if (['error', 'info'].includes(this.get('type'))) { if (['error', 'info'].includes(this.get('type'))) {
return; return;
@ -403,10 +403,7 @@ converse.plugins.add('converse-muc', {
}, },
async initialize() { async initialize() {
if (_converse.vcards) { this.setVCard();
this.vcard = _converse.vcards.findWhere({'jid': this.get('jid')}) ||
_converse.vcards.create({'jid': this.get('jid')});
}
this.set('box_id', `box-${btoa(this.get('jid'))}`); this.set('box_id', `box-${btoa(this.get('jid'))}`);
this.initFeatures(); // sendChatState depends on this.features this.initFeatures(); // sendChatState depends on this.features
@ -421,6 +418,14 @@ converse.plugins.add('converse-muc', {
this.enterRoom(); this.enterRoom();
}, },
async setVCard () {
await _converse.api.waitUntil('VCardsInitialized');
if (_converse.vcards) {
this.vcard = _converse.vcards.findWhere({'jid': this.get('jid')}) ||
_converse.vcards.create({'jid': this.get('jid')});
}
},
async enterRoom () { async enterRoom () {
const conn_status = this.get('connection_status'); const conn_status = this.get('connection_status');
_converse.log( _converse.log(

View File

@ -74,39 +74,6 @@ converse.plugins.add('converse-roster', {
}; };
/**
* Initialize the Bakcbone collections that represent the contats
* roster and the roster groups.
* @private
* @method _converse.initRoster
*/
_converse.initRoster = function () {
const storage = _converse.config.get('storage');
_converse.roster = new _converse.RosterContacts();
let id = `converse.contacts-${_converse.bare_jid}`;
_converse.roster.browserStorage = _converse.createStore(id, storage);
_converse.roster.data = new Backbone.Model();
id = `converse-roster-model-${_converse.bare_jid}`;
_converse.roster.data.id = id;
_converse.roster.data.browserStorage = _converse.createStore(id, storage);
_converse.roster.data.fetch();
id = `converse.roster.groups${_converse.bare_jid}`;
_converse.rostergroups = new _converse.RosterGroups();
_converse.rostergroups.browserStorage = _converse.createStore(id, storage);
/**
* Triggered once the `_converse.RosterContacts` and `_converse.RosterGroups` have
* been created, but not yet populated with data.
* This event is useful when you want to create views for these collections.
* @event _converse#chatBoxMaximized
* @example _converse.api.listen.on('rosterInitialized', () => { ... });
* @example _converse.api.waitUntil('rosterInitialized').then(() => { ... });
*/
_converse.api.trigger('rosterInitialized');
};
_converse.sendInitialPresence = function () { _converse.sendInitialPresence = function () {
if (_converse.send_initial_presence) { if (_converse.send_initial_presence) {
_converse.xmppstatus.sendPresence(); _converse.xmppstatus.sendPresence();
@ -243,6 +210,7 @@ converse.plugins.add('converse-roster', {
}, },
async initialize (attributes) { async initialize (attributes) {
this.initialized = u.getResolveablePromise();
this.setPresence(); this.setPresence();
const { jid } = attributes; const { jid } = attributes;
const bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase(); const bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase();
@ -268,6 +236,7 @@ converse.plugins.add('converse-roster', {
* @param { _converse.RosterContact } contact * @param { _converse.RosterContact } contact
*/ */
await _converse.api.trigger('rosterContactInitialized', this, {'Synchronous': true}); await _converse.api.trigger('rosterContactInitialized', this, {'Synchronous': true});
this.initialized.resolve();
}, },
setPresence () { setPresence () {
@ -995,7 +964,36 @@ converse.plugins.add('converse-roster', {
}); });
_converse.api.listen.on('presencesInitialized', (reconnecting) => { async function initRoster () {
// Initialize the Bakcbone collections that represent the contats
// roster and the roster groups.
await _converse.api.waitUntil('VCardsInitialized');
const storage = _converse.config.get('storage');
_converse.roster = new _converse.RosterContacts();
let id = `converse.contacts-${_converse.bare_jid}`;
_converse.roster.browserStorage = _converse.createStore(id, storage);
_converse.roster.data = new Backbone.Model();
id = `converse-roster-model-${_converse.bare_jid}`;
_converse.roster.data.id = id;
_converse.roster.data.browserStorage = _converse.createStore(id, storage);
_converse.roster.data.fetch();
id = `converse.roster.groups${_converse.bare_jid}`;
_converse.rostergroups = new _converse.RosterGroups();
_converse.rostergroups.browserStorage = _converse.createStore(id, storage);
/**
* Triggered once the `_converse.RosterContacts` and `_converse.RosterGroups` have
* been created, but not yet populated with data.
* This event is useful when you want to create views for these collections.
* @event _converse#chatBoxMaximized
* @example _converse.api.listen.on('rosterInitialized', () => { ... });
* @example _converse.api.waitUntil('rosterInitialized').then(() => { ... });
*/
_converse.api.trigger('rosterInitialized');
}
_converse.api.listen.on('presencesInitialized', async (reconnecting) => {
if (reconnecting) { if (reconnecting) {
/** /**
* Similar to `rosterInitialized`, but instead pertaining to reconnection. * Similar to `rosterInitialized`, but instead pertaining to reconnection.
@ -1006,7 +1004,7 @@ converse.plugins.add('converse-roster', {
*/ */
_converse.api.trigger('rosterReadyAfterReconnection'); _converse.api.trigger('rosterReadyAfterReconnection');
} else { } else {
_converse.initRoster(); await initRoster();
} }
_converse.roster.onConnected(); _converse.roster.onConnected();
_converse.registerPresenceHandler(); _converse.registerPresenceHandler();

View File

@ -65,6 +65,8 @@ converse.plugins.add('converse-vcard', {
*/ */
const { _converse } = this; const { _converse } = this;
_converse.api.promises.add('VCardsInitialized');
_converse.VCard = Backbone.Model.extend({ _converse.VCard = Backbone.Model.extend({
defaults: { defaults: {
@ -101,7 +103,9 @@ converse.plugins.add('converse-vcard', {
model: _converse.VCard, model: _converse.VCard,
initialize () { initialize () {
this.on('add', vcard => _converse.api.vcard.update(vcard)); this.on('add', vcard => {
_converse.api.vcard.update(vcard);
});
} }
}); });
@ -157,26 +161,36 @@ converse.plugins.add('converse-vcard', {
} }
/************************ BEGIN Event Handlers ************************/ /************************ BEGIN Event Handlers ************************/
_converse.initVCardCollection = function () { _converse.initVCardCollection = async function () {
_converse.vcards = new _converse.VCards(); _converse.vcards = new _converse.VCards();
const id = `${_converse.bare_jid}-converse.vcards`; _converse.vcards.browserStorage = _converse.createStore(`${_converse.bare_jid}-converse.vcards`);
_converse.vcards.browserStorage = _converse.createStore(id, _converse.config.get('storage')); await new Promise(resolve => {
_converse.vcards.fetch(); _converse.vcards.fetch({
} 'success': resolve,
'error': resolve
_converse.api.listen.on('statusInitialized', () => { }, {'silent': true});
_converse.initVCardCollection(); });
const vcards = _converse.vcards; const vcards = _converse.vcards;
if (_converse.session) { if (_converse.session) {
const jid = _converse.session.get('bare_jid'); const jid = _converse.session.get('bare_jid');
_converse.xmppstatus.vcard = vcards.findWhere({'jid': jid}) || vcards.create({'jid': jid}); _converse.xmppstatus.vcard = vcards.findWhere({'jid': jid}) || vcards.create({'jid': jid});
} }
}); /**
* Triggered as soon as the `_converse.vcards` collection has been initialized and populated from cache.
* @event _converse#VCardsInitialized
*/
_converse.api.trigger('VCardsInitialized');
}
_converse.api.listen.on('statusInitialized', _converse.initVCardCollection);
_converse.api.listen.on('clearSession', () => { _converse.api.listen.on('clearSession', () => {
if (_converse.shouldClearCache() && _converse.vcards) { if (_converse.shouldClearCache()) {
_converse.vcards.clearSession(); _converse.api.promises.add('VCardsInitialized');
delete _converse.vcards; if (_converse.vcards) {
_converse.vcards.clearSession();
delete _converse.vcards;
}
} }
}); });
@ -184,16 +198,24 @@ converse.plugins.add('converse-vcard', {
_converse.api.listen.on('addClientFeatures', () => _converse.api.disco.own.features.add(Strophe.NS.VCARD)); _converse.api.listen.on('addClientFeatures', () => _converse.api.disco.own.features.add(Strophe.NS.VCARD));
function setVCardOnModel (model) { async function setVCardOnModel (model) {
// TODO: if we can make this method async and wait for the VCard to let jid;
// be updated, then we'll avoid unnecessary re-rendering of roster contacts. if (model instanceof _converse.Message) {
const jid = model.get('jid'); if (model.get('type') === 'error') {
return;
}
jid = model.get('from');
} else {
jid = model.get('jid');
}
await _converse.api.waitUntil('VCardsInitialized');
model.vcard = _converse.vcards.findWhere({'jid': jid}); model.vcard = _converse.vcards.findWhere({'jid': jid});
if (!model.vcard) { if (!model.vcard) {
model.vcard = _converse.vcards.create({'jid': jid}); model.vcard = _converse.vcards.create({'jid': jid});
} }
} }
_converse.api.listen.on('rosterContactInitialized', contact => setVCardOnModel(contact)); _converse.api.listen.on('rosterContactInitialized', m => setVCardOnModel(m));
_converse.api.listen.on('messageInitialized', m => setVCardOnModel(m));
/************************ BEGIN API ************************/ /************************ BEGIN API ************************/
@ -286,9 +308,9 @@ converse.plugins.add('converse-vcard', {
* }); * });
*/ */
async update (model, force) { async update (model, force) {
const vcard = await this.get(model, force); const data = await this.get(model, force);
delete vcard['stanza'] delete data['stanza']
model.save(vcard); model.save(data);
} }
} }
}); });