Skip to content
Merged
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ module.exports = {
"files": ["src/**/*.ts", "test/**/*.ts"],
"extends": ["matrix-org/ts"],
"rules": {
// TypeScript has its own version of this
"babel/no-invalid-this": "off",

"quotes": "off",
},
}],
Expand Down
54 changes: 23 additions & 31 deletions src/ClientWidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,15 +330,13 @@
});
}

const onErr = (e: any) => {
const onErr = (e: unknown) => {
console.error("[ClientWidgetApi] Failed to handle navigation: ", e);
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Error handling navigation"},
});
this.handleDriverError(e, request, "Error handling navigation");
};

try {
this.driver.navigate(request.data.uri.toString()).catch(e => onErr(e)).then(() => {
this.driver.navigate(request.data.uri.toString()).catch((e: unknown) => onErr(e)).then(() => {
return this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});
});
} catch (e) {
Expand Down Expand Up @@ -437,7 +435,7 @@
if (request.data.room_ids) {
askRoomIds = request.data.room_ids as string[];
if (!Array.isArray(askRoomIds)) {
askRoomIds = [askRoomIds as any as string];

Check warning on line 438 in src/ClientWidgetApi.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
}
for (const roomId of askRoomIds) {
if (!this.canUseRoomTimeline(roomId)) {
Expand Down Expand Up @@ -554,11 +552,9 @@
delay_id: sentEvent.delayId,
}),
});
}).catch(e => {
}).catch((e: unknown) => {
console.error("error sending event: ", e);
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Error sending event"},
});
this.handleDriverError(e, request, "Error sending event");
});
}

Expand All @@ -581,11 +577,9 @@
case UpdateDelayedEventAction.Send:
this.driver.updateDelayedEvent(request.data.delay_id, request.data.action).then(() => {
return this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});
}).catch(e => {
}).catch((e: unknown) => {
console.error("error updating delayed event: ", e);
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Error updating delayed event"},
});
this.handleDriverError(e, request, "Error updating delayed event");
});
break;
default:
Expand Down Expand Up @@ -618,9 +612,7 @@
await this.transport.reply<ISendToDeviceFromWidgetResponseData>(request, {});
} catch (e) {
console.error("error sending to-device event", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Error sending event"},
});
this.handleDriverError(e, request, "Error sending event");
}
}
}
Expand Down Expand Up @@ -735,9 +727,7 @@
);
} catch (e) {
console.error("error getting the relations", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Unexpected error while reading relations" },
});
this.handleDriverError(e, request, "Unexpected error while reading relations");
}
}

Expand Down Expand Up @@ -778,9 +768,7 @@
);
} catch (e) {
console.error("error searching in the user directory", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Unexpected error while searching in the user directory" },
});
this.handleDriverError(e, request, "Unexpected error while searching in the user directory");
}
}

Expand All @@ -800,9 +788,7 @@
);
} catch (e) {
console.error("error while getting the media configuration", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Unexpected error while getting the media configuration" },
});
this.handleDriverError(e, request, "Unexpected error while getting the media configuration");
}
}

Expand All @@ -822,9 +808,7 @@
);
} catch (e) {
console.error("error while uploading a file", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Unexpected error while uploading a file" },
});
this.handleDriverError(e, request, "Unexpected error while uploading a file");
}
}

Expand All @@ -844,12 +828,20 @@
);
} catch (e) {
console.error("error while downloading a file", e);
this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Unexpected error while downloading a file" },
});
this.handleDriverError(e, request, "Unexpected error while downloading a file");
}
}

private handleDriverError(e: unknown, request: IWidgetApiRequest, message: string) {
const data = this.driver.processError(e);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of making driver implementations responsible for serializing errors. I'd much rather have it be done by something akin to a method on the error object itself, but that's not simple because errors are of an unknown type here. For now, the current solution works, but I'm open to suggestions.

this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {
message,
...data,
},
});
}

