From 60eeb864157f3caaebdb8a13815b7ad03bf1abb0 Mon Sep 17 00:00:00 2001 From: Gnuxie <50846879+Gnuxie@users.noreply.github.com> Date: Thu, 30 Apr 2026 19:14:15 +0100 Subject: [PATCH] Tighten ActionException wrapping to only accept Error. (#1111) * Tighten ActionException wrapping to only accept Error. For months we had a bug where the "exception kind" enum was being provided as the error instead of the wrapped error in bot sdk wrapper code. This exception kind enum was optional in the factory method being used for the MatrixException type and so this resulted in draupnir reporting simply `undefined` when there were errors. Which was a major issue because not only did it make it difficult to track problems down, but it made the software look like shit. Remarkably we were still able to remotely diagnose problems essentially blind here. But it was caused by the ActionException accepting `unknown` for Error in order to tolerate a number of causes. We figure that this is unacceptable because it allows for these kinds of bugs and also that it delays finding out where we are calling broken apis. Closes https://github.com/the-draupnir-project/Draupnir/issues/759. Closes https://github.com/the-draupnir-project/planning/issues/137. * Flakey test idk. --- .changeset/loud-lions-stick.md | 7 ++ .changeset/witty-dancers-join.md | 8 ++ .../appservice/AppServiceDraupnirManager.ts | 3 +- .../SqliteRoomStateBackingStore.ts | 3 +- apps/draupnir/src/commands/Rooms.tsx | 6 +- .../protections/DraupnirNews/DraupnirNews.tsx | 3 +- .../src/queues/EventRedactionQueue.ts | 3 +- apps/draupnir/src/report/ReportPoller.ts | 10 ++- .../test/integration/reportPollingTest.ts | 2 +- .../src/Client/BotSDKBaseClient.ts | 76 +++++++------------ .../src/Client/BotSDKJunkErrors.ts | 54 +++++++++++++ .../src/ClientManagement/ClientManagement.ts | 3 +- .../src/ClientManagement/JoinedRoomsSafe.ts | 3 +- .../src/Interface/MatrixData.ts | 10 ++- .../src/PolicyList/PolicyListManager.ts | 11 ++- .../Protection/MjolnirProtectedRoomsStore.ts | 11 ++- .../MjolnirWatchedPolicyRoomsStore.ts | 11 ++- .../StateTracking/RoomMembershipManager.ts | 6 +- .../src/SynapseAdmin/SynapseAdminClient.ts | 15 +++- .../src/index.ts | 1 + .../src/Interface/ActionException.ts | 47 +++++++++--- .../src/Interface/MatrixException.ts | 4 +- .../src/Interface/Task.ts | 8 +- .../src/Interface/Value.ts | 10 ++- 24 files changed, 225 insertions(+), 90 deletions(-) create mode 100644 .changeset/loud-lions-stick.md create mode 100644 .changeset/witty-dancers-join.md create mode 100644 packages/matrix-protection-suite-for-matrix-bot-sdk/src/Client/BotSDKJunkErrors.ts diff --git a/.changeset/loud-lions-stick.md b/.changeset/loud-lions-stick.md new file mode 100644 index 00000000..fd440910 --- /dev/null +++ b/.changeset/loud-lions-stick.md @@ -0,0 +1,7 @@ +--- +"draupnir": patch +--- + +Fixed an issue where sometimes Draupnir would log cryptic errors such as +`undefined: undefined`. +https://github.com/the-draupnir-project/Draupnir/issues/759. diff --git a/.changeset/witty-dancers-join.md b/.changeset/witty-dancers-join.md new file mode 100644 index 00000000..320c97c2 --- /dev/null +++ b/.changeset/witty-dancers-join.md @@ -0,0 +1,8 @@ +--- +"@the-draupnir-project/matrix-protection-suite": minor +"draupnir": patch +--- + +Tighten ActionException to only accept `Error` instead of `unknown`, leading to +less mistakes. We now also offer and use a `ensureThrowableIsError` to use when +checking if third-party apis are throwing junk, such as the matrix-bot-sdk. diff --git a/apps/draupnir/src/appservice/AppServiceDraupnirManager.ts b/apps/draupnir/src/appservice/AppServiceDraupnirManager.ts index de700d63..a6bd7dae 100644 --- a/apps/draupnir/src/appservice/AppServiceDraupnirManager.ts +++ b/apps/draupnir/src/appservice/AppServiceDraupnirManager.ts @@ -29,6 +29,7 @@ import { RoomCreator, RoomVersionMirror, Task, + assertThrowableIsError, isError, } from "matrix-protection-suite"; import { Draupnir } from "../Draupnir"; @@ -388,7 +389,7 @@ export class AppServiceDraupnirManager { return ActionException.Result( `Could not start draupnir ${clientUserID} for owner ${mjolnirRecord.owner}`, { - exception: e, + exception: assertThrowableIsError(e), exceptionKind: ActionExceptionKind.Unknown, } ); diff --git a/apps/draupnir/src/backingstore/better-sqlite3/SqliteRoomStateBackingStore.ts b/apps/draupnir/src/backingstore/better-sqlite3/SqliteRoomStateBackingStore.ts index e688183a..20871f20 100644 --- a/apps/draupnir/src/backingstore/better-sqlite3/SqliteRoomStateBackingStore.ts +++ b/apps/draupnir/src/backingstore/better-sqlite3/SqliteRoomStateBackingStore.ts @@ -13,6 +13,7 @@ import { RoomStateRevision, StateChange, StateEvent, + assertThrowableIsError, isError, } from "matrix-protection-suite"; import { @@ -232,7 +233,7 @@ export class SqliteRoomStateBackingStore } catch (e) { return Promise.resolve( ActionException.Result(`Unable to forget the room ${roomID}`, { - exception: e, + exception: assertThrowableIsError(e), exceptionKind: ActionExceptionKind.Unknown, }) ); diff --git a/apps/draupnir/src/commands/Rooms.tsx b/apps/draupnir/src/commands/Rooms.tsx index 64672e77..931014f5 100644 --- a/apps/draupnir/src/commands/Rooms.tsx +++ b/apps/draupnir/src/commands/Rooms.tsx @@ -15,6 +15,7 @@ import { Ok, Revision, WatchedPolicyRoom, + assertThrowableIsError, isError, } from "matrix-protection-suite"; import { @@ -244,7 +245,10 @@ export const DraupnirRoomsRemoveCommand = describeCommand({ } catch (exception) { return ActionException.Result( `Failed to leave ${roomRef.toPermalink()} - the room is no longer being protected, but the bot could not leave.`, - { exceptionKind: ActionExceptionKind.Unknown, exception } + { + exceptionKind: ActionExceptionKind.Unknown, + exception: assertThrowableIsError(exception), + } ); } return Ok(undefined); diff --git a/apps/draupnir/src/protections/DraupnirNews/DraupnirNews.tsx b/apps/draupnir/src/protections/DraupnirNews/DraupnirNews.tsx index c5fb0ed0..bd74f9ad 100644 --- a/apps/draupnir/src/protections/DraupnirNews/DraupnirNews.tsx +++ b/apps/draupnir/src/protections/DraupnirNews/DraupnirNews.tsx @@ -9,6 +9,7 @@ import { ActionException, ActionExceptionKind, allocateProtection, + assertThrowableIsError, ConstantPeriodBatch, describeProtection, EDStatic, @@ -136,7 +137,7 @@ async function fetchNews(newsURL: string): Promise> { (json) => Value.Decode(DraupnirNewsBlob, json), (error: unknown) => ActionException.Result("unable to fetch news", { - exception: error, + exception: assertThrowableIsError(error), exceptionKind: ActionExceptionKind.Unknown, }) ); diff --git a/apps/draupnir/src/queues/EventRedactionQueue.ts b/apps/draupnir/src/queues/EventRedactionQueue.ts index 53f28a85..9914c9f6 100644 --- a/apps/draupnir/src/queues/EventRedactionQueue.ts +++ b/apps/draupnir/src/queues/EventRedactionQueue.ts @@ -14,6 +14,7 @@ import ManagementRoomOutput from "../managementroom/ManagementRoomOutput"; import { MatrixSendClient } from "matrix-protection-suite-for-matrix-bot-sdk"; import { ActionExceptionKind, + assertThrowableIsError, RoomUpdateError, RoomUpdateException, } from "matrix-protection-suite"; @@ -157,7 +158,7 @@ export class EventRedactionQueue { const error = new RoomUpdateException( MatrixRoomReference.fromRoomID(redaction.roomID), ActionExceptionKind.Unknown, - e, + assertThrowableIsError(e), message ); errors.push(error); diff --git a/apps/draupnir/src/report/ReportPoller.ts b/apps/draupnir/src/report/ReportPoller.ts index 55fec39e..3a3619a5 100644 --- a/apps/draupnir/src/report/ReportPoller.ts +++ b/apps/draupnir/src/report/ReportPoller.ts @@ -23,6 +23,7 @@ import { Ok, RoomEvent, Task, + assertThrowableIsError, isError, } from "matrix-protection-suite"; @@ -120,7 +121,10 @@ export class ReportPoller { (exception: unknown) => ActionException.Result( `Failed to retrieve the context for an event ${report.event_id}`, - { exception, exceptionKind: ActionExceptionKind.Unknown } + { + exception: assertThrowableIsError(exception), + exceptionKind: ActionExceptionKind.Unknown, + } ) ); if (isError(eventContext)) { @@ -159,7 +163,7 @@ export class ReportPoller { } catch (e) { const error = new ActionException( ActionExceptionKind.Unknown, - e, + assertThrowableIsError(e), "Unable to set account data for report poller token" ); await this.draupnir.managementRoomOutput.logMessage( @@ -179,7 +183,7 @@ export class ReportPoller { } catch (e) { const error = new ActionException( ActionExceptionKind.Unknown, - e, + assertThrowableIsError(e), "failed to get abuse reports" ); await this.draupnir.managementRoomOutput.logMessage( diff --git a/apps/draupnir/test/integration/reportPollingTest.ts b/apps/draupnir/test/integration/reportPollingTest.ts index 2549055b..c9769357 100644 --- a/apps/draupnir/test/integration/reportPollingTest.ts +++ b/apps/draupnir/test/integration/reportPollingTest.ts @@ -82,7 +82,7 @@ describe("Test: Report polling", function (this: Mocha.Suite) { await reportEvent(); } // wait for them to come down the poll. - await new Promise((resolve) => setTimeout(resolve, 3000)); + await new Promise((resolve) => setTimeout(resolve, 5000)); expect(reportsFound.size).toBe(20); expect(duplicateReports.size).toBe(0); } as unknown as Mocha.AsyncFunc); diff --git a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Client/BotSDKBaseClient.ts b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Client/BotSDKBaseClient.ts index 74719a37..834c4661 100644 --- a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Client/BotSDKBaseClient.ts +++ b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Client/BotSDKBaseClient.ts @@ -1,8 +1,7 @@ -// SPDX-FileCopyrightText: 2024 Gnuxie +// SPDX-FileCopyrightText: 2024 - 2026 Gnuxie // // SPDX-License-Identifier: AFL-3.0 -import { StaticDecode, Type } from "@sinclair/typebox"; import { MatrixError } from "@vector-im/matrix-bot-sdk"; import { ActionException, @@ -54,17 +53,12 @@ import { } from "@the-draupnir-project/matrix-basic-types"; import { resolveRoomReferenceSafe } from "../SafeMatrixClient"; import { ResultError } from "@gnuxie/typescript-result"; -import util from "util"; import { RoomReactionSender } from "matrix-protection-suite/dist/Client/RoomReactionSender"; import { RoomEventGetter } from "matrix-protection-suite/dist/Client/RoomEventGetter"; +import { BotSDKJunkError, toMatrixJunkError } from "./BotSDKJunkErrors"; const log = new Logger("BotSDKBaseClient"); -const WeakError = Type.Object({ - message: Type.String(), - name: Type.String(), -}); - function toRoomID(room: MatrixRoomID | StringRoomID): StringRoomID { return typeof room === "string" ? room : room.toRoomIDOrAlias(); } @@ -80,39 +74,17 @@ function matrixExceptionFromMatrixError( }); } -function actionExceptionFromWeakError( - error: StaticDecode -): ActionResult { - return ActionException.Result(error.message, { +function matrixExceptionFromMatrixJunkError( + error: BotSDKJunkError +): ActionResult { + return MatrixException.R({ exception: error, - exceptionKind: ActionExceptionKind.Unknown, + matrixErrorCode: error.matrixErrorCode, + matrixErrorMessage: error.matrixErrorMessage, + message: error.message, }); } -function unknownError(error: unknown): never { - const printedError = (() => { - if (typeof error === "object" && error !== null) { - // eslint-disable-next-line @typescript-eslint/no-base-to-string - const toString = error.toString(); - if (toString !== "[object Object]") { - return toString; - } - } - try { - return JSON.stringify(error); - } catch { - return util.inspect(error, { - depth: 2, - maxArrayLength: 10, - breakLength: 80, - }); - } - })(); - throw new TypeError( - `What on earth are you throwing exactly? because it isn't an error: ${printedError}` - ); -} - /** * This is a utility to extract the raw matrix event from a wrapper type * that vector bot-sdk decided to use inappropriately. @@ -154,25 +126,31 @@ export function resultifyBotSDKRequestErrorWith404AsUndefined( if (is404(error)) { return Ok(undefined); } - if (error instanceof MatrixError) { - return matrixExceptionFromMatrixError(error); - } else if (Value.Check(WeakError, error)) { - return actionExceptionFromWeakError(error); - } else { - unknownError(error); + const coercedError = toMatrixJunkError(error); + if (coercedError instanceof MatrixError) { + return matrixExceptionFromMatrixError(coercedError); + } else if (coercedError instanceof BotSDKJunkError) { + return matrixExceptionFromMatrixJunkError(coercedError); } + return ActionException.Result(coercedError.message, { + exception: coercedError, + exceptionKind: ActionExceptionKind.Unknown, + }); } export function resultifyBotSDKRequestError( error: unknown ): ActionResult { - if (error instanceof MatrixError) { - return matrixExceptionFromMatrixError(error); - } else if (Value.Check(WeakError, error)) { - return actionExceptionFromWeakError(error); - } else { - unknownError(error); + const coercedError = toMatrixJunkError(error); + if (coercedError instanceof MatrixError) { + return matrixExceptionFromMatrixError(coercedError); + } else if (coercedError instanceof BotSDKJunkError) { + return matrixExceptionFromMatrixJunkError(coercedError); } + return ActionException.Result(coercedError.message, { + exception: coercedError, + exceptionKind: ActionExceptionKind.Unknown, + }); } export class BotSDKBaseClient diff --git a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Client/BotSDKJunkErrors.ts b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Client/BotSDKJunkErrors.ts new file mode 100644 index 00000000..1f9448d0 --- /dev/null +++ b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Client/BotSDKJunkErrors.ts @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2026 Gnuxie +// +// SPDX-License-Identifier: Apache-2.0 + +import { Type } from "@sinclair/typebox"; +import { MatrixError } from "@vector-im/matrix-bot-sdk"; +import { Value, assertThrowableIsError } from "matrix-protection-suite"; + +const MatrixErrorBody = Type.Object({ + errcode: Type.String(), + error: Type.String(), + retry_after_ms: Type.Optional(Type.Number()), +}); + +// The matrix-bot-sdk appservice code sometimes throws junk like `{ body: result }` +// for example in`Intent.ensureRegistered()`. +const MatrixErrorContainer = Type.Object({ + body: MatrixErrorBody, + statusCode: Type.Optional(Type.Number()), + headers: Type.Optional(Type.Record(Type.String(), Type.String())), +}); + +/** + * This exists because the matrix-bot-sdk and matrix-appservice-bridge have + * various inconsistent ways of throwing errors. And Draupnir's own code + * probably makes matters worse in utils.ts. So this is a wrapper for all of + * the undesirable junk that gets thrown. Eventually we want to replace as + * many bot-sdk apis as possible with the matrix-protection-suite ClientPlatform + * an also then implement those apis using fetch. + */ +export class BotSDKJunkError extends Error { + public constructor( + public readonly raw: unknown, + public readonly matrixErrorCode: string, + public readonly matrixErrorMessage: string + ) { + super( + `matrix-bot-sdk threw a non-Error matrix payload: ${matrixErrorCode}: ${matrixErrorMessage}` + ); + } +} + +export function toMatrixJunkError(error: unknown): BotSDKJunkError | Error { + if (error instanceof MatrixError) { + return error; + } + if (Value.Check(MatrixErrorContainer, error)) { + return new BotSDKJunkError(error, error.body.errcode, error.body.error); + } + if (Value.Check(MatrixErrorBody, error)) { + return new BotSDKJunkError(error, error.errcode, error.error); + } + return assertThrowableIsError(error); +} diff --git a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/ClientManagement.ts b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/ClientManagement.ts index f59f881c..ce8179f3 100644 --- a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/ClientManagement.ts +++ b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/ClientManagement.ts @@ -9,6 +9,7 @@ import { ClientRooms, ClientsInRoomMap, Ok, + assertThrowableIsError, } from "matrix-protection-suite"; import { MatrixSendClient } from "../MatrixEmitter"; import { @@ -34,7 +35,7 @@ export async function joinedRoomsSafe( (rooms) => Ok(rooms as StringRoomID[]), (exception: unknown) => ActionException.Result(`Unable to get joined rooms`, { - exception, + exception: assertThrowableIsError(exception), exceptionKind: ActionExceptionKind.Unknown, }) ); diff --git a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/JoinedRoomsSafe.ts b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/JoinedRoomsSafe.ts index 82d9adec..2d62e154 100644 --- a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/JoinedRoomsSafe.ts +++ b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/JoinedRoomsSafe.ts @@ -7,6 +7,7 @@ import { ActionExceptionKind, JoinedRoomsSafe, Ok, + assertThrowableIsError, } from "matrix-protection-suite"; import { MatrixSendClient } from "../MatrixEmitter"; import { StringRoomID } from "@the-draupnir-project/matrix-basic-types"; @@ -22,7 +23,7 @@ export function makeJoinedRoomsSafe( ActionException.Result( `Unable to fetch the joined rooms for ${clientUserID}`, { - exception, + exception: assertThrowableIsError(exception), exceptionKind: ActionExceptionKind.Unknown, } ) diff --git a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Interface/MatrixData.ts b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Interface/MatrixData.ts index 12830aae..7d48a750 100644 --- a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Interface/MatrixData.ts +++ b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Interface/MatrixData.ts @@ -12,6 +12,7 @@ import { PersistentConfigBackend, RoomStateRevisionIssuer, Value, + assertThrowableIsError, isError, } from "matrix-protection-suite"; import { MatrixSendClient } from "../MatrixEmitter"; @@ -74,7 +75,10 @@ export class BotSDKMatrixAccountData implements MatrixAccountData { ? Ok(undefined) : ActionException.Result( `Encountered an error when requesting matrix account data ${this.eventType}`, - { exception: error, exceptionKind: ActionExceptionKind.Unknown } + { + exception: assertThrowableIsError(error), + exceptionKind: ActionExceptionKind.Unknown, + } ) ); } @@ -91,7 +95,7 @@ export class BotSDKMatrixAccountData implements MatrixAccountData { ActionException.Result( `Unable to store matrix account data ${this.eventType}`, { - exception, + exception: assertThrowableIsError(exception), exceptionKind: ActionExceptionKind.Unknown, } ) @@ -162,7 +166,7 @@ export class BotSDKMatrixStateData implements MatrixStateData { ActionException.Result( `Unable to store the matrix state data ${this.eventType}`, { - exception, + exception: assertThrowableIsError(exception), exceptionKind: ActionExceptionKind.Known, } ) diff --git a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/PolicyList/PolicyListManager.ts b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/PolicyList/PolicyListManager.ts index 1c418284..61f12320 100644 --- a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/PolicyList/PolicyListManager.ts +++ b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/PolicyList/PolicyListManager.ts @@ -19,6 +19,7 @@ import { ActionException, ActionExceptionKind, Ok, + assertThrowableIsError, isError, PolicyRuleEvent, isPolicyRuleEvent, @@ -96,7 +97,10 @@ export class BotSDKPolicyRoomManager implements PolicyRoomManager { (exception: unknown) => ActionException.Result( "Could not create a list because we could not find the mxid of the list creator.", - { exception, exceptionKind: ActionExceptionKind.Unknown } + { + exception: assertThrowableIsError(exception), + exceptionKind: ActionExceptionKind.Unknown, + } ) ); if (isError(creator)) { @@ -173,7 +177,10 @@ export class BotSDKPolicyRoomManager implements PolicyRoomManager { (exception: unknown) => ActionException.Result( "Could not create a matrix room to serve as the new policy list.", - { exception, exceptionKind: ActionExceptionKind.Unknown } + { + exception: assertThrowableIsError(exception), + exceptionKind: ActionExceptionKind.Unknown, + } ) ); } diff --git a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Protection/MjolnirProtectedRoomsStore.ts b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Protection/MjolnirProtectedRoomsStore.ts index c7d2c20e..fede0c71 100644 --- a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Protection/MjolnirProtectedRoomsStore.ts +++ b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Protection/MjolnirProtectedRoomsStore.ts @@ -12,6 +12,7 @@ import { Value, isError, Ok, + assertThrowableIsError, } from "matrix-protection-suite"; import { MatrixSendClient } from "../MatrixEmitter"; @@ -29,7 +30,10 @@ export class BotSDKMjolnirProtectedRoomsStore implements PersistentMatrixData< (exception: unknown) => ActionException.Result( `Unable to load the account data for mjolnir protected_rooms`, - { exception, exceptionKind: ActionExceptionKind.Unknown } + { + exception: assertThrowableIsError(exception), + exceptionKind: ActionExceptionKind.Unknown, + } ) ); } @@ -47,7 +51,10 @@ export class BotSDKMjolnirProtectedRoomsStore implements PersistentMatrixData< (exception: unknown) => ActionException.Result( `Unable to set account data for mjolnir protected_rooms event`, - { exception, exceptionKind: ActionExceptionKind.Unknown } + { + exception: assertThrowableIsError(exception), + exceptionKind: ActionExceptionKind.Unknown, + } ) ); } diff --git a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Protection/MjolnirWatchedPolicyRoomsStore.ts b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Protection/MjolnirWatchedPolicyRoomsStore.ts index d051cdec..0d6f3639 100644 --- a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Protection/MjolnirWatchedPolicyRoomsStore.ts +++ b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/Protection/MjolnirWatchedPolicyRoomsStore.ts @@ -12,6 +12,7 @@ import { Ok, MjolnirWatchedPolicyRoomsEvent, MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE, + assertThrowableIsError, } from "matrix-protection-suite"; import { MatrixSendClient } from "../MatrixEmitter"; @@ -31,7 +32,10 @@ export class BotSDKMjolnirWatchedPolicyRoomsStore implements PersistentMatrixDat (exception: unknown) => ActionException.Result( `Unable to load the account data for mjolnir watched_lists`, - { exception, exceptionKind: ActionExceptionKind.Unknown } + { + exception: assertThrowableIsError(exception), + exceptionKind: ActionExceptionKind.Unknown, + } ) ); } @@ -49,7 +53,10 @@ export class BotSDKMjolnirWatchedPolicyRoomsStore implements PersistentMatrixDat (exception: unknown) => ActionException.Result( `Unable to set account data for mjolnir watched_lists`, - { exception, exceptionKind: ActionExceptionKind.Unknown } + { + exception: assertThrowableIsError(exception), + exceptionKind: ActionExceptionKind.Unknown, + } ) ); } diff --git a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/StateTracking/RoomMembershipManager.ts b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/StateTracking/RoomMembershipManager.ts index d2bf4227..7bc7c0f7 100644 --- a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/StateTracking/RoomMembershipManager.ts +++ b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/StateTracking/RoomMembershipManager.ts @@ -12,6 +12,7 @@ import { RoomMembershipManager, RoomMembershipRevisionIssuer, Value, + assertThrowableIsError, isError, } from "matrix-protection-suite"; import { MembershipEvent } from "matrix-protection-suite"; @@ -40,7 +41,10 @@ async function getRoomMembershipEvents( (exception: unknown) => ActionException.Result( `Unable to query room members from ${room.toPermalink()}`, - { exception, exceptionKind: ActionExceptionKind.Unknown } + { + exception: assertThrowableIsError(exception), + exceptionKind: ActionExceptionKind.Unknown, + } ) ); if (isError(rawMembersResult)) { diff --git a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/SynapseAdmin/SynapseAdminClient.ts b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/SynapseAdmin/SynapseAdminClient.ts index 423edfa8..7babb782 100644 --- a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/SynapseAdmin/SynapseAdminClient.ts +++ b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/SynapseAdmin/SynapseAdminClient.ts @@ -19,6 +19,7 @@ import { SynapseAdminPostUserDeactivateRequest, SynapseReport, Value, + assertThrowableIsError, isError, } from "matrix-protection-suite"; import { MatrixSendClient } from "../MatrixEmitter"; @@ -76,7 +77,10 @@ export class SynapseAdminClient { (exception: unknown) => ActionException.Result( `Unable to query whether the user ${this.clientUserID} is a Synapse Admin`, - { exception, exceptionKind: ActionExceptionKind.Unknown } + { + exception: assertThrowableIsError(exception), + exceptionKind: ActionExceptionKind.Unknown, + } ) ); if (isError(response)) { @@ -107,7 +111,10 @@ export class SynapseAdminClient { (exception: unknown) => ActionException.Result( `Unable to deactivate the user ${targetUserID}`, - { exception, exceptionKind: ActionExceptionKind.Unknown } + { + exception: assertThrowableIsError(exception), + exceptionKind: ActionExceptionKind.Unknown, + } ) ); } @@ -127,7 +134,7 @@ export class SynapseAdminClient { (_) => Ok(undefined), (exception: unknown) => ActionException.Result(`Unable to delete the room ${roomID}`, { - exception, + exception: assertThrowableIsError(exception), exceptionKind: ActionExceptionKind.Unknown, }) ); @@ -155,7 +162,7 @@ export class SynapseAdminClient { ActionException.Result( `Unable to make the user ${userID} admin in room ${roomID}`, { - exception, + exception: assertThrowableIsError(exception), exceptionKind: ActionExceptionKind.Unknown, } ) diff --git a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/index.ts b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/index.ts index 44635f07..1458585a 100644 --- a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/index.ts +++ b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/index.ts @@ -5,6 +5,7 @@ export * from "./Client/BotSDKAllClient"; export * from "./Client/BotSDKBaseClient"; export * from "./Client/BotSDKClientPlatform"; +export * from "./Client/BotSDKJunkErrors"; export * from "./ClientManagement/ClientCapabilityFactory"; export * from "./ClientManagement/ClientManagement"; diff --git a/packages/matrix-protection-suite/src/Interface/ActionException.ts b/packages/matrix-protection-suite/src/Interface/ActionException.ts index 6c7cac16..c6e4d6ec 100644 --- a/packages/matrix-protection-suite/src/Interface/ActionException.ts +++ b/packages/matrix-protection-suite/src/Interface/ActionException.ts @@ -26,6 +26,41 @@ export enum ActionExceptionKind { Unknown = "Unknown", } +function describeThrowable(throwable: unknown): string { + if (typeof throwable === "string") { + return throwable; + } + if ( + typeof throwable === "number" || + typeof throwable === "boolean" || + typeof throwable === "bigint" || + typeof throwable === "symbol" || + throwable === null || + throwable === undefined + ) { + return String(throwable); + } + if (Array.isArray(throwable)) { + return `array(length=${throwable.length})`; + } + if (typeof throwable === "object") { + const keys = Object.keys(throwable); + return keys.length === 0 + ? "object with no enumerable keys" + : `object with keys: ${keys.join(",")}`; + } + return typeof throwable; +} + +export function assertThrowableIsError(throwable: unknown): Error { + if (throwable instanceof Error) { + return throwable; + } + throw new TypeError( + `Expected a thrown Error instance but received ${describeThrowable(throwable)}` + ); +} + // TODO: I wonder if we could allow message to be JSX? /** * `ActionExceptions` are used to convert throwables into `ActionError`s. @@ -39,8 +74,7 @@ export class ActionException extends ActionError { constructor( public readonly exceptionKind: ActionExceptionKind, - // make a call to only allow Error in a moment. - public readonly exception: unknown, + public readonly exception: Error, message: string, { uuid = randomUUID(), @@ -69,7 +103,7 @@ export class ActionException extends ActionError { */ public static Result( message: string, - options: { exception: unknown; exceptionKind: ActionExceptionKind } + options: { exception: Error; exceptionKind: ActionExceptionKind } ): ActionResult { return ResultError( new ActionException(options.exceptionKind, options.exception, message) @@ -93,11 +127,6 @@ export class ActionException extends ActionError { public toReadableString(): string { const mainDetail = `ActionException: ${this.uuid}\n${super.toReadableString()}`; - if (this.exception instanceof Error) { - return `${mainDetail}\nfrom error: ${this.exception.name}: ${this.exception.message}\n${this.exception.stack}`; - } - // @typescript-eslint/restrict-template-expressions - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `${mainDetail}\nfrom unknown: ${this.exception}`; + return `${mainDetail}\nfrom error: ${this.exception.name}: ${this.exception.message}\n${this.exception.stack}`; } } diff --git a/packages/matrix-protection-suite/src/Interface/MatrixException.ts b/packages/matrix-protection-suite/src/Interface/MatrixException.ts index 6853bf21..8742654d 100644 --- a/packages/matrix-protection-suite/src/Interface/MatrixException.ts +++ b/packages/matrix-protection-suite/src/Interface/MatrixException.ts @@ -7,7 +7,7 @@ import { ActionException, ActionExceptionKind } from "./ActionException"; export class MatrixException extends ActionException implements ActionError { public constructor( - exception: unknown, + exception: Error, public readonly matrixErrorCode: string, public readonly matrixErrorMessage: string, message: string = matrixErrorMessage, @@ -30,7 +30,7 @@ export class MatrixException extends ActionException implements ActionError { }): ActionResult { return ResultError( new MatrixException( - options.exceptionKind, + options.exception, options.matrixErrorCode, options.matrixErrorMessage, options.message, diff --git a/packages/matrix-protection-suite/src/Interface/Task.ts b/packages/matrix-protection-suite/src/Interface/Task.ts index 08908bd0..67f2ff7e 100644 --- a/packages/matrix-protection-suite/src/Interface/Task.ts +++ b/packages/matrix-protection-suite/src/Interface/Task.ts @@ -5,7 +5,11 @@ import { ResultError } from "@gnuxie/typescript-result"; import { Logger } from "../Logging/Logger"; import { ActionError } from "./Action"; -import { ActionException, ActionExceptionKind } from "./ActionException"; +import { + ActionException, + ActionExceptionKind, + assertThrowableIsError, +} from "./ActionException"; const log = new Logger("Task"); @@ -67,7 +71,7 @@ export async function Task( } catch (exception) { const actionException = new ActionException( ActionExceptionKind.Unknown, - exception, + assertThrowableIsError(exception), "A Task failed with an unknown exception" ); globalTaskReporter(actionException, options); diff --git a/packages/matrix-protection-suite/src/Interface/Value.ts b/packages/matrix-protection-suite/src/Interface/Value.ts index f56ad385..2c323025 100644 --- a/packages/matrix-protection-suite/src/Interface/Value.ts +++ b/packages/matrix-protection-suite/src/Interface/Value.ts @@ -16,14 +16,18 @@ import { ValueErrorIterator, } from "@sinclair/typebox/compiler"; import { ActionResult, Ok, ResultError } from "./Action"; -import { ActionException, ActionExceptionKind } from "./ActionException"; +import { + ActionException, + ActionExceptionKind, + assertThrowableIsError, +} from "./ActionException"; import { Logger } from "../Logging/Logger"; export class DecodeException extends ActionException { private static log = new Logger("DecodeException"); constructor( message: string, - exception: unknown, + exception: Error, public readonly errors: ValueError[], suppressLog?: boolean ) { @@ -108,7 +112,7 @@ export class Value { throw e; } else { return ActionException.Result(`Unable to decode schema from value`, { - exception: e, + exception: assertThrowableIsError(e), exceptionKind: ActionExceptionKind.Unknown, }); }