diff --git a/CHANGES.md b/CHANGES.md index c454c3c2b0..b5418fcd55 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -43,7 +43,8 @@ - Fix: trying to use emojis with an uppercase letter breaks the message field. - Fix: renaming getEmojisByAtrribute to getEmojisByAttribute. -### Changes +### Changes and features +- Add support for XEP-0191 Blocking Command - Upgrade to Bootstrap 5 - Add an occupants filter to the MUC sidebar - Change contacts filter to rename the anachronistic `Online` state to `Available`. diff --git a/conversejs.doap b/conversejs.doap index 337a472f0f..c9568db77a 100644 --- a/conversejs.doap +++ b/conversejs.doap @@ -125,6 +125,12 @@ + + + + 11.0.0 + + diff --git a/karma.conf.js b/karma.conf.js index 1ae20e72e4..6bd6bc88b8 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -24,6 +24,7 @@ module.exports = function(config) { }, { pattern: "src/shared/tests/mock.js", type: 'module' }, + { pattern: "src/headless/plugins/blocklist/tests/blocklist.js", type: 'module' }, { pattern: "src/headless/plugins/bookmarks/tests/bookmarks.js", type: 'module' }, { pattern: "src/headless/plugins/bookmarks/tests/deprecated.js", type: 'module' }, { pattern: "src/headless/plugins/caps/tests/caps.js", type: 'module' }, diff --git a/package-lock.json b/package-lock.json index b0bcaf86f5..c3fa29cd84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9868,8 +9868,8 @@ }, "node_modules/strophe.js": { "version": "3.1.0", - "resolved": "git+ssh://git@github.com/strophe/strophejs.git#9b643a43b5cd79f0d8a2e0015e9d2c0d0fec8578", - "integrity": "sha512-/T9ptJEZPy98XaGaFoNfvrzcRQDafRDjSY/Kq3Gk3ilMEmD2oDGslyIdFFYxjAgU6ON6Z1e0Z9CnzgXggqHtFQ==", + "resolved": "git+ssh://git@github.com/strophe/strophejs.git#b4f3369ba07dee4f04750de6709ea8ebe8adf9a5", + "integrity": "sha512-M/T9Pio3eG7GUzVmQUSNg+XzjFwQ6qhzI+Z3uSwUIItxxpRIB8lQB2Afb0L7lbQiRYB7/9tS03GxksdqjfrS5g==", "license": "MIT", "optionalDependencies": { "@types/jsdom": "^21.1.7", @@ -11121,7 +11121,7 @@ "pluggable.js": "3.0.1", "sizzle": "^2.3.5", "sprintf-js": "^1.1.2", - "strophe.js": "strophe/strophejs#9b643a43b5cd79f0d8a2e0015e9d2c0d0fec8578", + "strophe.js": "strophe/strophejs#b4f3369ba07dee4f04750de6709ea8ebe8adf9a5", "urijs": "^1.19.10" }, "devDependencies": {} diff --git a/src/headless/index.js b/src/headless/index.js index 79fa78c1b2..f056e26fa1 100644 --- a/src/headless/index.js +++ b/src/headless/index.js @@ -15,6 +15,7 @@ import log from './log.js'; export { EmojiPicker } from './plugins/emoji/index.js'; export { Bookmark, Bookmarks } from './plugins/bookmarks/index.js'; // XEP-0199 XMPP Ping +import './plugins/blocklist/index.js'; import './plugins/bosh/index.js'; // XEP-0206 BOSH import './plugins/caps/index.js'; // XEP-0115 Entity Capabilities export { ChatBox, Message, Messages } from './plugins/chat/index.js'; // RFC-6121 Instant messaging diff --git a/src/headless/package.json b/src/headless/package.json index 3c61030d40..1a5f7de063 100644 --- a/src/headless/package.json +++ b/src/headless/package.json @@ -42,7 +42,7 @@ "pluggable.js": "3.0.1", "sizzle": "^2.3.5", "sprintf-js": "^1.1.2", - "strophe.js": "strophe/strophejs#9b643a43b5cd79f0d8a2e0015e9d2c0d0fec8578", + "strophe.js": "strophe/strophejs#b4f3369ba07dee4f04750de6709ea8ebe8adf9a5", "urijs": "^1.19.10" }, "devDependencies": {} diff --git a/src/headless/plugins/blocklist/api.js b/src/headless/plugins/blocklist/api.js new file mode 100644 index 0000000000..a7c7270d2f --- /dev/null +++ b/src/headless/plugins/blocklist/api.js @@ -0,0 +1,51 @@ +import promise_api from '../../shared/api/promise.js'; +import { sendBlockStanza, sendUnblockStanza } from './utils.js'; + +const { waitUntil } = promise_api; + +/** + * Groups methods relevant to XEP-0191 Blocking Command + * @namespace api.blocklist + * @memberOf api + */ +const blocklist = { + /** + * Retrieves the current user's blocklist + * @returns {Promise} + */ + async get() { + return await waitUntil('blocklistInitialized'); + }, + + /** + * Adds a new entity to the blocklist + * @param {string|string[]} jid + * @param {boolean} [send_stanza=true] + * @returns {Promise} + */ + async add(jid, send_stanza = true) { + const blocklist = await waitUntil('blocklistInitialized'); + const jids = Array.isArray(jid) ? jid : [jid]; + if (send_stanza) await sendBlockStanza(jids); + jids.forEach((jid) => blocklist.create({ jid })); + return blocklist; + }, + + /** + * Removes an entity from the blocklist + * @param {string|string[]} jid + * @param {boolean} [send_stanza=true] + * @returns {Promise} + */ + async remove(jid, send_stanza = true) { + const blocklist = await waitUntil('blocklistInitialized'); + const jids = Array.isArray(jid) ? jid : [jid]; + if (send_stanza) await sendUnblockStanza(jids); + blocklist.remove(jids); + return blocklist; + }, +}; + +const blocklist_api = { blocklist }; + +export default blocklist_api; diff --git a/src/headless/plugins/blocklist/collection.js b/src/headless/plugins/blocklist/collection.js new file mode 100644 index 0000000000..71ff417862 --- /dev/null +++ b/src/headless/plugins/blocklist/collection.js @@ -0,0 +1,88 @@ +import { getOpenPromise } from '@converse/openpromise'; +import { Collection } from '@converse/skeletor'; +import log from '../../log.js'; +import _converse from '../../shared/_converse.js'; +import { initStorage } from '../../utils/storage.js'; +import api from '../../shared/api/index.js'; +import converse from '../../shared/api/public.js'; +import BlockedEntity from './model.js'; + +const { stx, u } = converse.env; + +class Blocklist extends Collection { + get idAttribute() { + return 'jid'; + } + + constructor() { + super(); + this.model = BlockedEntity; + } + + async initialize() { + const { session } = _converse; + const cache_key = `converse.blocklist-${session.get('bare_jid')}`; + this.fetched_flag = `${cache_key}-fetched`; + initStorage(this, cache_key); + + await this.fetchBlocklist(); + + /** + * Triggered once the {@link Blocklist} collection + * has been created and cached blocklist have been fetched. + * @event _converse#blocklistInitialized + * @type {Blocklist} + * @example _converse.api.listen.on('blocklistInitialized', (blocklist) => { ... }); + */ + api.trigger('blocklistInitialized', this); + } + + fetchBlocklist() { + const deferred = getOpenPromise(); + if (window.sessionStorage.getItem(this.fetched_flag)) { + this.fetch({ + success: () => deferred.resolve(), + error: () => deferred.resolve(), + }); + } else { + this.fetchBlocklistFromServer(deferred); + } + return deferred; + } + + /** + * @param {Object} deferred + */ + async fetchBlocklistFromServer(deferred) { + const stanza = stx``; + + try { + this.onBlocklistReceived(deferred, await api.sendIQ(stanza)); + } catch (e) { + log.error(e); + deferred.resolve(); + return; + } + } + + /** + * @param {Object} deferred + * @param {Element} iq + */ + async onBlocklistReceived(deferred, iq) { + Array.from(iq.querySelectorAll('blocklist item')).forEach((item) => { + const jid = item.getAttribute('jid'); + const blocked = this.get(jid); + blocked ? blocked.save({ jid }) : this.create({ jid }); + }); + + window.sessionStorage.setItem(this.fetched_flag, 'true'); + if (deferred !== undefined) { + return deferred.resolve(); + } + } +} + +export default Blocklist; diff --git a/src/headless/plugins/blocklist/index.js b/src/headless/plugins/blocklist/index.js new file mode 100644 index 0000000000..1f1ffb1d1d --- /dev/null +++ b/src/headless/plugins/blocklist/index.js @@ -0,0 +1 @@ +import './plugin.js'; diff --git a/src/headless/plugins/blocklist/model.js b/src/headless/plugins/blocklist/model.js new file mode 100644 index 0000000000..aaae155bc6 --- /dev/null +++ b/src/headless/plugins/blocklist/model.js @@ -0,0 +1,16 @@ +import { Model } from '@converse/skeletor'; +import converse from '../../shared/api/public.js'; + +const { Strophe } = converse.env; + +class BlockedEntity extends Model { + get idAttribute () { + return 'jid'; + } + + getDisplayName () { + return Strophe.xmlunescape(this.get('name')); + } +} + +export default BlockedEntity; diff --git a/src/headless/plugins/blocklist/plugin.js b/src/headless/plugins/blocklist/plugin.js new file mode 100644 index 0000000000..8a552c69d3 --- /dev/null +++ b/src/headless/plugins/blocklist/plugin.js @@ -0,0 +1,73 @@ +/** + * @copyright The Converse.js contributors + * @license Mozilla Public License (MPLv2) + * @description Adds support for XEP-0191 Blocking Command + */ +import _converse from '../../shared/_converse.js'; +import api from '../../shared/api/index.js'; +import converse from '../../shared/api/public.js'; +import log from '../../log.js'; +import Blocklist from './collection.js'; +import BlockedEntity from './model.js'; +import blocklist_api from './api.js'; + +const { Strophe, sizzle } = converse.env; + +Strophe.addNamespace('BLOCKING', 'urn:xmpp:blocking'); + +converse.plugins.add('converse-blocking', { + dependencies: ['converse-disco'], + + initialize() { + const exports = { Blocklist, BlockedEntity }; + Object.assign(_converse.exports, exports); + Object.assign(api, blocklist_api); + + api.promises.add(['blocklistInitialized']); + + api.listen.on('connected', () => { + const connection = api.connection.get(); + connection.addHandler( + /** @param {Element} stanza */ (stanza) => { + const bare_jid = _converse.session.get('bare_jid'); + const from = stanza.getAttribute('from'); + if (Strophe.getBareJidFromJid(from ?? bare_jid) != bare_jid) { + log.warn(`Received a blocklist push stanza from a suspicious JID ${from}`); + return true; + } + + const add_jids = sizzle(`block[xmlns="${Strophe.NS.BLOCKING}"] item`, stanza).map( + /** @param {Element} item */ (item) => item.getAttribute('jid') + ); + if (add_jids.length) api.blocklist.add(add_jids, false); + + const remove_jids = sizzle(`unblock[xmlns="${Strophe.NS.BLOCKING}"] item`, stanza).map( + /** @param {Element} item */ (item) => item.getAttribute('jid') + ); + if (remove_jids.length) api.blocklist.remove(remove_jids, false); + + return true; + }, + Strophe.NS.BLOCKING, + 'iq', + 'set' + ); + }); + + api.listen.on('clearSession', () => { + const { state } = _converse; + if (state.blocklist) { + state.blocklist.clearStore({ 'silent': true }); + window.sessionStorage.removeItem(state.blocklist.fetched_flag); + delete state.blocklist; + } + }); + + api.listen.on('discoInitialized', async () => { + const domain = _converse.session.get('domain'); + if (await api.disco.supports(Strophe.NS.BLOCKING, domain)) { + _converse.state.blocklist = new _converse.exports.Blocklist(); + } + }); + }, +}); diff --git a/src/headless/plugins/blocklist/tests/blocklist.js b/src/headless/plugins/blocklist/tests/blocklist.js new file mode 100644 index 0000000000..a54471b76a --- /dev/null +++ b/src/headless/plugins/blocklist/tests/blocklist.js @@ -0,0 +1,176 @@ +/*global mock, converse */ +const { u, stx } = converse.env; + +fdescribe('A blocklist', function () { + beforeEach(() => { + jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }); + window.sessionStorage.removeItem('converse.blocklist-romeo@montague.lit-fetched'); + }); + + it( + 'is automatically fetched from the server once the user logs in', + mock.initConverse(['discoInitialized'], {}, async function (_converse) { + const { api } = _converse; + await mock.waitUntilDiscoConfirmed( + _converse, + _converse.domain, + [{ 'category': 'server', 'type': 'IM' }], + ['urn:xmpp:blocking'] + ); + await mock.waitForRoster(_converse, 'current', 0); + + const IQ_stanzas = _converse.api.connection.get().IQ_stanzas; + const sent_stanza = await u.waitUntil(() => IQ_stanzas.find((s) => s.querySelector('iq blocklist'))); + + expect(sent_stanza).toEqualStanza(stx` + + + `); + + const stanza = stx` + + + + + + `; + _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); + + const blocklist = await api.waitUntil('blocklistInitialized'); + expect(blocklist.length).toBe(2); + expect(blocklist.models.map((m) => m.get('jid'))).toEqual(['iago@shakespeare.lit', 'juliet@capulet.lit']); + }) + ); + + it( + 'is updated when the server sends IQ stanzas', + mock.initConverse(['discoInitialized'], {}, async function (_converse) { + const { api, domain } = _converse; + await mock.waitUntilDiscoConfirmed( + _converse, + domain, + [{ 'category': 'server', 'type': 'IM' }], + ['urn:xmpp:blocking'] + ); + await mock.waitForRoster(_converse, 'current', 0); + + const IQ_stanzas = api.connection.get().IQ_stanzas; + let sent_stanza = await u.waitUntil(() => IQ_stanzas.find((s) => s.querySelector('iq blocklist'))); + + const stanza = stx` + + + + + `; + _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); + + const blocklist = await api.waitUntil('blocklistInitialized'); + expect(blocklist.length).toBe(1); + + // The server sends a push IQ stanza + _converse.api.connection.get()._dataRecv( + mock.createRequest( + stx` + + + + + ` + ) + ); + await u.waitUntil(() => blocklist.length === 2); + expect(blocklist.models.map((m) => m.get('jid'))).toEqual(['iago@shakespeare.lit', 'juliet@capulet.lit']); + + // The server sends a push IQ stanza + _converse.api.connection.get()._dataRecv( + mock.createRequest( + stx` + + + + + ` + ) + ); + await u.waitUntil(() => blocklist.length === 1); + expect(blocklist.models.map((m) => m.get('jid'))).toEqual(['iago@shakespeare.lit']); + }) + ); + + it( + 'can be updated via the api', + mock.initConverse(['discoInitialized'], {}, async function (_converse) { + const { api, domain } = _converse; + await mock.waitUntilDiscoConfirmed( + _converse, + domain, + [{ 'category': 'server', 'type': 'IM' }], + ['urn:xmpp:blocking'] + ); + await mock.waitForRoster(_converse, 'current', 0); + + const IQ_stanzas = api.connection.get().IQ_stanzas; + let sent_stanza = await u.waitUntil(() => IQ_stanzas.find((s) => s.querySelector('iq blocklist'))); + + _converse.api.connection.get()._dataRecv(mock.createRequest( + stx` + + + + `)); + + const blocklist = await api.waitUntil('blocklistInitialized'); + expect(blocklist.length).toBe(1); + + api.blocklist.add('juliet@capulet.lit'); + + sent_stanza = await u.waitUntil(() => IQ_stanzas.find((s) => s.querySelector('iq block'))); + expect(sent_stanza).toEqualStanza(stx` + + + + + `); + + _converse.api.connection.get()._dataRecv(mock.createRequest( + stx``) + ); + + await u.waitUntil(() => blocklist.length === 2); + expect(blocklist.models.map((m) => m.get('jid'))).toEqual(['iago@shakespeare.lit', 'juliet@capulet.lit']); + + api.blocklist.remove('juliet@capulet.lit'); + + sent_stanza = await u.waitUntil(() => IQ_stanzas.find((s) => s.querySelector('iq unblock'))); + expect(sent_stanza).toEqualStanza(stx` + + + + + `); + + _converse.api.connection.get()._dataRecv(mock.createRequest( + stx``) + ); + + await u.waitUntil(() => blocklist.length === 1); + expect(blocklist.models.map((m) => m.get('jid'))).toEqual(['iago@shakespeare.lit']); + }) + ); +}); diff --git a/src/headless/plugins/blocklist/utils.js b/src/headless/plugins/blocklist/utils.js new file mode 100644 index 0000000000..b0732fba0f --- /dev/null +++ b/src/headless/plugins/blocklist/utils.js @@ -0,0 +1,34 @@ +import converse from '../../shared/api/public.js'; +import send_api from '../../shared/api/send.js'; + +const { Strophe, stx, u } = converse.env; + +/** + * Sends an IQ stanza to remove one or more JIDs from the blocklist + * @param {string|string[]} jid + */ +export async function sendUnblockStanza(jid) { + const jids = Array.isArray(jid) ? jid : [jid]; + const stanza = stx` + + + ${jids.map((id) => stx``)} + + `; + await send_api.sendIQ(stanza); +} + +/** + * Sends an IQ stanza to add one or more JIDs from the blocklist + * @param {string|string[]} jid + */ +export async function sendBlockStanza(jid) { + const jids = Array.isArray(jid) ? jid : [jid]; + const stanza = stx` + + + ${jids.map((id) => stx``)} + + `; + await send_api.sendIQ(stanza); +} diff --git a/src/headless/plugins/bookmarks/api.js b/src/headless/plugins/bookmarks/api.js index 2a2b773167..2343e9be1a 100644 --- a/src/headless/plugins/bookmarks/api.js +++ b/src/headless/plugins/bookmarks/api.js @@ -4,8 +4,7 @@ import promise_api from '../../shared/api/promise.js'; const { waitUntil } = promise_api; /** - * Groups methods relevant to XEP-0402 MUC bookmarks. - * + * Groups methods relevant to XEP-0402 (and XEP-0048) MUC bookmarks. * @namespace api.bookmarks * @memberOf api */ diff --git a/src/headless/plugins/bookmarks/collection.js b/src/headless/plugins/bookmarks/collection.js index 6537e5371e..0e6260da24 100644 --- a/src/headless/plugins/bookmarks/collection.js +++ b/src/headless/plugins/bookmarks/collection.js @@ -38,7 +38,7 @@ class Bookmarks extends Collection { await this.fetchBookmarks(); /** - * Triggered once the _converse.Bookmarks collection + * Triggered once the {@link Bookmarks} collection * has been created and cached bookmarks have been fetched. * @event _converse#bookmarksInitialized * @type {Bookmarks} diff --git a/src/headless/plugins/bookmarks/model.js b/src/headless/plugins/bookmarks/model.js index 50036ce0cc..76a9ab3227 100644 --- a/src/headless/plugins/bookmarks/model.js +++ b/src/headless/plugins/bookmarks/model.js @@ -1,10 +1,10 @@ -import converse from '../../shared/api/public.js'; import { Model } from '@converse/skeletor'; +import converse from '../../shared/api/public.js'; const { Strophe } = converse.env; class Bookmark extends Model { - get idAttribute () { // eslint-disable-line class-methods-use-this + get idAttribute () { return 'jid'; } diff --git a/src/headless/plugins/disco/api.js b/src/headless/plugins/disco/api.js index 797972319f..5254e30191 100644 --- a/src/headless/plugins/disco/api.js +++ b/src/headless/plugins/disco/api.js @@ -382,14 +382,6 @@ export default { return entity.waitUntilFeaturesDiscovered; }, - /** - * @deprecated Use {@link api.disco.refresh} instead. - * @method api.disco.refreshFeatures - */ - refreshFeatures (jid) { - return api.refresh(jid); - }, - /** * Return all the features associated with a disco entity * diff --git a/src/headless/plugins/disco/entity.js b/src/headless/plugins/disco/entity.js index 22f80eff88..a4ed6cd43d 100644 --- a/src/headless/plugins/disco/entity.js +++ b/src/headless/plugins/disco/entity.js @@ -70,7 +70,7 @@ class DiscoEntity extends Model { */ async getFeature (feature) { await this.waitUntilFeaturesDiscovered; - if (this.features.findWhere({ 'var': feature })) { + if (this.features.findWhere({ var: feature })) { return this; } } diff --git a/src/headless/plugins/disco/tests/disco.js b/src/headless/plugins/disco/tests/disco.js index 29c71b7b1a..6e4a39ead7 100644 --- a/src/headless/plugins/disco/tests/disco.js +++ b/src/headless/plugins/disco/tests/disco.js @@ -1,5 +1,7 @@ /*global mock, converse */ +const { u, $iq, stx } = converse.env; + describe("Service Discovery", function () { describe("Whenever a server is queried for its features", function () { @@ -9,7 +11,6 @@ describe("Service Discovery", function () { ['discoInitialized'], {}, async function (_converse) { - const { u, $iq } = converse.env; const IQ_stanzas = _converse.api.connection.get().IQ_stanzas; const IQ_ids = _converse.api.connection.get().IQ_ids; await u.waitUntil(function () { @@ -17,63 +18,27 @@ describe("Service Discovery", function () { return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'); }).length > 0; }); - /* - * - * - * - * - * - * - * - * - * - * - * - * - * - */ let stanza = IQ_stanzas.find(function (iq) { return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'); }); const info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)]; - stanza = $iq({ - 'type': 'result', - 'from': 'montague.lit', - 'to': 'romeo@montague.lit/orchard', - 'id': info_IQ_id - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', { - 'category': 'server', - 'type': 'im'}).up() - .c('identity', { - 'category': 'conference', - 'type': 'text', - 'name': 'Play-Specific Chatrooms'}).up() - .c('identity', { - 'category': 'directory', - 'type': 'chatroom', - 'name': 'Play-Specific Chatrooms'}).up() - .c('feature', { - 'var': 'http://jabber.org/protocol/disco#info'}).up() - .c('feature', { - 'var': 'http://jabber.org/protocol/disco#items'}).up() - .c('feature', { - 'var': 'jabber:iq:register'}).up() - .c('feature', { - 'var': 'jabber:iq:time'}).up() - .c('feature', { - 'var': 'jabber:iq:version'}); + stanza = stx` + + + + + + + + + + + + `; _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await u.waitUntil(function () { @@ -111,35 +76,22 @@ describe("Service Discovery", function () { stanza = IQ_stanzas.find(iq => iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]')); const items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)]; - stanza = $iq({ - 'type': 'result', - 'from': 'montague.lit', - 'to': 'romeo@montague.lit/orchard', - 'id': items_IQ_id - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'}) - .c('item', { - 'jid': 'people.shakespeare.lit', - 'name': 'Directory of Characters'}).up() - .c('item', { - 'jid': 'plays.shakespeare.lit', - 'name': 'Play-Specific Chatrooms'}).up() - .c('item', { - 'jid': 'words.shakespeare.lit', - 'name': 'Gateway to Marlowe IM'}).up() - .c('item', { - 'jid': 'montague.lit', - 'node': 'books', - 'name': 'Books by and about Shakespeare'}).up() - .c('item', { - 'node': 'montague.lit', - 'name': 'Wear your literary taste with pride'}).up() - .c('item', { - 'jid': 'montague.lit', - 'node': 'music', - 'name': 'Music from the time of Shakespeare' - }); - _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); + _converse.api.connection.get()._dataRecv(mock.createRequest(stx` + + + + + + + + + + `)); const entities = await _converse.api.disco.entities.get() expect(entities.length).toBe(5); // We have an extra entity, which is the user's JID diff --git a/src/headless/plugins/disco/utils.js b/src/headless/plugins/disco/utils.js index 60813edafd..0c9ebad3a2 100644 --- a/src/headless/plugins/disco/utils.js +++ b/src/headless/plugins/disco/utils.js @@ -7,6 +7,9 @@ import { createStore } from '../../utils/storage.js'; const { Strophe, $iq } = converse.env; +/** + * @param {Element} stanza + */ function onDiscoInfoRequest (stanza) { const node = stanza.getElementsByTagName('query')[0].getAttribute('node'); const attrs = {xmlns: Strophe.NS.DISCO_INFO}; diff --git a/src/headless/plugins/muc/tests/messages.js b/src/headless/plugins/muc/tests/messages.js index 35727d14dd..8edbe2e190 100644 --- a/src/headless/plugins/muc/tests/messages.js +++ b/src/headless/plugins/muc/tests/messages.js @@ -96,8 +96,12 @@ describe("A MUC message", function () { const muc_jid = 'lounge@montague.lit'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const impersonated_jid = `${muc_jid}/alice`; - const received_stanza = u.toStanza(` - + const received_stanza = stx` + Yet I should kill thee with much cherishing. - - `); + `; spyOn(converse.env.log, 'error').and.callThrough(); _converse.api.connection.get()._dataRecv(mock.createRequest(received_stanza)); await u.waitUntil(() => converse.env.log.error.calls.count() === 1); diff --git a/src/headless/plugins/roster/utils.js b/src/headless/plugins/roster/utils.js index cfa0394da6..4d8b9cb51b 100644 --- a/src/headless/plugins/roster/utils.js +++ b/src/headless/plugins/roster/utils.js @@ -123,7 +123,7 @@ export async function onClearSession () { /** * Roster specific event handler for the presencesInitialized event - * @param { Boolean } reconnecting + * @param {Boolean} reconnecting */ export function onPresencesInitialized (reconnecting) { if (reconnecting) { diff --git a/src/headless/plugins/smacks/tests/smacks.js b/src/headless/plugins/smacks/tests/smacks.js index 0011de163d..c9c329b031 100644 --- a/src/headless/plugins/smacks/tests/smacks.js +++ b/src/headless/plugins/smacks/tests/smacks.js @@ -11,10 +11,11 @@ describe("XEP-0198 Stream Management", function () { it("gets enabled with an stanza and resumed with a stanza", mock.initConverse( ['chatBoxesInitialized'], - { 'auto_login': false, - 'enable_smacks': true, - 'show_controlbox_by_default': true, - 'smacks_max_unacked_stanzas': 2 + { auto_login: false, + enable_smacks: true, + show_controlbox_by_default: true, + smacks_max_unacked_stanzas: 2, + blacklisted_plugins: ['converse-blocking'] }, async function (_converse) { diff --git a/src/headless/shared/_converse.js b/src/headless/shared/_converse.js index 3758f01334..f0620a0fe5 100644 --- a/src/headless/shared/_converse.js +++ b/src/headless/shared/_converse.js @@ -89,7 +89,7 @@ class ConversePrivateGlobal extends EventEmitter(Object) { this.storage = /** @type {Record} */{}; this.promises = { - 'initialized': getOpenPromise(), + initialized: getOpenPromise(), }; this.NUM_PREKEYS = 100; // DEPRECATED. Set here so that tests can override diff --git a/src/headless/shared/constants.js b/src/headless/shared/constants.js index 195b2b0b90..bc62e01fc2 100644 --- a/src/headless/shared/constants.js +++ b/src/headless/shared/constants.js @@ -112,6 +112,7 @@ Strophe.addNamespace('XHTML', 'http://www.w3.org/1999/xhtml'); export const CORE_PLUGINS = [ 'converse-adhoc', 'converse-bookmarks', + 'converse-blocking', 'converse-bosh', 'converse-caps', 'converse-chat', diff --git a/src/headless/types/plugins/blocking/collection.d.ts b/src/headless/types/plugins/blocking/collection.d.ts new file mode 100644 index 0000000000..d710d7f72b --- /dev/null +++ b/src/headless/types/plugins/blocking/collection.d.ts @@ -0,0 +1,21 @@ +export default Blocklist; +declare class Blocklist extends Collection { + constructor(); + get idAttribute(): string; + model: typeof BlockedEntity; + initialize(): Promise; + fetched_flag: string; + fetchBlocklist(): any; + /** + * @param {Object} deferred + */ + fetchBlocklistFromServer(deferred: any): Promise; + /** + * @param {Object} deferred + * @param {Element} iq + */ + onBlocklistReceived(deferred: any, iq: Element): Promise; +} +import { Collection } from '@converse/skeletor'; +import BlockedEntity from './model.js'; +//# sourceMappingURL=collection.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/blocking/index.d.ts b/src/headless/types/plugins/blocking/index.d.ts new file mode 100644 index 0000000000..e26a57a8ca --- /dev/null +++ b/src/headless/types/plugins/blocking/index.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/blocking/model.d.ts b/src/headless/types/plugins/blocking/model.d.ts new file mode 100644 index 0000000000..05b0828531 --- /dev/null +++ b/src/headless/types/plugins/blocking/model.d.ts @@ -0,0 +1,6 @@ +export default BlockedEntity; +declare class BlockedEntity extends Model { + getDisplayName(): any; +} +import { Model } from '@converse/skeletor'; +//# sourceMappingURL=model.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/blocking/plugin.d.ts b/src/headless/types/plugins/blocking/plugin.d.ts new file mode 100644 index 0000000000..6d717ab1c9 --- /dev/null +++ b/src/headless/types/plugins/blocking/plugin.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=plugin.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/blocklist/api.d.ts b/src/headless/types/plugins/blocklist/api.d.ts new file mode 100644 index 0000000000..830f4b4944 --- /dev/null +++ b/src/headless/types/plugins/blocklist/api.d.ts @@ -0,0 +1,26 @@ +export default blocklist_api; +declare namespace blocklist_api { + export { blocklist }; +} +declare namespace blocklist { + /** + * Retrieves the current user's blocklist + * @returns {Promise} + */ + function get(): Promise; + /** + * Adds a new entity to the blocklist + * @param {string|string[]} jid + * @param {boolean} [send_stanza=true] + * @returns {Promise} + */ + function add(jid: string | string[], send_stanza?: boolean): Promise; + /** + * Removes an entity from the blocklist + * @param {string|string[]} jid + * @param {boolean} [send_stanza=true] + * @returns {Promise} + */ + function remove(jid: string | string[], send_stanza?: boolean): Promise; +} +//# sourceMappingURL=api.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/blocklist/collection.d.ts b/src/headless/types/plugins/blocklist/collection.d.ts new file mode 100644 index 0000000000..d710d7f72b --- /dev/null +++ b/src/headless/types/plugins/blocklist/collection.d.ts @@ -0,0 +1,21 @@ +export default Blocklist; +declare class Blocklist extends Collection { + constructor(); + get idAttribute(): string; + model: typeof BlockedEntity; + initialize(): Promise; + fetched_flag: string; + fetchBlocklist(): any; + /** + * @param {Object} deferred + */ + fetchBlocklistFromServer(deferred: any): Promise; + /** + * @param {Object} deferred + * @param {Element} iq + */ + onBlocklistReceived(deferred: any, iq: Element): Promise; +} +import { Collection } from '@converse/skeletor'; +import BlockedEntity from './model.js'; +//# sourceMappingURL=collection.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/blocklist/index.d.ts b/src/headless/types/plugins/blocklist/index.d.ts new file mode 100644 index 0000000000..e26a57a8ca --- /dev/null +++ b/src/headless/types/plugins/blocklist/index.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/blocklist/model.d.ts b/src/headless/types/plugins/blocklist/model.d.ts new file mode 100644 index 0000000000..05b0828531 --- /dev/null +++ b/src/headless/types/plugins/blocklist/model.d.ts @@ -0,0 +1,6 @@ +export default BlockedEntity; +declare class BlockedEntity extends Model { + getDisplayName(): any; +} +import { Model } from '@converse/skeletor'; +//# sourceMappingURL=model.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/blocklist/plugin.d.ts b/src/headless/types/plugins/blocklist/plugin.d.ts new file mode 100644 index 0000000000..6d717ab1c9 --- /dev/null +++ b/src/headless/types/plugins/blocklist/plugin.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=plugin.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/blocklist/utils.d.ts b/src/headless/types/plugins/blocklist/utils.d.ts new file mode 100644 index 0000000000..8d89f758f0 --- /dev/null +++ b/src/headless/types/plugins/blocklist/utils.d.ts @@ -0,0 +1,11 @@ +/** + * Sends an IQ stanza to remove one or more JIDs from the blocklist + * @param {string|string[]} jid + */ +export function sendUnblockStanza(jid: string | string[]): Promise; +/** + * Sends an IQ stanza to add one or more JIDs from the blocklist + * @param {string|string[]} jid + */ +export function sendBlockStanza(jid: string | string[]): Promise; +//# sourceMappingURL=utils.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/disco/api.d.ts b/src/headless/types/plugins/disco/api.d.ts index e09947a532..91ec0a0101 100644 --- a/src/headless/types/plugins/disco/api.d.ts +++ b/src/headless/types/plugins/disco/api.d.ts @@ -193,11 +193,6 @@ declare namespace _default { * await api.disco.refresh('room@conference.example.org'); */ export function refresh(jid: string): Promise; - /** - * @deprecated Use {@link api.disco.refresh} instead. - * @method api.disco.refreshFeatures - */ - export function refreshFeatures(jid: any): any; /** * Return all the features associated with a disco entity * diff --git a/src/headless/types/plugins/roster/utils.d.ts b/src/headless/types/plugins/roster/utils.d.ts index 5622581a22..1ccb97557a 100644 --- a/src/headless/types/plugins/roster/utils.d.ts +++ b/src/headless/types/plugins/roster/utils.d.ts @@ -5,7 +5,7 @@ export function unregisterPresenceHandler(): void; export function onClearSession(): Promise; /** * Roster specific event handler for the presencesInitialized event - * @param { Boolean } reconnecting + * @param {Boolean} reconnecting */ export function onPresencesInitialized(reconnecting: boolean): void; /** diff --git a/src/shared/tests/mock.js b/src/shared/tests/mock.js index 34a69ef0f4..94938a53c3 100644 --- a/src/shared/tests/mock.js +++ b/src/shared/tests/mock.js @@ -38,7 +38,6 @@ function initConverse (promise_names=[], settings=null, func) { } document.title = "Converse Tests"; - await _initConverse(settings); await Promise.all((promise_names || []).map(_converse.api.waitUntil)); @@ -57,17 +56,19 @@ function initConverse (promise_names=[], settings=null, func) { async function waitUntilDiscoConfirmed (_converse, entity_jid, identities, features=[], items=[], type='info') { const sel = `iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#${type}"]`; - const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.filter(iq => sizzle(sel, iq).length).pop()); - const stanza = $iq({ - 'type': 'result', - 'from': entity_jid, - 'to': 'romeo@montague.lit/orchard', - 'id': iq.getAttribute('id'), - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#'+type}); - - identities?.forEach(identity => stanza.c('identity', {'category': identity.category, 'type': identity.type}).up()); - features?.forEach(feature => stanza.c('feature', {'var': feature}).up()); - items?.forEach(item => stanza.c('item', {'jid': item}).up()); + const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.find(iq => sizzle(sel, iq).length)); + const stanza = stx` + + + ${identities?.map(identity => stx``)} + ${features?.map(feature => stx``)} + ${items?.map(item => stx``)} + + `; _converse.api.connection.get()._dataRecv(createRequest(stanza)); } diff --git a/tsconfig.json b/tsconfig.json index 33ed716d0e..b7c9b4428b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "src/website.js" ], "compilerOptions": { - "target": "es2016", + "target": "es2022", "module": "esnext", "types": [