private handleMessage(ev: CustomEvent<IWidgetApiRequest>) {
if (this.isStopped) return;
const actionEv = new CustomEvent(`action:${ev.detail.action}`, {
Expand Down
15 changes: 14 additions & 1 deletion src/WidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
import { ITransport } from "./transport/ITransport";
import { PostmessageTransport } from "./transport/PostmessageTransport";
import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./interfaces/WidgetApiAction";
import { IWidgetApiErrorResponseData } from "./interfaces/IWidgetApiErrorResponse";
import { IWidgetApiErrorResponseData, IWidgetApiErrorResponseDataDetails } from "./interfaces/IWidgetApiErrorResponse";
import { IStickerActionRequestData } from "./interfaces/StickerAction";
import { IStickyActionRequestData, IStickyActionResponseData } from "./interfaces/StickyAction";
import {
Expand Down Expand Up @@ -95,6 +95,19 @@ import {
UpdateDelayedEventAction,
} from "./interfaces/UpdateDelayedEventAction";

export class WidgetApiResponseError extends Error {
static {
this.prototype.name = this.name;
}

public constructor(
message: string,
public readonly data: IWidgetApiErrorResponseDataDetails,
) {
super(message);
}
}

/**
* API handler for widgets. This raises events for each action
* received as `action:${action}` (eg: "action:screenshot").
Expand Down
11 changes: 11 additions & 0 deletions src/driver/WidgetDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
IRoomEvent,
IRoomAccountData,
ITurnServer,
IWidgetApiErrorResponseDataDetails,
UpdateDelayedEventAction,
} from "..";

Expand Down Expand Up @@ -358,4 +359,14 @@ export abstract class WidgetDriver {
): Promise<{ file: XMLHttpRequestBodyInit }> {
throw new Error("Download file is not implemented");
}

/**
* Expresses an error thrown by this driver in a format compatible with the Widget API.
* @param error The error to handle.
* @returns The error expressed as a {@link IWidgetApiErrorResponseDataDetails},
* or undefined if it cannot be expressed as one.
*/
public processError(error: unknown): IWidgetApiErrorResponseDataDetails | undefined {
return undefined;
}
}
38 changes: 30 additions & 8 deletions src/interfaces/IWidgetApiErrorResponse.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2020 - 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,20 +16,42 @@

import { IWidgetApiResponse, IWidgetApiResponseData } from "./IWidgetApiResponse";

/**
* The format of errors returned by Matrix API requests
* made by a WidgetDriver.
*/
export interface IMatrixApiError {
/** The HTTP status code of the associated request. */
http_status: number; // eslint-disable-line camelcase
/** Any HTTP response headers that are relevant to the error. */
http_headers: {[name: string]: string}; // eslint-disable-line camelcase
/** The URL of the failed request. */
url: string;
/** @see {@link https://spec.matrix.org/latest/client-server-api/#standard-error-response} */
response: {
errcode: string;
error: string;
} & IWidgetApiResponseData; // extensible
}

export interface IWidgetApiErrorResponseDataDetails {
/** Set if the error came from a Matrix API request made by a widget driver */
matrix_api_error?: IMatrixApiError; // eslint-disable-line camelcase
}

export interface IWidgetApiErrorResponseData extends IWidgetApiResponseData {
error: {
/** A user-friendly string describing the error */
message: string;
};
} & IWidgetApiErrorResponseDataDetails;
}

export interface IWidgetApiErrorResponse extends IWidgetApiResponse {
response: IWidgetApiErrorResponseData;
}

export function isErrorResponse(responseData: IWidgetApiResponseData): boolean {
if ("error" in responseData) {
const err = <IWidgetApiErrorResponseData>responseData;
return !!err.error.message;
}
return false;
export function isErrorResponse(responseData: IWidgetApiResponseData): responseData is IWidgetApiErrorResponseData {
const error = responseData.error;
return typeof error === "object" && error !== null &&
"message" in error && typeof error.message === "string";
}
20 changes: 11 additions & 9 deletions src/transport/ITransport.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2020 - 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -71,11 +71,12 @@ export interface ITransport extends EventEmitter {

/**
* Sends a request to the remote end.
* @param {WidgetApiAction} action The action to send.
* @param {IWidgetApiRequestData} data The request data.
* @returns {Promise<IWidgetApiResponseData>} A promise which resolves
* to the remote end's response, or throws with an Error if the request
* failed.
* @param action The action to send.
* @param data The request data.
* @returns A promise which resolves to the remote end's response.
* @throws {Error} if the request failed with a generic error.
* @throws {WidgetApiResponseError} if the request failed with error details
* that can be communicated to the Widget API.
*/
send<T extends IWidgetApiRequestData, R extends IWidgetApiResponseData = IWidgetApiAcknowledgeResponseData>(
action: WidgetApiAction,
Expand All @@ -88,9 +89,10 @@ export interface ITransport extends EventEmitter {
* data.
* @param {WidgetApiAction} action The action to send.
* @param {IWidgetApiRequestData} data The request data.
* @returns {Promise<IWidgetApiResponseData>} A promise which resolves
* to the remote end's response, or throws with an Error if the request
* failed.
* @returns {Promise<IWidgetApiResponseData>} A promise which resolves to the remote end's response
* @throws {Error} if the request failed with a generic error.
* @throws {WidgetApiResponseError} if the request failed with error details
* that can be communicated to the Widget API.
*/
sendComplete<T extends IWidgetApiRequestData, R extends IWidgetApiResponse>(action: WidgetApiAction, data: T)
: Promise<R>;
Expand Down
8 changes: 4 additions & 4 deletions src/transport/PostmessageTransport.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2020 - 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,11 +19,11 @@ import { ITransport } from "./ITransport";
import {
invertedDirection,
isErrorResponse,
IWidgetApiErrorResponseData,
IWidgetApiRequest,
IWidgetApiRequestData,
IWidgetApiResponse,
IWidgetApiResponseData,
WidgetApiResponseError,
WidgetApiAction,
WidgetApiDirection,
WidgetApiToWidgetAction,
Expand Down Expand Up @@ -194,8 +194,8 @@ export class PostmessageTransport extends EventEmitter implements ITransport {
if (!req) return; // response to an unknown request

if (isErrorResponse(response.response)) {
const err = <IWidgetApiErrorResponseData>response.response;
req.reject(new Error(err.error.message));
const {message, ...data} = response.response.error;
req.reject(new WidgetApiResponseError(message, data));
} else {
req.resolve(response);
}
Expand Down
Loading
Loading