import log from '@converse/headless/log'; import { Model } from '@converse/skeletor/src/model.js'; import { _converse, api, converse } from '@converse/headless/core'; import { getOpenPromise } from '@converse/openpromise'; import { initStorage } from '@converse/headless/utils/storage.js'; import { restoreOMEMOSession } from './utils.js'; const { Strophe, $build, $iq, sizzle } = converse.env; /** * @class * @namespace _converse.DeviceList * @memberOf _converse */ const DeviceList = Model.extend({ idAttribute: 'jid', async initialize () { this.initialized = getOpenPromise(); await this.initDevices(); this.initialized.resolve(); }, initDevices () { this.devices = new _converse.Devices(); const id = `converse.devicelist-${_converse.bare_jid}-${this.get('jid')}`; initStorage(this.devices, id); return this.fetchDevices(); }, async onDevicesFound (collection) { if (collection.length === 0) { let ids = []; try { ids = await this.fetchDevicesFromServer(); } catch (e) { if (e === null) { log.error(`Timeout error while fetching devices for ${this.get('jid')}`); } else { log.error(`Could not fetch devices for ${this.get('jid')}`); log.error(e); } this.destroy(); } if (this.get('jid') === _converse.bare_jid) { this.publishCurrentDevice(ids); } } }, fetchDevices () { if (this._devices_promise === undefined) { this._devices_promise = new Promise(resolve => { this.devices.fetch({ 'success': c => resolve(this.onDevicesFound(c)), 'error': (_, e) => { log.error(e); resolve(); } }); }); } return this._devices_promise; }, async getOwnDeviceId () { let device_id = _converse.omemo_store.get('device_id'); if (!this.devices.get(device_id)) { // Generate a new bundle if we cannot find our device await _converse.omemo_store.generateBundle(); device_id = _converse.omemo_store.get('device_id'); } return device_id; }, async publishCurrentDevice (device_ids) { if (this.get('jid') !== _converse.bare_jid) { return; // We only publish for ourselves. } await restoreOMEMOSession(); if (!_converse.omemo_store) { // Happens during tests. The connection gets torn down // before publishCurrentDevice has time to finish. log.warn('publishCurrentDevice: omemo_store is not defined, likely a timing issue'); return; } if (!device_ids.includes(await this.getOwnDeviceId())) { return this.publishDevices(); } }, async fetchDevicesFromServer () { const stanza = $iq({ 'type': 'get', 'from': _converse.bare_jid, 'to': this.get('jid') }).c('pubsub', { 'xmlns': Strophe.NS.PUBSUB }) .c('items', { 'node': Strophe.NS.OMEMO_DEVICELIST }); const iq = await api.sendIQ(stanza); const selector = `list[xmlns="${Strophe.NS.OMEMO}"] device`; const device_ids = sizzle(selector, iq).map(d => d.getAttribute('id')); const jid = this.get('jid'); return Promise.all(device_ids.map(id => this.devices.create({ id, jid }, { 'promise': true }))); }, /** * Send an IQ stanza to the current user's "devices" PEP node to * ensure that all devices are published for potential chat partners to see. * See: https://xmpp.org/extensions/xep-0384.html#usecases-announcing */ publishDevices () { const item = $build('item', { 'id': 'current' }).c('list', { 'xmlns': Strophe.NS.OMEMO }); this.devices.filter(d => d.get('active')).forEach(d => item.c('device', { 'id': d.get('id') }).up()); const options = { 'pubsub#access_model': 'open' }; return api.pubsub.publish(null, Strophe.NS.OMEMO_DEVICELIST, item, options, false); }, async removeOwnDevices (device_ids) { if (this.get('jid') !== _converse.bare_jid) { throw new Error("Cannot remove devices from someone else's device list"); } await Promise.all(device_ids.map(id => this.devices.get(id)).map(d => new Promise(resolve => d.destroy({ 'success': resolve, 'error': (_, e) => { log.error(e); resolve(); } })) )); return this.publishDevices(); } }); export default DeviceList;