From a92c95a0dd89718a61ef952dc0522ba79a903c19 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 18 Sep 2024 18:08:36 +0100 Subject: [PATCH] Make src/index.ts use the BotModeToggle to manage the bot. Now we need to change the integration test's makeMjolnir do the same. --- src/DraupnirBotMode.ts | 104 ++++++++++++++++++++++++++++--- src/index.ts | 25 +++----- src/safemode/DraupnirSafeMode.ts | 2 +- 3 files changed, 107 insertions(+), 24 deletions(-) diff --git a/src/DraupnirBotMode.ts b/src/DraupnirBotMode.ts index 3bfbaa54..82509fda 100644 --- a/src/DraupnirBotMode.ts +++ b/src/DraupnirBotMode.ts @@ -15,6 +15,10 @@ import { RoomStateBackingStore, ClientsInRoomMap, Task, + Logger, + Ok, + ActionException, + ActionExceptionKind, } from "matrix-protection-suite"; import { BotSDKLogServiceLogger, @@ -43,17 +47,34 @@ import { } from "./safemode/SafeModeToggle"; import { SafeModeDraupnir } from "./safemode/DraupnirSafeMode"; import { ResultError } from "@gnuxie/typescript-result"; -import { SafeModeCause } from "./safemode/SafeModeCause"; +import { SafeModeCause, SafeModeReason } from "./safemode/SafeModeCause"; setGlobalLoggerProvider(new BotSDKLogServiceLogger()); +const log = new Logger("DraupnirBotMode"); + export function constructWebAPIs(draupnir: Draupnir): WebAPIs { return new WebAPIs(draupnir.reportManager, draupnir.config); } -export class DraupnirBotModeToggle implements SafeModeToggle { +/** + * The bot mode toggle allows the entrypoint, either src/index.ts or + * manual test scripts, to setup and control Draupnir. + * This includes the webAPIS that accompany Draupnir in bot mode. + * + * The appservice also implements `SafeModeToggle` but has different requirements. + * This interface is exlusively used by the entrypoints to draupnir's bot mode. + */ +interface BotModeTogle extends SafeModeToggle { + encryptionInitialized(): Promise; + stopEverything(): void; + startFromScratch(options?: SafeModeToggleOptions): Promise>; +} + +export class DraupnirBotModeToggle implements BotModeTogle { private draupnir: Draupnir | null = null; private safeModeDraupnir: SafeModeDraupnir | null = null; + private webAPIs: WebAPIs | null = null; private constructor( private readonly clientUserID: StringUserID, @@ -161,13 +182,27 @@ export class DraupnirBotModeToggle implements SafeModeToggle { if (isError(draupnirResult)) { return draupnirResult; } - this.safeModeDraupnir?.stop(); - this.safeModeDraupnir = null; this.draupnir = draupnirResult.ok; void Task(this.draupnir.start()); if (options?.sendStatusOnStart) { void Task(this.draupnir.startupComplete()); + try { + this.webAPIs = constructWebAPIs(this.draupnir); + await this.webAPIs.start(); + } catch (e) { + if (e instanceof Error) { + this.stopDraupnir(); + log.error("Failed to start webAPIs", e); + return ActionException.Result("Failed to start webAPIs", { + exceptionKind: ActionExceptionKind.Unknown, + exception: e, + }); + } else { + throw new TypeError("Someone is throwing garbage."); + } + } } + this.stopSafeModeDraupnir(); return draupnirResult; } public async switchToSafeMode( @@ -189,13 +224,68 @@ export class DraupnirBotModeToggle implements SafeModeToggle { if (isError(safeModeResult)) { return safeModeResult; } - this.draupnir?.stop(); - this.draupnir = null; + this.stopDraupnir(); this.safeModeDraupnir = safeModeResult.ok; this.safeModeDraupnir.start(); if (options?.sendStatusOnStart) { - this.safeModeDraupnir.sendStartupComplete(); + this.safeModeDraupnir.startupComplete(); } return safeModeResult; } + + public async startFromScratch( + options?: SafeModeToggleOptions + ): Promise> { + const draupnirResult = await this.switchToDraupnir(options ?? {}); + if (isError(draupnirResult)) { + if (this.config.safeMode?.bootOnStartupFailure) { + log.error( + "Failed to start draupnir, switching to safe mode as configured", + draupnirResult.error + ); + return (await this.switchToSafeMode( + { + reason: SafeModeReason.InitializationError, + error: draupnirResult.error, + }, + options ?? {} + )) as Result; + } else { + return draupnirResult; + } + } + return Ok(undefined); + } + + public async encryptionInitialized(): Promise { + if (this.draupnir !== null) { + try { + this.webAPIs = constructWebAPIs(this.draupnir); + await this.webAPIs.start(); + await this.draupnir.startupComplete(); + } catch (e) { + this.stopEverything(); + throw e; + } + } else if (this.safeModeDraupnir !== null) { + this.safeModeDraupnir.startupComplete(); + } + } + + private stopDraupnir(): void { + this.draupnir?.stop(); + this.draupnir = null; + this.webAPIs?.stop(); + this.webAPIs = null; + } + + private stopSafeModeDraupnir(): void { + this.safeModeDraupnir?.stop(); + this.safeModeDraupnir = null; + } + + public stopEverything(): void { + this.stopDraupnir(); + this.stopSafeModeDraupnir(); + } } diff --git a/src/index.ts b/src/index.ts index bd06055f..b006bf26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,9 +9,7 @@ // import * as path from "path"; - import { Healthz } from "./health/healthz"; - import { LogLevel, LogService, @@ -24,11 +22,9 @@ import { import { StoreType } from "@matrix-org/matrix-sdk-crypto-nodejs"; import { read as configRead } from "./config"; import { initializeSentry, patchMatrixClient } from "./utils"; -import { DraupnirBotModeToggle, constructWebAPIs } from "./DraupnirBotMode"; -import { Draupnir } from "./Draupnir"; +import { DraupnirBotModeToggle } from "./DraupnirBotMode"; import { SafeMatrixEmitterWrapper } from "matrix-protection-suite-for-matrix-bot-sdk"; -import { DefaultEventDecoder, Task } from "matrix-protection-suite"; -import { WebAPIs } from "./webapis/WebAPIs"; +import { DefaultEventDecoder } from "matrix-protection-suite"; import { SqliteRoomStateBackingStore } from "./backingstore/better-sqlite3/SqliteRoomStateBackingStore"; void (async function () { @@ -51,8 +47,7 @@ void (async function () { healthz.listen(); } - let bot: Draupnir | null = null; - let apis: WebAPIs | null = null; + let bot: DraupnirBotModeToggle | null = null; try { const storagePath = path.isAbsolute(config.dataPath) ? config.dataPath @@ -101,7 +96,7 @@ void (async function () { eventDecoder ) : undefined; - const toggle = await DraupnirBotModeToggle.create( + bot = await DraupnirBotModeToggle.create( client, new SafeMatrixEmitterWrapper(client, eventDecoder), config, @@ -109,25 +104,23 @@ void (async function () { ); // We don't want to send the status on start, as we need to initialize e2ee first (using client.start); - bot = (await toggle.switchToDraupnir({ sendStatusOnStart: false })).expect( - "Failed to initialize Draupnir" + (await bot.startFromScratch({ sendStatusOnStart: false })).expect( + "Failed to start Draupnir" ); - apis = constructWebAPIs(bot); } catch (err) { console.error( `Failed to setup mjolnir from the config ${config.dataPath}: ${err}` ); + bot?.stopEverything(); throw err; } try { await config.RUNTIME.client.start(); - void Task(bot.startupComplete()); - await apis.start(); + await bot.encryptionInitialized(); healthz.isHealthy = true; } catch (err) { console.error(`Mjolnir failed to start: ${err}`); - bot.stop(); - apis.stop(); + bot.stopEverything(); throw err; } })(); diff --git a/src/safemode/DraupnirSafeMode.ts b/src/safemode/DraupnirSafeMode.ts index e2aeacc3..994e1969 100644 --- a/src/safemode/DraupnirSafeMode.ts +++ b/src/safemode/DraupnirSafeMode.ts @@ -101,7 +101,7 @@ export class SafeModeDraupnir implements MatrixAdaptorContext { this.clientRooms.off("timeline", this.timelineEventListener); } - public sendStartupComplete(): void { + public startupComplete(): void { void Task( sendMatrixEventsFromDeadDocument( this.clientPlatform.toRoomMessageSender(),