Pārlūkot izejas kodu

Add support for XEP-0191 Blocking Command

JC Brand 4 mēneši atpakaļ
vecāks
revīzija
66a944dadb
39 mainītis faili ar 624 papildinājumiem un 130 dzēšanām
  1. 2 1
      CHANGES.md
  2. 6 0
      conversejs.doap
  3. 1 0
      karma.conf.js
  4. 3 3
      package-lock.json
  5. 1 0
      src/headless/index.js
  6. 1 1
      src/headless/package.json
  7. 51 0
      src/headless/plugins/blocklist/api.js
  8. 88 0
      src/headless/plugins/blocklist/collection.js
  9. 1 0
      src/headless/plugins/blocklist/index.js
  10. 16 0
      src/headless/plugins/blocklist/model.js
  11. 73 0
      src/headless/plugins/blocklist/plugin.js
  12. 176 0
      src/headless/plugins/blocklist/tests/blocklist.js
  13. 34 0
      src/headless/plugins/blocklist/utils.js
  14. 1 2
      src/headless/plugins/bookmarks/api.js
  15. 1 1
      src/headless/plugins/bookmarks/collection.js
  16. 2 2
      src/headless/plugins/bookmarks/model.js
  17. 0 8
      src/headless/plugins/disco/api.js
  18. 1 1
      src/headless/plugins/disco/entity.js
  19. 34 82
      src/headless/plugins/disco/tests/disco.js
  20. 3 0
      src/headless/plugins/disco/utils.js
  21. 7 4
      src/headless/plugins/muc/tests/messages.js
  22. 1 1
      src/headless/plugins/roster/utils.js
  23. 5 4
      src/headless/plugins/smacks/tests/smacks.js
  24. 1 1
      src/headless/shared/_converse.js
  25. 1 0
      src/headless/shared/constants.js
  26. 21 0
      src/headless/types/plugins/blocking/collection.d.ts
  27. 2 0
      src/headless/types/plugins/blocking/index.d.ts
  28. 6 0
      src/headless/types/plugins/blocking/model.d.ts
  29. 2 0
      src/headless/types/plugins/blocking/plugin.d.ts
  30. 26 0
      src/headless/types/plugins/blocklist/api.d.ts
  31. 21 0
      src/headless/types/plugins/blocklist/collection.d.ts
  32. 2 0
      src/headless/types/plugins/blocklist/index.d.ts
  33. 6 0
      src/headless/types/plugins/blocklist/model.d.ts
  34. 2 0
      src/headless/types/plugins/blocklist/plugin.d.ts
  35. 11 0
      src/headless/types/plugins/blocklist/utils.d.ts
  36. 0 5
      src/headless/types/plugins/disco/api.d.ts
  37. 1 1
      src/headless/types/plugins/roster/utils.d.ts
  38. 13 12
      src/shared/tests/mock.js
  39. 1 1
      tsconfig.json

