Pārlūkot izejas kodu

Improved error parsing

- Add TypeScript types
- Add a hook to allow plugins to do more feature-specific error parsing
JC Brand 6 mēneši atpakaļ
vecāks
revīzija
07a3b44363

+ 2 - 2
src/headless/plugins/mam/utils.js

@@ -22,8 +22,8 @@ const u = converse.env.utils;
 /**
  * @param {Element} iq
  */
-export function onMAMError(iq) {
-    const err = parseErrorStanza(iq);
+export async function onMAMError(iq) {
+    const err = await parseErrorStanza(iq);
     if (err?.name === 'feature-not-implemented') {
         log.warn(`Message Archive Management (XEP-0313) not supported by ${iq.getAttribute('from')}`);
     } else {

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

@@ -1717,7 +1717,7 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) {
             );
         } catch (e) {
             if (u.isErrorStanza(e)) {
-                const err = parseErrorStanza(e);
+                const err = await 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') {

+ 20 - 1
src/headless/plugins/pubsub/index.js

@@ -8,7 +8,7 @@ import converse from '../../shared/api/public.js';
 import pubsub_api from './api.js';
 import '../disco/index.js';
 
-const { Strophe } = converse.env;
+const { Strophe, sizzle } = converse.env;
 
 Strophe.addNamespace('PUBSUB_ERROR', Strophe.NS.PUBSUB + '#errors');
 
@@ -16,6 +16,25 @@ converse.plugins.add('converse-pubsub', {
     dependencies: ['converse-disco'],
 
     initialize() {
+        const { api } = _converse;
         Object.assign(_converse.api, pubsub_api);
+
+        api.listen.on(
+            'parseErrorStanza',
+            /**
+             * @param {Element} stanza
+             * @param {import('shared/types.js').ErrorExtra} extra
+             */
+            (stanza, extra) => {
+                const pubsub_err = sizzle(`error [xmlns="${Strophe.NS.PUBSUB_ERROR}"]`, stanza).pop();
+                if (pubsub_err) {
+                    return {
+                        ...extra,
+                        [Strophe.NS.PUBSUB_ERROR]: pubsub_err.nodeName,
+                    };
+                }
+                return extra;
+            }
+        );
     },
 });

+ 41 - 11
src/headless/shared/errors.js

@@ -16,30 +16,60 @@ export class TimeoutError extends Error {
 
 export class StanzaError extends Error {
     /**
-     * @param {Element} stanza
-     * @param {string} [message]
+     * @typedef {import("./types").ErrorName} ErrorName
+     * @typedef {import("./types").ErrorType} ErrorType
+     * @typedef {import("./types").ErrorExtra} ErrorExtra
      */
-    constructor(stanza, message) {
-        super(message);
-        this.name = stanza.nodeName;
-        this.stanza = stanza;
+
+    /**
+     * @param {ErrorName|'unknown'} name
+     * @param {Element} e - The <error> element from a stanza
+     * @param {Object} extra - Extra properties from plugin parsers
+     */
+    constructor(name, e, extra) {
+        super(e.querySelector('text')?.textContent ?? '');
+        /** @type {ErrorName} */
+        this.name = name
+        /** @type {ErrorType} */
+        this.type = /** @type {ErrorType} */ (e.getAttribute('type'));
+        /** @type {Element} */
+        this.el = e;
+        /** @type {ErrorExtra} */
+        this.extra = extra;
     }
 }
 
-export class StanzaParseError extends StanzaError {
+export class StanzaParseError extends Error {
     /**
      * @param {Element} stanza
      * @param {string} [message]
      */
     constructor(stanza, message) {
-        super(stanza, message);
+        super(message);
         this.name = 'StanzaParseError';
+        this.stanza = stanza;
     }
 }
 
-export class NotImplementedError extends StanzaError {}
-export class ForbiddenError extends StanzaError {}
 export class BadRequestError extends StanzaError {}
-export class NotAllowedError extends StanzaError {}
+export class ConflictError extends StanzaError {}
+export class FeatureNotImplementedError extends StanzaError {}
+export class ForbiddenError extends StanzaError {}
+export class GoneError extends StanzaError {}
+export class InternalServerError extends StanzaError {}
 export class ItemNotFoundError extends StanzaError {}
+export class JIDMalformedError extends StanzaError {}
 export class NotAcceptableError extends StanzaError {}
+export class NotAllowedError extends StanzaError {}
+export class NotAuthorizedError extends StanzaError {}
+export class PaymentRequiredError extends StanzaError {}
+export class RecipientUnavailableError extends StanzaError {}
+export class RedirectError extends StanzaError {}
+export class RegistrationRequiredError extends StanzaError {}
+export class RemoteServerNotFoundError extends StanzaError {}
+export class RemoteServerTimeoutError extends StanzaError {}
+export class ResourceConstraintError extends StanzaError {}
+export class ServiceUnavailableError extends StanzaError {}
+export class SubscriptionRequiredError extends StanzaError {}
+export class UndefinedConditionError extends StanzaError {}
+export class UnexpectedRequestError extends StanzaError {}

+ 62 - 19
src/headless/shared/parsers.js

@@ -16,32 +16,75 @@ import * as errors from './errors.js';
 
 const { NS } = Strophe;
 
-
 /**
- * @param {Element} stanza
- * @returns {errors.StanzaError|null}
+ * @param {Element|Error} stanza - The stanza to be parsed. As a convenience,
+ * an Error element can be passed in as well, so that this function can be
+ * called in a catch block without first checking if a stanza or Error
+ * element was received.
+ * @returns {Promise<Error|errors.StanzaError|null>}
  */
-export function parseErrorStanza(stanza) {
+export async function parseErrorStanza(stanza) {
+    if (stanza instanceof Error) return stanza;
+    if (stanza.getAttribute('type') !== 'error') return null;
+
     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);
-    } else if (nodeName === 'not-acceptable') {
-        return new errors.NotAcceptableError(stanza);
+    const name = e?.nodeName;
+
+    /**
+     * *Hook* which allows plugins to add application-specific error parsing
+     * @event _converse#parseErrorStanza
+     */
+    const extra = await api.hook('parseErrorStanza', stanza, {});
+
+    if (name === 'bad-request') {
+        return new errors.BadRequestError(name, error, extra);
+    } else if (name === 'conflict') {
+        return new errors.ConflictError(name, error, extra);
+    } else if (name === 'feature-not-implemented') {
+        return new errors.FeatureNotImplementedError(name, error, extra);
+    } else if (name === 'forbidden') {
+        return new errors.ForbiddenError(name, error, extra);
+    } else if (name === 'gone') {
+        return new errors.GoneError(name, error, extra);
+    } else if (name === 'internal-server-error') {
+        return new errors.InternalServerError(name, error, extra);
+    } else if (name === 'item-not-found') {
+        return new errors.ItemNotFoundError(name, error, extra);
+    } else if (name === 'jid-malformed') {
+        return new errors.JIDMalformedError(name, error, extra);
+    } else if (name === 'not-acceptable') {
+        return new errors.NotAcceptableError(name, error, extra);
+    } else if (name === 'not-allowed') {
+        return new errors.NotAllowedError(name, error, extra);
+    } else if (name === 'not-authorized') {
+        return new errors.NotAuthorizedError(name, error, extra);
+    } else if (name === 'payment-required') {
+        return new errors.PaymentRequiredError(name, error, extra);
+    } else if (name === 'recipient-unavailable') {
+        return new errors.RecipientUnavailableError(name, error, extra);
+    } else if (name === 'redirect') {
+        return new errors.RedirectError(name, error, extra);
+    } else if (name === 'registration-required') {
+        return new errors.RegistrationRequiredError(name, error, extra);
+    } else if (name === 'remote-server-not-found') {
+        return new errors.RemoteServerNotFoundError(name, error, extra);
+    } else if (name === 'remote-server-timeout') {
+        return new errors.RemoteServerTimeoutError(name, error, extra);
+    } else if (name === 'resource-constraint') {
+        return new errors.ResourceConstraintError(name, error, extra);
+    } else if (name === 'service-unavailable') {
+        return new errors.ServiceUnavailableError(name, error, extra);
+    } else if (name === 'subscription-required') {
+        return new errors.SubscriptionRequiredError(name, error, extra);
+    } else if (name === 'undefined-condition') {
+        return new errors.UndefinedConditionError(name, error, extra);
+    } else if (name === 'unexpected-request') {
+        return new errors.UnexpectedRequestError(name, error, extra);
     }
-    return new errors.StanzaError(stanza);
+    return new errors.StanzaError('unknown', error);
 }
 
 /**

+ 29 - 0
src/headless/shared/types.ts

@@ -87,3 +87,32 @@ export type XEP372Reference = {
     value: string;
     uri: string;
 };
+
+export type ErrorExtra = Record<string, string>;
+
+// https://datatracker.ietf.org/doc/html/rfc6120#section-8.3
+export type ErrorName =
+    | 'bad-request'
+    | 'conflict'
+    | 'feature-not-implemented'
+    | 'forbidden'
+    | 'gone'
+    | 'internal-server-error'
+    | 'item-not-found'
+    | 'jid-malformed'
+    | 'not-acceptable'
+    | 'not-allowed'
+    | 'not-authorized'
+    | 'payment-required'
+    | 'recipient-unavailable'
+    | 'redirect'
+    | 'registration-required'
+    | 'remote-server-not-found'
+    | 'remote-server-timeout'
+    | 'resource-constraint'
+    | 'service-unavailable'
+    | 'subscription-required'
+    | 'undefined-condition'
+    | 'unexpected-request';
+
+export type ErrorType = 'auth' | 'cancel' | 'continue' | 'modify' | 'wait';

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

@@ -1,7 +1,7 @@
 /**
  * @param {Element} iq
  */
-export function onMAMError(iq: Element): void;
+export function onMAMError(iq: Element): Promise<void>;
 /**
  * Handle returned IQ stanza containing Message Archive
  * Management (XEP-0313) preferences.

+ 53 - 4
src/headless/types/shared/errors.d.ts

@@ -12,6 +12,25 @@ export class TimeoutError extends Error {
     retry_event_id: any;
 }
 export class StanzaError extends Error {
+    /**
+     * @typedef {import("./types").ErrorName} ErrorName
+     * @typedef {import("./types").ErrorType} ErrorType
+     * @typedef {import("./types").ErrorExtra} ErrorExtra
+     */
+    /**
+     * @param {ErrorName|'unknown'} name
+     * @param {Element} e - The <error> element from a stanza
+     * @param {Object} extra - Extra properties from plugin parsers
+     */
+    constructor(name: import("./types").ErrorName | "unknown", e: Element, extra: any);
+    /** @type {ErrorType} */
+    type: import("./types").ErrorType;
+    /** @type {Element} */
+    el: Element;
+    /** @type {ErrorExtra} */
+    extra: import("./types").ErrorExtra;
+}
+export class StanzaParseError extends Error {
     /**
      * @param {Element} stanza
      * @param {string} [message]
@@ -19,18 +38,48 @@ export class StanzaError extends Error {
     constructor(stanza: Element, message?: string);
     stanza: Element;
 }
-export class StanzaParseError extends StanzaError {
+export class BadRequestError extends StanzaError {
+}
+export class ConflictError extends StanzaError {
 }
-export class NotImplementedError extends StanzaError {
+export class FeatureNotImplementedError extends StanzaError {
 }
 export class ForbiddenError extends StanzaError {
 }
-export class BadRequestError extends StanzaError {
+export class GoneError extends StanzaError {
 }
-export class NotAllowedError extends StanzaError {
+export class InternalServerError extends StanzaError {
 }
 export class ItemNotFoundError extends StanzaError {
 }
+export class JIDMalformedError extends StanzaError {
+}
 export class NotAcceptableError extends StanzaError {
 }
+export class NotAllowedError extends StanzaError {
+}
+export class NotAuthorizedError extends StanzaError {
+}
+export class PaymentRequiredError extends StanzaError {
+}
+export class RecipientUnavailableError extends StanzaError {
+}
+export class RedirectError extends StanzaError {
+}
+export class RegistrationRequiredError extends StanzaError {
+}
+export class RemoteServerNotFoundError extends StanzaError {
+}
+export class RemoteServerTimeoutError extends StanzaError {
+}
+export class ResourceConstraintError extends StanzaError {
+}
+export class ServiceUnavailableError extends StanzaError {
+}
+export class SubscriptionRequiredError extends StanzaError {
+}
+export class UndefinedConditionError extends StanzaError {
+}
+export class UnexpectedRequestError extends StanzaError {
+}
 //# sourceMappingURL=errors.d.ts.map

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

@@ -1,8 +1,11 @@
 /**
- * @param {Element} stanza
- * @returns {errors.StanzaError|null}
+ * @param {Element|Error} stanza - The stanza to be parsed. As a convenience,
+ * an Error element can be passed in as well, so that this function can be
+ * called in a catch block without first checking if a stanza or Error
+ * element was received.
+ * @returns {Promise<Error|errors.StanzaError|null>}
  */
-export function parseErrorStanza(stanza: Element): errors.StanzaError | null;
+export function parseErrorStanza(stanza: Element | Error): Promise<Error | errors.StanzaError | null>;
 /**
  * Extract the XEP-0359 stanza IDs from the passed in stanza
  * and return a map containing them.

+ 3 - 0
src/headless/types/shared/types.d.ts

@@ -61,5 +61,8 @@ export type XEP372Reference = {
     value: string;
     uri: string;
 };
+export type ErrorExtra = Record<string, string>;
+export type ErrorName = 'bad-request' | 'conflict' | 'feature-not-implemented' | 'forbidden' | 'gone' | 'internal-server-error' | 'item-not-found' | 'jid-malformed' | 'not-acceptable' | 'not-allowed' | 'not-authorized' | 'payment-required' | 'recipient-unavailable' | 'redirect' | 'registration-required' | 'remote-server-not-found' | 'remote-server-timeout' | 'resource-constraint' | 'service-unavailable' | 'subscription-required' | 'undefined-condition' | 'unexpected-request';
+export type ErrorType = 'auth' | 'cancel' | 'continue' | 'modify' | 'wait';
 export {};
 //# sourceMappingURL=types.d.ts.map