Explorar o código

Add error parsing to api.pubsub.config.get

JC Brand hai 6 meses
pai
achega
79f74d3b52

+ 1 - 1
src/headless/plugins/chat/model.js

@@ -22,7 +22,7 @@ class ChatBox extends ModelWithMessages(ModelWithContact(ColorAwareModel(ChatBox
      * @typedef {import('./message.js').default} Message
      * @typedef {import('../muc/muc.js').default} MUC
      * @typedef {import('./types').MessageAttributes} MessageAttributes
-     * @typedef {import('../../shared/parsers').StanzaParseError} StanzaParseError
+     * @typedef {import('../../shared/errors').StanzaParseError} StanzaParseError
      */
 
     defaults () {

+ 8 - 9
src/headless/plugins/chat/parsers.js

@@ -7,10 +7,9 @@ import converse from '../../shared/api/public.js';
 import dayjs from 'dayjs';
 import log from '../../log.js';
 import u from '../../utils/index.js';
-import { rejectMessage } from '../../shared/actions';
-
+import { rejectMessage } from '../../shared/actions.js';
+import { StanzaParseError } from '../../shared/errors.js';
 import {
-    StanzaParseError,
     getChatMarker,
     getChatState,
     getCorrectionAttributes,
@@ -45,8 +44,8 @@ export async function parseMessage (stanza) {
     const resource = _converse.session.get('resource');
     if (api.settings.get('filter_by_resource') && to_resource && to_resource !== resource) {
         return new StanzaParseError(
+            stanza,
             `Ignoring incoming message intended for a different resource: ${to_jid}`,
-            stanza
         );
     }
 
@@ -62,7 +61,7 @@ export async function parseMessage (stanza) {
         } else {
             // Prevent message forging via carbons: https://xmpp.org/extensions/xep-0280.html#security
             rejectMessage(stanza, 'Rejecting carbon from invalid JID');
-            return new StanzaParseError(`Rejecting carbon from invalid JID ${to_jid}`, stanza);
+            return new StanzaParseError(stanza, `Rejecting carbon from invalid JID ${to_jid}`);
         }
     }
 
@@ -75,8 +74,8 @@ export async function parseMessage (stanza) {
             from_jid = stanza.getAttribute('from');
         } else {
             return new StanzaParseError(
+                stanza,
                 `Invalid Stanza: alleged MAM message from ${stanza.getAttribute('from')}`,
-                stanza
             );
         }
     }
@@ -85,8 +84,8 @@ export async function parseMessage (stanza) {
     const is_me = from_bare_jid === bare_jid;
     if (is_me && to_jid === null) {
         return new StanzaParseError(
+            stanza,
             `Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`,
-            stanza
         );
     }
 
@@ -99,8 +98,8 @@ export async function parseMessage (stanza) {
         if (contact === undefined && !api.settings.get('allow_non_roster_messaging')) {
             log.error(stanza);
             return new StanzaParseError(
+                stanza,
                 `Blocking messaging with a JID not in our roster because allow_non_roster_messaging is false.`,
-                stanza
             );
         }
     }
@@ -146,7 +145,7 @@ export async function parseMessage (stanza) {
     if (attrs.is_archived) {
         const from = original_stanza.getAttribute('from');
         if (from && from !== bare_jid) {
-            return new StanzaParseError(`Invalid Stanza: Forged MAM message from ${from}`, stanza);
+            return new StanzaParseError(stanza, `Invalid Stanza: Forged MAM message from ${from}`);
         }
     }
     attrs = Object.assign(

+ 1 - 3
src/headless/plugins/chat/utils.js

@@ -2,7 +2,7 @@
  * @module:headless-plugins-chat-utils
  * @typedef {import('./model.js').default} ChatBox
  * @typedef {import('./types.ts').MessageAttributes} MessageAttributes
- * @typedef {import('../../shared/parsers').StanzaParseError} StanzaParseError
+ * @typedef {import('../../shared/errors').StanzaParseError} StanzaParseError
  * @typedef {import('strophe.js').Builder} Builder
  */
 import sizzle from "sizzle";
@@ -42,7 +42,6 @@ export async function onClearSession () {
     }
 }
 
-
 /**
  * Given a stanza, determine whether it's a new
  * message, i.e. not a MAM archived one.
@@ -60,7 +59,6 @@ export function isNewMessage (message) {
     return !(message['is_delayed'] && message['is_archived']);
 }
 
-
 /**
  * @param {Element} stanza
  */

+ 3 - 1
src/headless/plugins/mam/utils.js

@@ -14,6 +14,7 @@ import { parseMessage } from '../../plugins/chat/parsers.js';
 import { CHATROOMS_TYPE } from '../../shared/constants.js';
 import { TimeoutError } from '../../shared/errors.js';
 import MAMPlaceholderMessage from './placeholder.js';
+import { parseErrorStanza } from '../../shared/parsers.js';
 
 const { NS } = Strophe;
 const u = converse.env.utils;
@@ -22,7 +23,8 @@ const u = converse.env.utils;
  * @param {Element} iq
  */
 export function onMAMError(iq) {
-    if (iq?.querySelectorAll('feature-not-implemented').length) {
+    const err = parseErrorStanza(iq);
+    if (err?.name === 'feature-not-implemented') {
         log.warn(`Message Archive Management (XEP-0313) not supported by ${iq.getAttribute('from')}`);
     } else {
         log.error(`Error while trying to set archiving preferences for ${iq.getAttribute('from')}.`);

+ 10 - 7
src/headless/plugins/muc/muc.js

@@ -24,7 +24,7 @@ import { Strophe, $build, $iq, $msg, $pres } from 'strophe.js';
 import { TimeoutError } from '../../shared/errors.js';
 import { computeAffiliationsDelta, setAffiliations, getAffiliationList } from './affiliations/utils.js';
 import { initStorage, createStore } from '../../utils/storage.js';
-import { isArchived } from '../../shared/parsers.js';
+import { isArchived, parseErrorStanza } from '../../shared/parsers.js';
 import { getUniqueId, isErrorObject, safeSave } from '../../utils/index.js';
 import { isUniView } from '../../utils/session.js';
 import { parseMUCMessage, parseMUCPresence } from './parsers.js';
@@ -52,7 +52,7 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
      * @typedef {import('./types').MUCMessageAttributes} MUCMessageAttributes
      * @typedef {module:shared.converse.UserMessage} UserMessage
      * @typedef {import('strophe.js').Builder} Builder
-     * @typedef {import('../../shared/parsers').StanzaParseError} StanzaParseError
+     * @typedef {import('../../shared/errors').StanzaParseError} StanzaParseError
      */
 
     defaults () {
@@ -1716,12 +1716,15 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
                             .c('value').t(nick)
             );
         } catch (e) {
-            if (sizzle(`service-unavailable[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
-                err_msg = __("Can't register your nickname in this groupchat, it doesn't support registration.");
-            } else if (sizzle(`bad-request[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
-                err_msg = __("Can't register your nickname in this groupchat, invalid data form supplied.");
+            if (u.isErrorStanza(e)) {
+                const err = parseErrorStanza(e);
+                if (err?.name === 'service-unavailable') {
+                    err_msg = __("Can't register your nickname in this groupchat, it doesn't support registration.");
+                } else if (err?.name === 'bad-request') {
+                    err_msg = __("Can't register your nickname in this groupchat, invalid data form supplied.");
+                }
+                log.error(err_msg);
             }
-            log.error(err_msg);
             log.error(e);
             return err_msg;
         }

+ 1 - 1
src/headless/plugins/muc/occupant.js

@@ -16,7 +16,7 @@ import { sendChatState } from '../../shared/actions';
 class MUCOccupant extends ModelWithMessages(ColorAwareModel(Model)) {
     /**
      * @typedef {import('../chat/types').MessageAttributes} MessageAttributes
-     * @typedef {import('../../shared/parsers').StanzaParseError} StanzaParseError
+     * @typedef {import('../../shared/errors').StanzaParseError} StanzaParseError
      */
 
     constructor(attributes, options) {

+ 5 - 5
src/headless/plugins/muc/parsers.js

@@ -7,8 +7,8 @@ import dayjs from 'dayjs';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import converse from '../../shared/api/public.js';
+import { StanzaParseError } from '../../shared/errors.js';
 import {
-    StanzaParseError,
     getChatMarker,
     getChatState,
     getCorrectionAttributes,
@@ -161,8 +161,8 @@ export async function parseMUCMessage (stanza, chatbox) {
 
     if (sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length) {
         return new StanzaParseError(
+            stanza,
             `Invalid Stanza: Forged MAM groupchat message from ${stanza.getAttribute('from')}`,
-            stanza
         );
     }
     const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
@@ -219,16 +219,16 @@ export async function parseMUCMessage (stanza, chatbox) {
 
     if (attrs.is_archived && original_stanza.getAttribute('from') !== attrs.from_muc) {
         return new StanzaParseError(
+            stanza,
             `Invalid Stanza: Forged MAM message from ${original_stanza.getAttribute('from')}`,
-            stanza
         );
     } else if (attrs.is_archived && original_stanza.getAttribute('from') !== chatbox.get('jid')) {
         return new StanzaParseError(
+            stanza,
             `Invalid Stanza: Forged MAM groupchat message from ${stanza.getAttribute('from')}`,
-            stanza
         );
     } else if (attrs.is_carbon) {
-        return new StanzaParseError('Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied', stanza);
+        return new StanzaParseError(stanza, 'Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied');
     }
 
     // We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates.

+ 7 - 1
src/headless/plugins/pubsub/api.js

@@ -6,9 +6,10 @@ import converse from '../../shared/api/public.js';
 import _converse from '../../shared/_converse.js';
 import api from '../../shared/api/index.js';
 import log from '../../log.js';
+import { parseErrorStanza } from '../../shared/parsers.js';
 import { parseStanzaForPubSubConfig } from './parsers.js';
 
-const { Strophe, stx } = converse.env;
+const { Strophe, stx, u } = converse.env;
 
 export default {
     /**
@@ -30,6 +31,8 @@ export default {
              * @returns {Promise<import('./types').PubSubConfigOptions>}
              */
             async get(jid, node) {
+                if (!node) throw new Error('api.pubsub.config.get: Node value required');
+
                 const bare_jid = _converse.session.get('bare_jid');
                 const full_jid = _converse.session.get('jid');
                 const entity_jid = jid || bare_jid;
@@ -46,6 +49,9 @@ export default {
                 try {
                     response = await api.sendIQ(stanza);
                 } catch (error) {
+                    if (u.isErrorStanza(error)) {
+                        throw parseErrorStanza(error);
+                    }
                     throw error;
                 }
                 return parseStanzaForPubSubConfig(response);

+ 236 - 154
src/headless/plugins/pubsub/tests/config.js

@@ -1,162 +1,244 @@
 /* global mock, converse */
-const { Strophe, sizzle, stx, u } = converse.env;
+const { Strophe, sizzle, stx, u, errors } = converse.env;
 
 describe('The pubsub API', function () {
-    it(
-        "can be used to fetch a nodes's configuration settings",
-        mock.initConverse([], {}, async function (_converse) {
-            await mock.waitForRoster(_converse, 'current', 0);
-            const { api } = _converse;
-            const sent_stanzas = api.connection.get().sent_stanzas;
-            const own_jid = _converse.session.get('jid');
 
-            const node = 'princely_musings';
-            const pubsub_jid = 'pubsub.shakespeare.lit';
-            const promise = api.pubsub.config.get(pubsub_jid, node);
-            const sent_stanza = await u.waitUntil(() =>
-                sent_stanzas.filter((iq) => sizzle('pubsub configure', iq)).pop()
-            );
+    describe('fetching a nodes config settings', function () {
+        it(
+            "can be used to fetch a nodes's configuration settings",
+            mock.initConverse([], {}, async function (_converse) {
+                await mock.waitForRoster(_converse, 'current', 0);
+                const { api } = _converse;
+                const sent_stanzas = api.connection.get().sent_stanzas;
+                const own_jid = _converse.session.get('jid');
 
-            const response = stx`
-            <iq type='result'
-                xmlns="jabber:client"
-                from='${pubsub_jid}'
-                to='${own_jid}'
-                id="${sent_stanza.getAttribute('id')}">
-            <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
-                <configure node='${node}'>
-                <x xmlns='jabber:x:data' type='form'>
-                    <field var='FORM_TYPE' type='hidden'>
-                    <value>http://jabber.org/protocol/pubsub#node_config</value>
-                    </field>
-                    <field var='pubsub#title' type='text-single'
-                        label='A friendly name for the node'/>
-                    <field var='pubsub#deliver_notifications' type='boolean'
-                        label='Whether to deliver event notifications'>
-                    <value>true</value>
-                    </field>
-                    <field var='pubsub#deliver_payloads' type='boolean'
-                        label='Whether to deliver payloads with event notifications'>
-                    <value>true</value>
-                    </field>
-                    <field var='pubsub#notify_config' type='boolean'
-                        label='Notify subscribers when the node configuration changes'>
-                    <value>0</value>
-                    </field>
-                    <field var='pubsub#notify_delete' type='boolean'
-                        label='Notify subscribers when the node is deleted'>
-                    <value>false</value>
-                    </field>
-                    <field var='pubsub#notify_retract' type='boolean'
-                        label='Notify subscribers when items are removed from the node'>
-                    <value>false</value>
-                    </field>
-                    <field var='pubsub#notify_sub' type='boolean'
-                        label='Notify owners about new subscribers and unsubscribes'>
-                    <value>0</value>
-                    </field>
-                    <field var='pubsub#persist_items' type='boolean'
-                        label='Persist items to storage'>
-                    <value>1</value>
-                    </field>
-                    <field var='pubsub#max_items' type='text-single'
-                        label='Max # of items to persist. \`max\` for no specific limit other than a server imposed maximum.'>
-                    <value>10</value>
-                    </field>
-                    <field var='pubsub#item_expire' type='text-single'
-                        label='Time after which to automatically purge items. \`max\` for no specific limit other than a server imposed maximum.'>
-                    <value>604800</value>
-                    </field>
-                    <field var='pubsub#subscribe' type='boolean'
-                        label='Whether to allow subscriptions'>
-                    <value>1</value>
-                    </field>
-                    <field var='pubsub#access_model' type='list-single'
-                        label='Specify the subscriber model'>
-                    <option><value>authorize</value></option>
-                    <option><value>open</value></option>
-                    <option><value>presence</value></option>
-                    <option><value>roster</value></option>
-                    <option><value>whitelist</value></option>
-                    <value>open</value>
-                    </field>
-                    <field var='pubsub#roster_groups_allowed' type='list-multi'
-                        label='Roster groups allowed to subscribe'>
-                    <option><value>friends</value></option>
-                    <option><value>courtiers</value></option>
-                    <option><value>servants</value></option>
-                    <option><value>enemies</value></option>
-                    </field>
-                    <field var='pubsub#publish_model' type='list-single'
-                        label='Specify the publisher model'>
-                    <option><value>publishers</value></option>
-                    <option><value>subscribers</value></option>
-                    <option><value>open</value></option>
-                    <value>publishers</value>
-                    </field>
-                    <field var='pubsub#purge_offline' type='boolean'
-                        label='Purge all items when the relevant publisher goes offline?'>
-                    <value>0</value>
-                    </field>
-                    <field var='pubsub#max_payload_size' type='text-single'
-                        label='Max Payload size in bytes'>
-                    <value>1028</value>
-                    </field>
-                    <field var='pubsub#send_last_published_item' type='list-single'
-                        label='When to send the last published item'>
-                    <option label='Never'><value>never</value></option>
-                    <option label='When a new subscription is processed'><value>on_sub</value></option>
-                    <option label='When a new subscription is processed and whenever a subscriber comes online'>
-                        <value>on_sub_and_presence</value>
-                    </option>
-                    <value>never</value>
-                    </field>
-                    <field var='pubsub#presence_based_delivery' type='boolean'
-                        label='Deliver event notifications only to available users'>
-                    <value>0</value>
-                    </field>
-                    <field var='pubsub#notification_type' type='list-single'
-                        label='Specify the delivery style for event notifications'>
-                    <option><value>normal</value></option>
-                    <option><value>headline</value></option>
-                    <value>headline</value>
-                    </field>
-                    <field var='pubsub#type' type='text-single'
-                        label='Specify the semantic type of payload data to be provided at this node.'>
-                    <value>urn:example:e2ee:bundle</value>
-                    </field>
-                    <field var='pubsub#dataform_xslt' type='text-single' label='Payload XSLT'/>
-                </x>
-                </configure>
-            </pubsub>
-            </iq>`;
+                const node = 'princely_musings';
+                const pubsub_jid = 'pubsub.shakespeare.lit';
+                const promise = api.pubsub.config.get(pubsub_jid, node);
+                const sent_stanza = await u.waitUntil(() =>
+                    sent_stanzas.filter((iq) => sizzle('pubsub configure', iq)).pop()
+                );
 
-            _converse.api.connection.get()._dataRecv(mock.createRequest(response));
+                const response = stx`
+                    <iq type='result'
+                        xmlns="jabber:client"
+                        from='${pubsub_jid}'
+                        to='${own_jid}'
+                        id="${sent_stanza.getAttribute('id')}">
+                    <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
+                        <configure node='${node}'>
+                        <x xmlns='jabber:x:data' type='form'>
+                            <field var='FORM_TYPE' type='hidden'>
+                            <value>http://jabber.org/protocol/pubsub#node_config</value>
+                            </field>
+                            <field var='pubsub#title' type='text-single'
+                                label='A friendly name for the node'/>
+                            <field var='pubsub#deliver_notifications' type='boolean'
+                                label='Whether to deliver event notifications'>
+                            <value>true</value>
+                            </field>
+                            <field var='pubsub#deliver_payloads' type='boolean'
+                                label='Whether to deliver payloads with event notifications'>
+                            <value>true</value>
+                            </field>
+                            <field var='pubsub#notify_config' type='boolean'
+                                label='Notify subscribers when the node configuration changes'>
+                            <value>0</value>
+                            </field>
+                            <field var='pubsub#notify_delete' type='boolean'
+                                label='Notify subscribers when the node is deleted'>
+                            <value>false</value>
+                            </field>
+                            <field var='pubsub#notify_retract' type='boolean'
+                                label='Notify subscribers when items are removed from the node'>
+                            <value>false</value>
+                            </field>
+                            <field var='pubsub#notify_sub' type='boolean'
+                                label='Notify owners about new subscribers and unsubscribes'>
+                            <value>0</value>
+                            </field>
+                            <field var='pubsub#persist_items' type='boolean'
+                                label='Persist items to storage'>
+                            <value>1</value>
+                            </field>
+                            <field var='pubsub#max_items' type='text-single'
+                                label='Max # of items to persist. \`max\` for no specific limit other than a server imposed maximum.'>
+                            <value>10</value>
+                            </field>
+                            <field var='pubsub#item_expire' type='text-single'
+                                label='Time after which to automatically purge items. \`max\` for no specific limit other than a server imposed maximum.'>
+                            <value>604800</value>
+                            </field>
+                            <field var='pubsub#subscribe' type='boolean'
+                                label='Whether to allow subscriptions'>
+                            <value>1</value>
+                            </field>
+                            <field var='pubsub#access_model' type='list-single'
+                                label='Specify the subscriber model'>
+                            <option><value>authorize</value></option>
+                            <option><value>open</value></option>
+                            <option><value>presence</value></option>
+                            <option><value>roster</value></option>
+                            <option><value>whitelist</value></option>
+                            <value>open</value>
+                            </field>
+                            <field var='pubsub#roster_groups_allowed' type='list-multi'
+                                label='Roster groups allowed to subscribe'>
+                            <option><value>friends</value></option>
+                            <option><value>courtiers</value></option>
+                            <option><value>servants</value></option>
+                            <option><value>enemies</value></option>
+                            </field>
+                            <field var='pubsub#publish_model' type='list-single'
+                                label='Specify the publisher model'>
+                            <option><value>publishers</value></option>
+                            <option><value>subscribers</value></option>
+                            <option><value>open</value></option>
+                            <value>publishers</value>
+                            </field>
+                            <field var='pubsub#purge_offline' type='boolean'
+                                label='Purge all items when the relevant publisher goes offline?'>
+                            <value>0</value>
+                            </field>
+                            <field var='pubsub#max_payload_size' type='text-single'
+                                label='Max Payload size in bytes'>
+                            <value>1028</value>
+                            </field>
+                            <field var='pubsub#send_last_published_item' type='list-single'
+                                label='When to send the last published item'>
+                            <option label='Never'><value>never</value></option>
+                            <option label='When a new subscription is processed'><value>on_sub</value></option>
+                            <option label='When a new subscription is processed and whenever a subscriber comes online'>
+                                <value>on_sub_and_presence</value>
+                            </option>
+                            <value>never</value>
+                            </field>
+                            <field var='pubsub#presence_based_delivery' type='boolean'
+                                label='Deliver event notifications only to available users'>
+                            <value>0</value>
+                            </field>
+                            <field var='pubsub#notification_type' type='list-single'
+                                label='Specify the delivery style for event notifications'>
+                            <option><value>normal</value></option>
+                            <option><value>headline</value></option>
+                            <value>headline</value>
+                            </field>
+                            <field var='pubsub#type' type='text-single'
+                                label='Specify the semantic type of payload data to be provided at this node.'>
+                            <value>urn:example:e2ee:bundle</value>
+                            </field>
+                            <field var='pubsub#dataform_xslt' type='text-single' label='Payload XSLT'/>
+                        </x>
+                        </configure>
+                    </pubsub>
+                </iq>`;
 
-            const result = await promise;
-            expect(result).toEqual({
-                access_model: null,
-                dataform_xslt: null,
-                deliver_notifications: true,
-                deliver_payloads: true,
-                item_expire: '604800',
-                max_items: '10',
-                max_payload_size: '1028',
-                notification_type: null,
-                notify_config: false,
-                notify_delete: false,
-                notify_retract: false,
-                notify_sub: false,
-                persist_items: true,
-                presence_based_delivery: false,
-                publish_model: null,
-                purge_offline: false,
-                roster_groups_allowed: null,
-                send_last_published_item: null,
-                subscribe: true,
-                title: null,
-                type: 'urn:example:e2ee:bundle',
-            });
-        })
-    );
+                _converse.api.connection.get()._dataRecv(mock.createRequest(response));
+
+                const result = await promise;
+                expect(result).toEqual({
+                    access_model: null,
+                    dataform_xslt: null,
+                    deliver_notifications: true,
+                    deliver_payloads: true,
+                    item_expire: '604800',
+                    max_items: '10',
+                    max_payload_size: '1028',
+                    notification_type: null,
+                    notify_config: false,
+                    notify_delete: false,
+                    notify_retract: false,
+                    notify_sub: false,
+                    persist_items: true,
+                    presence_based_delivery: false,
+                    publish_model: null,
+                    purge_offline: false,
+                    roster_groups_allowed: null,
+                    send_last_published_item: null,
+                    subscribe: true,
+                    title: null,
+                    type: 'urn:example:e2ee:bundle',
+                });
+            })
+        );
+
+        it(
+            "handles error cases",
+            mock.initConverse([], {}, async function (_converse) {
+
+                await mock.waitForRoster(_converse, 'current', 0);
+                const { api } = _converse;
+                const sent_stanzas = api.connection.get().sent_stanzas;
+                const own_jid = _converse.session.get('jid');
+
+                const node = 'princely_musings';
+                const pubsub_jid = 'pubsub.shakespeare.lit';
+
+                let promise = api.pubsub.config.get(pubsub_jid, node);
+                let sent_stanza = await u.waitUntil(() =>
+                    sent_stanzas.filter((iq) => sizzle('pubsub configure', iq)).pop()
+                );
+                let response = stx`<iq type='error'
+                        xmlns="jabber:client"
+                        from='${pubsub_jid}'
+                        to='${own_jid}'
+                        id="${sent_stanza.getAttribute('id')}">
+                    <error type='cancel'>
+                        <feature-not-implemented xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+                        <unsupported xmlns='http://jabber.org/protocol/pubsub#errors' feature='config-node'/>
+                    </error>
+                </iq>`
+
+                let first_error_thrown = false;
+                promise.catch((e) => {
+                    expect(e instanceof errors.NotImplementedError).toBe(true);
+                    first_error_thrown = true;
+                }).finally(() => {
+                    expect(first_error_thrown).toBe(true);
+                });
+                _converse.api.connection.get()._dataRecv(mock.createRequest(response));
+
+                promise = api.pubsub.config.get(pubsub_jid, node);
+                sent_stanza = await u.waitUntil(() =>
+                    sent_stanzas.filter((iq) => sizzle('pubsub configure', iq)).pop()
+                );
+                response = stx`<iq type='error'
+                        xmlns="jabber:client"
+                        from='${pubsub_jid}'
+                        to='${own_jid}'
+                        id="${sent_stanza.getAttribute('id')}">
+                    <error type='auth'><forbidden xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>
+                </iq>`;
+
+                let second_error_thrown = false;
+                promise.catch((e) => {
+                    expect(e instanceof errors.ForbiddenError).toBe(true);
+                    second_error_thrown = true;
+                }).finally(() => {
+                    expect(second_error_thrown).toBe(true);
+                });
+                _converse.api.connection.get()._dataRecv(mock.createRequest(response));
+
+                promise = api.pubsub.config.get(pubsub_jid, node);
+                sent_stanza = await u.waitUntil(() =>
+                    sent_stanzas.filter((iq) => sizzle('pubsub configure', iq)).pop()
+                );
+                response = stx`<iq type='error'
+                        xmlns="jabber:client"
+                        from='${pubsub_jid}'
+                        to='${own_jid}'
+                        id="${sent_stanza.getAttribute('id')}">
+                    <error type='cancel'><item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>
+                </iq>`;
+
+                let third_error_thrown = false;
+                promise.catch((e) => {
+                    expect(e instanceof errors.ItemNotFoundError).toBe(true);
+                    third_error_thrown = true;
+                }).finally(() => {
+                    expect(third_error_thrown).toBe(true);
+                });
+                _converse.api.connection.get()._dataRecv(mock.createRequest(response));
+        }));
+    });
 });

+ 8 - 6
src/headless/shared/api/public.js

@@ -1,9 +1,14 @@
 /**
  * @typedef {module:shared-api-public.ConversePrivateGlobal} ConversePrivateGlobal
  */
+import { sprintf } from 'sprintf-js';
 import dayjs from 'dayjs';
 import sizzle from 'sizzle';
 import URI from 'urijs';
+import { Strophe, $build, $iq, $msg, $pres, stx } from 'strophe.js';
+import { Collection, Model } from "@converse/skeletor";
+import { filesize } from 'filesize';
+import { html } from 'lit';
 
 import api from './index.js';
 import _converse from '../_converse.js';
@@ -13,13 +18,9 @@ import ConnectionFeedback from './../connection/feedback.js';
 import u, { setLogLevelFromRoute } from '../../utils/index.js';
 import { ANONYMOUS, CHAT_STATES, KEYCODES, VERSION_NAME } from '../constants.js';
 import { isTestEnv } from '../../utils/session.js';
-import { Collection, Model } from "@converse/skeletor";
-import { Strophe, $build, $iq, $msg, $pres, stx } from 'strophe.js';
 import { TimeoutError } from '../errors.js';
-import { filesize } from 'filesize';
-import { html } from 'lit';
 import { initAppSettings } from '../settings/utils.js';
-import { sprintf } from 'sprintf-js';
+import * as errors from '../errors.js';
 
 _converse.api = api;
 
@@ -178,7 +179,7 @@ const converse = Object.assign(/** @type {ConversePrivateGlobal} */(window).conv
      * @property {function} converse.env.sprintf
      * @property {object} converse.env._           - The instance of [lodash-es](http://lodash.com) used by Converse.
      * @property {object} converse.env.dayjs       - [DayJS](https://github.com/iamkun/dayjs) date manipulation library.
-     * @property {object} converse.env.utils       - Module containing common utility methods used by Converse.
+     * @property {Array<Error>} converse.env.errors
      * @memberOf converse
      */
     'env': {
@@ -195,6 +196,7 @@ const converse = Object.assign(/** @type {ConversePrivateGlobal} */(window).conv
         URI,
         VERSION_NAME,
         dayjs,
+        errors,
         filesize,
         html,
         log,

+ 31 - 3
src/headless/shared/errors.js

@@ -1,16 +1,44 @@
+export class MethodNotImplementedError extends Error {}
+
 /**
  * Custom error for indicating timeouts
  * @namespace converse.env
  */
 export class TimeoutError extends Error {
-
     /**
      * @param  {string} message
      */
-    constructor (message) {
+    constructor(message) {
         super(message);
         this.retry_event_id = null;
     }
 }
 
-export class NotImplementedError extends Error {}
+export class StanzaError extends Error {
+    /**
+     * @param {Element} stanza
+     * @param {string} [message]
+     */
+    constructor(stanza, message) {
+        super(message);
+        this.name = stanza.nodeName;
+        this.stanza = stanza;
+    }
+}
+
+export class StanzaParseError extends StanzaError {
+    /**
+     * @param {Element} stanza
+     * @param {string} [message]
+     */
+    constructor(stanza, message) {
+        super(stanza, message);
+        this.name = 'StanzaParseError';
+    }
+}
+
+export class NotImplementedError extends StanzaError {}
+export class ForbiddenError extends StanzaError {}
+export class BadRequestError extends StanzaError {}
+export class NotAllowedError extends StanzaError {}
+export class ItemNotFoundError extends StanzaError {}

+ 2 - 1
src/headless/shared/index.js

@@ -1,7 +1,8 @@
 import * as parsers from './parsers.js';
 import * as constants from './constants.js';
+import * as errors from './errors.js';
 import api from './api/index.js';
 import _converse from './_converse';
 import i18n from './i18n';
 
-export { _converse, api, constants, i18n, parsers };
+export { _converse, api, constants, i18n, parsers, errors };

+ 4 - 4
src/headless/shared/model-with-messages.js

@@ -11,7 +11,7 @@ import converse from './api/public.js';
 import api from './api/index.js';
 import { isNewMessage } from '../plugins/chat/utils.js';
 import _converse from './_converse.js';
-import { NotImplementedError } from './errors.js';
+import { MethodNotImplementedError } from './errors.js';
 import { sendMarker, sendReceiptStanza, sendRetractionMessage } from './actions.js';
 import {parseMessage} from '../plugins/chat/parsers';
 
@@ -30,7 +30,7 @@ const { Strophe, $msg, u } = converse.env;
  */
 export default function ModelWithMessages(BaseModel) {
     /**
-     * @typedef {import('./parsers').StanzaParseError} StanzaParseError
+     * @typedef {import('./errors').StanzaParseError} StanzaParseError
      * @typedef {import('../plugins/chat/message').default} Message
      * @typedef {import('../plugins/chat/model').default} ChatBox
      * @typedef {import('../plugins/muc/muc').default} MUC
@@ -158,7 +158,7 @@ export default function ModelWithMessages(BaseModel) {
          * @param {MessageAttributes|Error} attrs_or_error
          */
         async onMessage(attrs_or_error) {
-            throw new NotImplementedError('onMessage is not implemented');
+            throw new MethodNotImplementedError('onMessage is not implemented');
         }
 
         /**
@@ -262,7 +262,7 @@ export default function ModelWithMessages(BaseModel) {
          * @return {Promise<MessageAttributes>}
          */
         async getOutgoingMessageAttributes(_attrs) {
-            throw new NotImplementedError('getOutgoingMessageAttributes is not implemented');
+            throw new MethodNotImplementedError('getOutgoingMessageAttributes is not implemented');
         }
 
         /**

+ 25 - 10
src/headless/shared/parsers.js

@@ -11,20 +11,35 @@ import { decodeHTMLEntities } from '../utils/html.js';
 import { getAttributes } from '../utils/stanza.js';
 import { rejectMessage } from './actions.js';
 import { XFORM_TYPE_MAP,  XFORM_VALIDATE_TYPE_MAP } from './constants.js';
+import * as errors from './errors.js';
 
 
 const { NS } = Strophe;
 
-export class StanzaParseError extends Error {
-    /**
-     * @param {string} message
-     * @param {Element} stanza
-     */
-    constructor (message, stanza) {
-        super(message);
-        this.name = 'StanzaParseError';
-        this.stanza = stanza;
+
+/**
+ * @param {Element} stanza
+ * @returns {errors.StanzaError|null}
+ */
+export function parseErrorStanza(stanza) {
+    const error = stanza.querySelector('error');
+    if (!error) return null;
+
+    const e = sizzle(`[xmlns="${Strophe.NS.STANZAS}"]`, error).pop();
+    const nodeName = e?.nodeName;
+
+    if (nodeName === 'feature-not-implemented') {
+        return new errors.NotImplementedError(stanza);
+    } else if (nodeName === 'forbidden') {
+        return new errors.ForbiddenError(stanza);
+    } else if (nodeName === 'bad-request') {
+        return new errors.BadRequestError(stanza);
+    } else if (nodeName === 'not-allowed') {
+        return new errors.NotAllowedError(stanza);
+    } else if (nodeName === 'item-not-found') {
+        return new errors.ItemNotFoundError(stanza);
     }
+    return new errors.StanzaError(stanza);
 }
 
 /**
@@ -292,7 +307,7 @@ export function throwErrorIfInvalidForward (stanza) {
     if (bare_forward) {
         rejectMessage(stanza, 'Forwarded messages not part of an encapsulating protocol are not supported');
         const from_jid = stanza.getAttribute('from');
-        throw new StanzaParseError(`Ignoring unencapsulated forwarded message from ${from_jid}`, stanza);
+        throw new errors.StanzaParseError(stanza, `Ignoring unencapsulated forwarded message from ${from_jid}`);
     }
 }
 

+ 2 - 2
src/headless/types/plugins/chat/model.d.ts

@@ -274,7 +274,7 @@ declare class ChatBox extends ChatBox_base {
      * @typedef {import('./message.js').default} Message
      * @typedef {import('../muc/muc.js').default} MUC
      * @typedef {import('./types').MessageAttributes} MessageAttributes
-     * @typedef {import('../../shared/parsers').StanzaParseError} StanzaParseError
+     * @typedef {import('../../shared/errors').StanzaParseError} StanzaParseError
      */
     defaults(): {
         bookmarked: boolean;
@@ -291,7 +291,7 @@ declare class ChatBox extends ChatBox_base {
     /**
      * @param {MessageAttributes|StanzaParseError} attrs_or_error
      */
-    onMessage(attrs_or_error: import("./types").MessageAttributes | import("../../shared/parsers").StanzaParseError): Promise<void>;
+    onMessage(attrs_or_error: import("./types").MessageAttributes | import("../../shared/errors").StanzaParseError): Promise<void>;
     onPresenceChanged(item: any): void;
     close(): Promise<void>;
     /**

+ 1 - 1
src/headless/types/plugins/chat/parsers.d.ts

@@ -4,5 +4,5 @@
  * @returns {Promise<import('./types.ts').MessageAttributes|StanzaParseError>}
  */
 export function parseMessage(stanza: Element): Promise<import("./types.ts").MessageAttributes | StanzaParseError>;
-import { StanzaParseError } from '../../shared/parsers';
+import { StanzaParseError } from '../../shared/errors.js';
 //# sourceMappingURL=parsers.d.ts.map

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

@@ -20,7 +20,7 @@ export function handleMessageStanza(stanza: Element | Builder): Promise<true | v
 export function enableCarbons(): Promise<void>;
 export type ChatBox = import("./model.js").default;
 export type MessageAttributes = import("./types.ts").MessageAttributes;
-export type StanzaParseError = import("../../shared/parsers").StanzaParseError;
+export type StanzaParseError = import("../../shared/errors").StanzaParseError;
 export type Builder = import("strophe.js").Builder;
 import { Model } from '@converse/skeletor';
 //# sourceMappingURL=utils.d.ts.map

+ 2 - 2
src/headless/types/plugins/muc/muc.d.ts

@@ -211,7 +211,7 @@ declare class MUC extends MUC_base {
      * @typedef {import('./types').MUCMessageAttributes} MUCMessageAttributes
      * @typedef {module:shared.converse.UserMessage} UserMessage
      * @typedef {import('strophe.js').Builder} Builder
-     * @typedef {import('../../shared/parsers').StanzaParseError} StanzaParseError
+     * @typedef {import('../../shared/errors').StanzaParseError} StanzaParseError
      */
     defaults(): {
         bookmarked: boolean;
@@ -754,7 +754,7 @@ declare class MUC extends MUC_base {
      * should be called.
      * @param {MUCMessageAttributes|StanzaParseError} attrs_or_error - A promise which resolves to the message attributes.
      */
-    onMessage(attrs_or_error: import("./types").MUCMessageAttributes | import("../../shared/parsers.js").StanzaParseError): Promise<void>;
+    onMessage(attrs_or_error: import("./types").MUCMessageAttributes | import("../../shared/errors.js").StanzaParseError): Promise<void>;
     /**
      * @param {Element} pres
      */

+ 2 - 2
src/headless/types/plugins/muc/occupant.d.ts

@@ -202,7 +202,7 @@ declare const MUCOccupant_base: {
 declare class MUCOccupant extends MUCOccupant_base {
     /**
      * @typedef {import('../chat/types').MessageAttributes} MessageAttributes
-     * @typedef {import('../../shared/parsers').StanzaParseError} StanzaParseError
+     * @typedef {import('../../shared/errors').StanzaParseError} StanzaParseError
      */
     constructor(attributes: any, options: any);
     vcard: any;
@@ -221,7 +221,7 @@ declare class MUCOccupant extends MUCOccupant_base {
      * This method houldn't be called directly, instead {@link MUC#queueMessage} should be called.
      * @param {MessageAttributes|StanzaParseError} attrs_or_error
      */
-    onMessage(attrs_or_error: import("../chat/types").MessageAttributes | import("../../shared/parsers").StanzaParseError): Promise<void>;
+    onMessage(attrs_or_error: import("../chat/types").MessageAttributes | import("../../shared/errors").StanzaParseError): Promise<void>;
     /**
      * Return roles which may be assigned to this occupant
      * @returns {typeof ROLES} - An array of assignable roles

+ 1 - 1
src/headless/types/plugins/muc/parsers.d.ts

@@ -28,5 +28,5 @@ export function parseMemberListIQ(iq: Element): import("./types").MemberListItem
 export function parseMUCPresence(stanza: Element, chatbox: MUC): import("./types").MUCPresenceAttributes;
 export type MUC = import("../muc/muc.js").default;
 export type MUCMessageAttributes = import("./types").MUCMessageAttributes;
-import { StanzaParseError } from '../../shared/parsers';
+import { StanzaParseError } from '../../shared/errors.js';
 //# sourceMappingURL=parsers.d.ts.map

+ 5 - 7
src/headless/types/shared/api/send.d.ts

@@ -1,12 +1,10 @@
 declare namespace _default {
     /**
      * @typedef {import('strophe.js').Builder} Builder
-     * @typedef {import('strophe.js').Stanza} Stanza
-     */
-    /**
+     *
      * Allows you to send XML stanzas.
      * @method _converse.api.send
-     * @param {Element|Builder|Stanza} stanza
+     * @param {Element|Builder} stanza
      * @returns {void}
      * @example
      * const msg = converse.env.$msg({
@@ -16,11 +14,11 @@ declare namespace _default {
      * });
      * _converse.api.send(msg);
      */
-    function send(stanza: Element | import("strophe.js").Builder | import("strophe.js").Stanza): void;
+    function send(stanza: Element | import("strophe.js").Builder): void;
     /**
      * Send an IQ stanza
      * @method _converse.api.sendIQ
-     * @param {Element|Builder|Stanza} stanza
+     * @param {Element|Builder} stanza
      * @param {number} [timeout] - The default timeout value is taken from
      *  the `stanza_timeout` configuration setting.
      * @param {boolean} [reject=true] - Whether an error IQ should cause the promise
@@ -30,7 +28,7 @@ declare namespace _default {
      *  If the IQ stanza being sent is of type `result` or `error`, there's
      *  nothing to wait for, so an already resolved promise is returned.
      */
-    function sendIQ(stanza: Element | import("strophe.js").Builder | import("strophe.js").Stanza, timeout?: number, reject?: boolean): Promise<any>;
+    function sendIQ(stanza: Element | import("strophe.js").Builder, timeout?: number, reject?: boolean): Promise<any>;
 }
 export default _default;
 //# sourceMappingURL=send.d.ts.map

+ 21 - 1
src/headless/types/shared/errors.d.ts

@@ -1,3 +1,5 @@
+export class MethodNotImplementedError extends Error {
+}
 /**
  * Custom error for indicating timeouts
  * @namespace converse.env
@@ -9,6 +11,24 @@ export class TimeoutError extends Error {
     constructor(message: string);
     retry_event_id: any;
 }
-export class NotImplementedError extends Error {
+export class StanzaError extends Error {
+    /**
+     * @param {Element} stanza
+     * @param {string} [message]
+     */
+    constructor(stanza: Element, message?: string);
+    stanza: Element;
+}
+export class StanzaParseError extends StanzaError {
+}
+export class NotImplementedError extends StanzaError {
+}
+export class ForbiddenError extends StanzaError {
+}
+export class BadRequestError extends StanzaError {
+}
+export class NotAllowedError extends StanzaError {
+}
+export class ItemNotFoundError extends StanzaError {
 }
 //# sourceMappingURL=errors.d.ts.map

+ 2 - 1
src/headless/types/shared/index.d.ts

@@ -3,5 +3,6 @@ import api from './api/index.js';
 import * as constants from './constants.js';
 import i18n from './i18n';
 import * as parsers from './parsers.js';
-export { _converse, api, constants, i18n, parsers };
+import * as errors from './errors.js';
+export { _converse, api, constants, i18n, parsers, errors };
 //# sourceMappingURL=index.d.ts.map

+ 6 - 8
src/headless/types/shared/parsers.d.ts

@@ -1,3 +1,8 @@
+/**
+ * @param {Element} stanza
+ * @returns {errors.StanzaError|null}
+ */
+export function parseErrorStanza(stanza: Element): errors.StanzaError | null;
 /**
  * Extract the XEP-0359 stanza IDs from the passed in stanza
  * and return a map containing them.
@@ -136,12 +141,5 @@ export function getInputType(field: Element): any;
  * @returns {import('./types').XForm}
  */
 export function parseXForm(stanza: Element): import("./types").XForm;
-export class StanzaParseError extends Error {
-    /**
-     * @param {string} message
-     * @param {Element} stanza
-     */
-    constructor(message: string, stanza: Element);
-    stanza: Element;
-}
+import * as errors from './errors.js';
 //# sourceMappingURL=parsers.d.ts.map