+ 2 - 1
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`.

+ 6 - 0
conversejs.doap

@@ -125,6 +125,12 @@
         <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html"/>
       </xmpp:SupportedXep>
     </implements>
+    <implements>
+      <xmpp:SupportedXep>
+        <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0191.html"/>
+        <xmpp:since>11.0.0</xmpp:since>
+      </xmpp:SupportedXep>
+    </implements>
     <implements>
       <xmpp:SupportedXep>
         <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html"/>

+ 1 - 0
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' },

+ 3 - 3
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": {}

+ 1 - 0
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

+ 1 - 1
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": {}

+ 51 - 0
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<import('./collection').default>}
+     */
+    async get() {
+        return await waitUntil('blocklistInitialized');
+    },
+
+    /**
+     * Adds a new entity to the blocklist
+     * @param {string|string[]} jid
+     * @param {boolean} [send_stanza=true]
+     * @returns {Promise<import('./collection').default>}
+     */
+    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<import('./collection').default>}
+     */
+    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;

+ 88 - 0
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`<iq xmlns="jabber:client"
+            type="get"
+            id="${u.getUniqueId()}"><blocklist xmlns="urn:xmpp:blocking"/></iq>`;
+
+        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;

+ 1 - 0
src/headless/plugins/blocklist/index.js

@@ -0,0 +1 @@
+import './plugin.js';

+ 16 - 0
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;

+ 73 - 0
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-blocklist', {
+    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();
+            }
+        });
+    },
+});

+ 176 - 0
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`
+                <iq xmlns="jabber:client" type="get" id="${sent_stanza.getAttribute('id')}">
+                    <blocklist xmlns="urn:xmpp:blocking"/>
+                </iq>`);
+
+            const stanza = stx`
+                    <iq xmlns="jabber:client"
+                        to="${_converse.api.connection.get().jid}"
+                        type="result"
+                        id="${sent_stanza.getAttribute('id')}">
+                    <blocklist xmlns='urn:xmpp:blocking'>
+                        <item jid='iago@shakespeare.lit'/>
+                        <item jid='juliet@capulet.lit'/>
+                    </blocklist>
+                </iq>`;
+            _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`
+                    <iq xmlns="jabber:client"
+                        to="${api.connection.get().jid}"
+                        type="result"
+                        id="${sent_stanza.getAttribute('id')}">
+                    <blocklist xmlns='urn:xmpp:blocking'>
+                        <item jid='iago@shakespeare.lit'/>
+                    </blocklist>
+                </iq>`;
+            _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`
+                    <iq xmlns="jabber:client"
+                        to="${api.connection.get().jid}"
+                        type="set"
+                        id="${u.getUniqueId()}">
+                    <block xmlns='urn:xmpp:blocking'>
+                        <item jid='juliet@capulet.lit'/>
+                    </block>
+                </iq>`
+                )
+            );
+            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`
+                    <iq xmlns="jabber:client"
+                        to="${api.connection.get().jid}"
+                        type="set"
+                        id="${u.getUniqueId()}">
+                    <unblock xmlns='urn:xmpp:blocking'>
+                        <item jid='juliet@capulet.lit'/>
+                    </unblock>
+                </iq>`
+                )
+            );
+            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`<iq xmlns="jabber:client"
+                        to="${api.connection.get().jid}"
+                        type="result"
+                        id="${sent_stanza.getAttribute('id')}">
+                    <blocklist xmlns='urn:xmpp:blocking'>
+                        <item jid='iago@shakespeare.lit'/>
+                    </blocklist>
+                </iq>`));
+
+            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`
+                <iq xmlns="jabber:client" type="set" id="${sent_stanza.getAttribute('id')}">
+                    <block xmlns='urn:xmpp:blocking'>
+                        <item jid='juliet@capulet.lit'/>
+                    </block>
+                </iq>`);
+
+            _converse.api.connection.get()._dataRecv(mock.createRequest(
+                stx`<iq xmlns="jabber:client" type="result" id="${sent_stanza.getAttribute('id')}"/>`)
+            );
+
+            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`
+                <iq xmlns="jabber:client" type="set" id="${sent_stanza.getAttribute('id')}">
+                    <unblock xmlns='urn:xmpp:blocking'>
+                        <item jid='juliet@capulet.lit'/>
+                    </unblock>
+                </iq>`);
+
+            _converse.api.connection.get()._dataRecv(mock.createRequest(
+                stx`<iq xmlns="jabber:client" type="result" id="${sent_stanza.getAttribute('id')}"/>`)
+            );
+
+            await u.waitUntil(() => blocklist.length === 1);
+            expect(blocklist.models.map((m) => m.get('jid'))).toEqual(['iago@shakespeare.lit']);
+        })
+    );
+});

+ 34 - 0
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`
+        <iq xmlns="jabber:client" type="set" id="${u.getUniqueId()}">
+            <unblock xmlns="${Strophe.NS.BLOCKING}">
+                ${jids.map((id) => stx`<item jid="${id}"/>`)}
+            </unblock>
+        </iq>`;
+    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`
+        <iq xmlns="jabber:client" type="set" id="${u.getUniqueId()}">
+            <block xmlns="${Strophe.NS.BLOCKING}">
+                ${jids.map((id) => stx`<item jid="${id}"/>`)}
+            </block>
+        </iq>`;
+    await send_api.sendIQ(stanza);
+}

+ 1 - 2
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
  */

+ 1 - 1
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}

+ 2 - 2
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';
     }
 

+ 0 - 8
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
          *

+ 1 - 1
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;
         }
     }

+ 34 - 82
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;
             });
-            /* <iq type='result'
-             *      from='plays.shakespeare.lit'
-             *      to='romeo@montague.net/orchard'
-             *      id='info1'>
-             *  <query xmlns='http://jabber.org/protocol/disco#info'>
-             *      <identity
-             *          category='server'
-             *          type='im'/>
-             *      <identity
-             *          category='conference'
-             *          type='text'
-             *          name='Play-Specific Chatrooms'/>
-             *      <identity
-             *          category='directory'
-             *          type='chatroom'
-             *          name='Play-Specific Chatrooms'/>
-             *      <feature var='http://jabber.org/protocol/disco#info'/>
-             *      <feature var='http://jabber.org/protocol/disco#items'/>
-             *      <feature var='http://jabber.org/protocol/muc'/>
-             *      <feature var='jabber:iq:register'/>
-             *      <feature var='jabber:iq:search'/>
-             *      <feature var='jabber:iq:time'/>
-             *      <feature var='jabber:iq:version'/>
-             *  </query>
-             *  </iq>
-             */
             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`
+                <iq xmlns="jabber:client"
+                    type='result'
+                    from='montague.lit'
+                    to='romeo@montague.lit/orchard'
+                    id='${info_IQ_id}'>
+                    <query xmlns='http://jabber.org/protocol/disco#info'>
+                        <identity category='server' type='im'/>
+                        <identity category='conference' type='text' name='Play-Specific Chatrooms'/>
+                        <identity category='directory' type='chatroom' name='Play-Specific Chatrooms'/>
+                        <feature var='http://jabber.org/protocol/disco#info'/>
+                        <feature var='http://jabber.org/protocol/disco#items'/>
+                        <feature var='jabber:iq:register'/>
+                        <feature var='jabber:iq:time'/>
+                        <feature var='jabber:iq:version'/>
+                    </query>
+                </iq>`;
             _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`
+                <iq xmlns="jabber:client"
+                    type='result'
+                    from='montague.lit'
+                    to='romeo@montague.lit/orchard'
+                    id='${items_IQ_id}'>
+                    <query xmlns='http://jabber.org/protocol/disco#items'>
+                        <item jid='people.shakespeare.lit' name='Directory of Characters'/>
+                        <item jid='plays.shakespeare.lit' name='Play-Specific Chatrooms'/>
+                        <item jid='words.shakespeare.lit' name='Gateway to Marlowe IM'/>
+                        <item jid='montague.lit' node='books' name='Books by and about Shakespeare'/>
+                        <item node='montague.lit' name='Wear your literary taste with pride'/>
+                        <item jid='montague.lit' node='music' name='Music from the time of Shakespeare'/>
+                    </query>
+                </iq>`));
 
             const entities = await _converse.api.disco.entities.get()
             expect(entities.length).toBe(5); // We have an extra entity, which is the user's JID

+ 3 - 0
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};

+ 7 - 4
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(`
-            <message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.api.connection.get().getUniqueId()}'>
+        const received_stanza = stx`
+            <message to='${_converse.jid}'
+                    xmlns="jabber:client"
+                    from='${muc_jid}/mallory'
+                    type='groupchat'
+                    id='${_converse.api.connection.get().getUniqueId()}'>
                 <forwarded xmlns='urn:xmpp:forward:0'>
                     <delay xmlns='urn:xmpp:delay' stamp='2019-07-10T23:08:25Z'/>
                     <message from='${impersonated_jid}'
@@ -108,8 +112,7 @@ describe("A MUC message", function () {
                         <body>Yet I should kill thee with much cherishing.</body>
                     </message>
                 </forwarded>
-            </message>
-        `);
+            </message>`;
         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);

+ 1 - 1
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) {

+ 5 - 4
src/headless/plugins/smacks/tests/smacks.js

@@ -11,10 +11,11 @@ describe("XEP-0198 Stream Management", function () {
     it("gets enabled with an <enable> stanza and resumed with a <resume> 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-blocklist']
             },
             async function (_converse) {
 

+ 1 - 1
src/headless/shared/_converse.js

@@ -89,7 +89,7 @@ class ConversePrivateGlobal extends EventEmitter(Object) {
         this.storage = /** @type {Record<string, Storage.LocalForage>} */{};
 
         this.promises = {
-            'initialized': getOpenPromise(),
+            initialized: getOpenPromise(),
         };
 
         this.NUM_PREKEYS = 100; // DEPRECATED. Set here so that tests can override

+ 1 - 0
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-blocklist',
     'converse-bosh',
     'converse-caps',
     'converse-chat',

+ 21 - 0
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<void>;
+    fetched_flag: string;
+    fetchBlocklist(): any;
+    /**
+     * @param {Object} deferred
+     */
+    fetchBlocklistFromServer(deferred: any): Promise<void>;
+    /**
+     * @param {Object} deferred
+     * @param {Element} iq
+     */
+    onBlocklistReceived(deferred: any, iq: Element): Promise<any>;
+}
+import { Collection } from '@converse/skeletor';
+import BlockedEntity from './model.js';
+//# sourceMappingURL=collection.d.ts.map

+ 2 - 0
src/headless/types/plugins/blocking/index.d.ts

@@ -0,0 +1,2 @@
+export {};
+//# sourceMappingURL=index.d.ts.map

+ 6 - 0
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

+ 2 - 0
src/headless/types/plugins/blocking/plugin.d.ts

@@ -0,0 +1,2 @@
+export {};
+//# sourceMappingURL=plugin.d.ts.map

+ 26 - 0
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<import('./collection').default>}
+     */
+    function get(): Promise<import("./collection").default>;
+    /**
+     * Adds a new entity to the blocklist
+     * @param {string|string[]} jid
+     * @param {boolean} [send_stanza=true]
+     * @returns {Promise<import('./collection').default>}
+     */
+    function add(jid: string | string[], send_stanza?: boolean): Promise<import("./collection").default>;
+    /**
+     * Removes an entity from the blocklist
+     * @param {string|string[]} jid
+     * @param {boolean} [send_stanza=true]
+     * @returns {Promise<import('./collection').default>}
+     */
+    function remove(jid: string | string[], send_stanza?: boolean): Promise<import("./collection").default>;
+}
+//# sourceMappingURL=api.d.ts.map

+ 21 - 0
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<void>;
+    fetched_flag: string;
+    fetchBlocklist(): any;
+    /**
+     * @param {Object} deferred
+     */
+    fetchBlocklistFromServer(deferred: any): Promise<void>;
+    /**
+     * @param {Object} deferred
+     * @param {Element} iq
+     */
+    onBlocklistReceived(deferred: any, iq: Element): Promise<any>;
+}
+import { Collection } from '@converse/skeletor';
+import BlockedEntity from './model.js';
+//# sourceMappingURL=collection.d.ts.map

+ 2 - 0
src/headless/types/plugins/blocklist/index.d.ts

@@ -0,0 +1,2 @@
+export {};
+//# sourceMappingURL=index.d.ts.map

+ 6 - 0
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

+ 2 - 0
src/headless/types/plugins/blocklist/plugin.d.ts

@@ -0,0 +1,2 @@
+export {};
+//# sourceMappingURL=plugin.d.ts.map

+ 11 - 0
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<void>;
+/**
+ * Sends an IQ stanza to add one or more JIDs from the blocklist
+ * @param {string|string[]} jid
+ */
+export function sendBlockStanza(jid: string | string[]): Promise<void>;
+//# sourceMappingURL=utils.d.ts.map

+ 0 - 5
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<any>;
-        /**
-         * @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
          *

+ 1 - 1
src/headless/types/plugins/roster/utils.d.ts

@@ -5,7 +5,7 @@ export function unregisterPresenceHandler(): void;
 export function onClearSession(): Promise<void>;
 /**
  * Roster specific event handler for the presencesInitialized event
- * @param { Boolean } reconnecting
+ * @param {Boolean} reconnecting
  */
 export function onPresencesInitialized(reconnecting: boolean): void;
 /**

+ 13 - 12
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`
+            <iq type="result"
+                from="${entity_jid}"
+                to="${_converse.session.get('jid')}"
+                id="${iq.getAttribute('id')}"
+                xmlns="jabber:client">
+            <query xmlns="http://jabber.org/protocol/disco#${type}">
+                ${identities?.map(identity => stx`<identity category="${identity.category}" type="${identity.type}"></identity>`)}
+                ${features?.map(feature => stx`<feature var="${feature}"></feature>`)}
+                ${items?.map(item => stx`<item jid="${item}"></item>`)}
+            </query>
+            </iq>`;
     _converse.api.connection.get()._dataRecv(createRequest(stanza));
 }
 

+ 1 - 1
tsconfig.json

@@ -9,7 +9,7 @@
       "src/website.js"
   ],
   "compilerOptions": {
-    "target": "es2016",
+    "target": "es2022",
     "module": "esnext",
 
     "types": [