From be1c2903fb233c2e4f0550b29be32a1b54ef9f0d Mon Sep 17 00:00:00 2001 From: gnuxie Date: Mon, 16 Sep 2024 11:16:36 +0100 Subject: [PATCH 01/18] Simplify Draupnir manager by always listening. --- src/appservice/AppServiceDraupnirManager.ts | 1 - .../StandardDraupnirManager.ts | 53 ++++++------------- 2 files changed, 16 insertions(+), 38 deletions(-) diff --git a/src/appservice/AppServiceDraupnirManager.ts b/src/appservice/AppServiceDraupnirManager.ts index bcd3d0f0..c1adbc9c 100644 --- a/src/appservice/AppServiceDraupnirManager.ts +++ b/src/appservice/AppServiceDraupnirManager.ts @@ -134,7 +134,6 @@ export class AppServiceDraupnirManager { if (isError(managedDraupnir)) { return managedDraupnir; } - this.baseManager.startDraupnir(mxid); incrementGaugeValue(this.instanceCountGauge, "offline", localPart); decrementGaugeValue(this.instanceCountGauge, "disabled", localPart); incrementGaugeValue(this.instanceCountGauge, "online", localPart); diff --git a/src/draupnirfactory/StandardDraupnirManager.ts b/src/draupnirfactory/StandardDraupnirManager.ts index 2c76f99f..b7fa2f05 100644 --- a/src/draupnirfactory/StandardDraupnirManager.ts +++ b/src/draupnirfactory/StandardDraupnirManager.ts @@ -23,9 +23,8 @@ import { } from "@the-draupnir-project/matrix-basic-types"; export class StandardDraupnirManager { - private readonly readyDraupnirs = new Map(); - private readonly listeningDraupnirs = new Map(); - private readonly failedDraupnirs = new Map(); + private readonly draupnir = new Map(); + private readonly failedDraupnir = new Map(); public constructor(protected readonly draupnirFactory: DraupnirFactory) { // nothing to do. @@ -41,11 +40,7 @@ export class StandardDraupnirManager { managementRoom, config ); - if (this.isDraupnirReady(clientUserID)) { - return ActionError.Result( - `There is a draupnir for ${clientUserID} already waiting to be started` - ); - } else if (this.isDraupnirListening(clientUserID)) { + if (this.isDraupnirListening(clientUserID)) { return ActionError.Result( `There is a draupnir for ${clientUserID} already running` ); @@ -58,21 +53,20 @@ export class StandardDraupnirManager { ); return draupnir; } - this.readyDraupnirs.set(clientUserID, draupnir.ok); - this.failedDraupnirs.delete(clientUserID); + // FIXME: This is a little more than suspect that there are no handlers if starting fails? + // unclear to me what can fail though. + void Task(draupnir.ok.start()); + this.draupnir.set(clientUserID, draupnir.ok); + this.failedDraupnir.delete(clientUserID); return draupnir; } - public isDraupnirReady(draupnirClientID: StringUserID): boolean { - return this.readyDraupnirs.has(draupnirClientID); - } - public isDraupnirListening(draupnirClientID: StringUserID): boolean { - return this.listeningDraupnirs.has(draupnirClientID); + return this.draupnir.has(draupnirClientID); } public isDraupnirFailed(draupnirClientID: StringUserID): boolean { - return this.failedDraupnirs.has(draupnirClientID); + return this.failedDraupnir.has(draupnirClientID); } public reportUnstartedDraupnir( @@ -80,50 +74,35 @@ export class StandardDraupnirManager { cause: unknown, draupnirClientID: StringUserID ): void { - this.failedDraupnirs.set( + this.failedDraupnir.set( draupnirClientID, new UnstartedDraupnir(draupnirClientID, failType, cause) ); } public getUnstartedDraupnirs(): UnstartedDraupnir[] { - return [...this.failedDraupnirs.values()]; + return [...this.failedDraupnir.values()]; } public findUnstartedDraupnir( draupnirClientID: StringUserID ): UnstartedDraupnir | undefined { - return this.failedDraupnirs.get(draupnirClientID); + return this.failedDraupnir.get(draupnirClientID); } public findRunningDraupnir( draupnirClientID: StringUserID ): Draupnir | undefined { - return this.listeningDraupnirs.get(draupnirClientID); - } - - public startDraupnir(clientUserID: StringUserID): void { - const draupnir = this.readyDraupnirs.get(clientUserID); - if (draupnir === undefined) { - throw new TypeError( - `Trying to start a draupnir that hasn't been created ${clientUserID}` - ); - } - // FIXME: This is a little more than suspect that there are no handlers if starting fails? - // unclear to me what can fail though. - void Task(draupnir.start()); - this.listeningDraupnirs.set(clientUserID, draupnir); - this.readyDraupnirs.delete(clientUserID); + return this.draupnir.get(draupnirClientID); } public stopDraupnir(clientUserID: StringUserID): void { - const draupnir = this.listeningDraupnirs.get(clientUserID); + const draupnir = this.draupnir.get(clientUserID); if (draupnir === undefined) { return; } else { draupnir.stop(); - this.listeningDraupnirs.delete(clientUserID); - this.readyDraupnirs.set(clientUserID, draupnir); + this.draupnir.delete(clientUserID); } } } From 47468e115571536d7c4e763f0d93e4d681edc382 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Tue, 17 Sep 2024 15:27:45 +0100 Subject: [PATCH 02/18] SafeModeDraupnir (not plugged in). --- src/appservice/AppServiceDraupnirManager.ts | 2 +- src/draupnirfactory/DraupnirFactory.ts | 34 +++++++++ .../StandardDraupnirManager.ts | 45 ++++++++++- src/safemode/DraupnirSafeMode.ts | 69 +++++++++++++++++ src/safemode/SafeModeAdaptor.ts | 74 +++++++++++++++++++ src/safemode/SafeModeCause.ts | 14 ++++ .../commands/RestartDraupnirCommand.ts | 3 + src/safemode/commands/SafeModeCommands.ts | 34 +++++++++ 8 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 src/safemode/DraupnirSafeMode.ts create mode 100644 src/safemode/SafeModeAdaptor.ts create mode 100644 src/safemode/SafeModeCause.ts create mode 100644 src/safemode/commands/RestartDraupnirCommand.ts create mode 100644 src/safemode/commands/SafeModeCommands.ts diff --git a/src/appservice/AppServiceDraupnirManager.ts b/src/appservice/AppServiceDraupnirManager.ts index c1adbc9c..ea9b4fc7 100644 --- a/src/appservice/AppServiceDraupnirManager.ts +++ b/src/appservice/AppServiceDraupnirManager.ts @@ -287,7 +287,7 @@ export class AppServiceDraupnirManager { mjolnirRecord: MjolnirRecord ): Promise> { const clientUserID = this.draupnirMXID(mjolnirRecord); - if (this.baseManager.isDraupnirListening(clientUserID)) { + if (this.baseManager.isDraupnirAvailable(clientUserID)) { throw new TypeError( `${mjolnirRecord.local_part} is already running, we cannot start it.` ); diff --git a/src/draupnirfactory/DraupnirFactory.ts b/src/draupnirfactory/DraupnirFactory.ts index 5567f014..a5d10604 100644 --- a/src/draupnirfactory/DraupnirFactory.ts +++ b/src/draupnirfactory/DraupnirFactory.ts @@ -5,6 +5,7 @@ import { ActionResult, ClientsInRoomMap, + Ok, StandardLoggableConfigTracker, isError, } from "matrix-protection-suite"; @@ -21,6 +22,8 @@ import { StringUserID, MatrixRoomID, } from "@the-draupnir-project/matrix-basic-types"; +import { SafeModeDraupnir } from "../safemode/DraupnirSafeMode"; +import { SafeModeCause } from "../safemode/SafeModeCause"; export class DraupnirFactory { public constructor( @@ -84,4 +87,35 @@ export class DraupnirFactory { configLogTracker ); } + + public async makeSafeModeDraupnir( + clientUserID: StringUserID, + managementRoom: MatrixRoomID, + config: IConfig, + cause: SafeModeCause + ): Promise> { + const client = await this.clientProvider(clientUserID); + const clientRooms = await this.clientsInRoomMap.makeClientRooms( + clientUserID, + async () => joinedRoomsSafe(client) + ); + if (isError(clientRooms)) { + return clientRooms; + } + const clientPlatform = this.clientCapabilityFactory.makeClientPlatform( + clientUserID, + client + ); + return Ok( + new SafeModeDraupnir( + cause, + client, + clientUserID, + clientPlatform, + managementRoom, + clientRooms.ok, + config + ) + ); + } } diff --git a/src/draupnirfactory/StandardDraupnirManager.ts b/src/draupnirfactory/StandardDraupnirManager.ts index b7fa2f05..0cc78a57 100644 --- a/src/draupnirfactory/StandardDraupnirManager.ts +++ b/src/draupnirfactory/StandardDraupnirManager.ts @@ -21,10 +21,13 @@ import { StringUserID, MatrixRoomID, } from "@the-draupnir-project/matrix-basic-types"; +import { SafeModeDraupnir } from "../safemode/DraupnirSafeMode"; +import { SafeModeCause } from "../safemode/SafeModeCause"; export class StandardDraupnirManager { private readonly draupnir = new Map(); private readonly failedDraupnir = new Map(); + private readonly safeModeDraupnir = new Map(); public constructor(protected readonly draupnirFactory: DraupnirFactory) { // nothing to do. @@ -40,7 +43,7 @@ export class StandardDraupnirManager { managementRoom, config ); - if (this.isDraupnirListening(clientUserID)) { + if (this.isDraupnirAvailable(clientUserID)) { return ActionError.Result( `There is a draupnir for ${clientUserID} already running` ); @@ -61,8 +64,44 @@ export class StandardDraupnirManager { return draupnir; } - public isDraupnirListening(draupnirClientID: StringUserID): boolean { - return this.draupnir.has(draupnirClientID); + public async makeSafeModeDraupnir( + clientUserID: StringUserID, + managementRoom: MatrixRoomID, + config: IConfig, + cause: SafeModeCause + ): Promise> { + if (this.isDraupnirAvailable(clientUserID)) { + return ActionError.Result( + `There is a draupnir for ${clientUserID} already running` + ); + } + const safeModeDraupnir = await this.draupnirFactory.makeSafeModeDraupnir( + clientUserID, + managementRoom, + config, + cause + ); + if (isError(safeModeDraupnir)) { + this.reportUnstartedDraupnir( + DraupnirFailType.InitializationError, + safeModeDraupnir.error, + clientUserID + ); + return safeModeDraupnir; + } + safeModeDraupnir.ok.start(); + this.safeModeDraupnir.set(clientUserID, safeModeDraupnir.ok); + return safeModeDraupnir; + } + + /** + * Whether the draupnir is available to the user, either normally or via safe mode. + */ + public isDraupnirAvailable(draupnirClientID: StringUserID): boolean { + return ( + this.draupnir.has(draupnirClientID) || + this.safeModeDraupnir.has(draupnirClientID) + ); } public isDraupnirFailed(draupnirClientID: StringUserID): boolean { diff --git a/src/safemode/DraupnirSafeMode.ts b/src/safemode/DraupnirSafeMode.ts new file mode 100644 index 00000000..ca074db7 --- /dev/null +++ b/src/safemode/DraupnirSafeMode.ts @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { + ClientPlatform, + ClientRooms, + EventReport, + RoomEvent, +} from "matrix-protection-suite"; +import { MatrixAdaptorContext } from "../commands/interface-manager/MPSMatrixInterfaceAdaptor"; +import { + StringUserID, + StringRoomID, + MatrixRoomID, +} from "@the-draupnir-project/matrix-basic-types"; +import { MatrixSendClient } from "matrix-protection-suite-for-matrix-bot-sdk"; +import { MatrixReactionHandler } from "../commands/interface-manager/MatrixReactionHandler"; +import { IConfig } from "../config"; +import { SafeModeCause } from "./SafeModeCause"; + +export class SafeModeDraupnir implements MatrixAdaptorContext { + public reactionHandler: MatrixReactionHandler; + private readonly timelineEventListener = this.handleTimelineEvent.bind(this); + + public constructor( + public readonly cause: SafeModeCause, + public readonly client: MatrixSendClient, + public readonly clientUserID: StringUserID, + public readonly clientPlatform: ClientPlatform, + public readonly managementRoom: MatrixRoomID, + private readonly clientRooms: ClientRooms, + public readonly config: IConfig + //private readonly roomStateManager: RoomStateManager, + //private readonly policyRoomManager: PolicyRoomManager, + //private readonly roomMembershipManager: RoomMembershipManager, + ) { + this.reactionHandler = new MatrixReactionHandler( + managementRoom.toRoomIDOrAlias(), + client, + this.clientUserID, + this.clientPlatform + ); + } + + handleTimelineEvent(_roomID: StringRoomID, _event: RoomEvent): void { + throw new Error("Method not implemented."); + } + handleEventReport(_report: EventReport): void { + throw new Error("Method not implemented."); + } + + public get commandRoomID(): StringRoomID { + return this.managementRoom.toRoomIDOrAlias(); + } + + /** + * Start responding to events. + * This will not start the appservice from listening and responding + * to events. Nor will it start any syncing client. + */ + public start(): void { + this.clientRooms.on("timeline", this.timelineEventListener); + } + + public stop(): void { + this.clientRooms.off("timeline", this.timelineEventListener); + } +} diff --git a/src/safemode/SafeModeAdaptor.ts b/src/safemode/SafeModeAdaptor.ts new file mode 100644 index 00000000..6e97a4af --- /dev/null +++ b/src/safemode/SafeModeAdaptor.ts @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { + CommandPrefixExtractor, + MatrixInterfaceCommandDispatcher, + StandardAdaptorContextToCommandContextTranslator, + StandardMatrixInterfaceAdaptor, + StandardMatrixInterfaceCommandDispatcher, +} from "@the-draupnir-project/interface-manager"; +import { SafeModeDraupnir } from "./DraupnirSafeMode"; +import { + MPSCommandDispatcherCallbacks, + MPSMatrixInterfaceAdaptorCallbacks, + MatrixEventContext, + invocationInformationFromMatrixEventcontext, +} from "../commands/interface-manager/MPSMatrixInterfaceAdaptor"; +import { userLocalpart } from "@the-draupnir-project/matrix-basic-types"; +import { + SafeModeCommands, + SafeModeHelpCommand, +} from "./commands/SafeModeCommands"; + +export const SafeModeContextToCommandContextTranslator = + new StandardAdaptorContextToCommandContextTranslator(); + +export const SafeModeInterfaceAdaptor = new StandardMatrixInterfaceAdaptor< + SafeModeDraupnir, + MatrixEventContext +>( + SafeModeContextToCommandContextTranslator, + invocationInformationFromMatrixEventcontext, + MPSMatrixInterfaceAdaptorCallbacks, + MPSCommandDispatcherCallbacks +); + +function makePrefixExtractor( + safeModeDraupnir: SafeModeDraupnir +): CommandPrefixExtractor { + const plainPrefixes = [ + "!draupnir", + userLocalpart(safeModeDraupnir.clientUserID), + safeModeDraupnir.clientUserID, + ]; + const allPossiblePrefixes = [ + ...plainPrefixes.map((p) => `!${p}`), + ...plainPrefixes.map((p) => `${p}:`), + ...plainPrefixes, + ...(safeModeDraupnir.config.commands.allowNoPrefix ? ["!"] : []), + ]; + return (body) => { + const isPrefixUsed = allPossiblePrefixes.find((p) => + body.toLowerCase().startsWith(p.toLowerCase()) + ); + return isPrefixUsed ? "draupnir" : undefined; + }; +} + +export function makeSafeModeCommandDispatcher( + safeModeDraupnir: SafeModeDraupnir +): MatrixInterfaceCommandDispatcher { + return new StandardMatrixInterfaceCommandDispatcher( + SafeModeInterfaceAdaptor, + safeModeDraupnir, + SafeModeCommands, + SafeModeHelpCommand, + invocationInformationFromMatrixEventcontext, + { + ...MPSCommandDispatcherCallbacks, + prefixExtractor: makePrefixExtractor(safeModeDraupnir), + } + ); +} diff --git a/src/safemode/SafeModeCause.ts b/src/safemode/SafeModeCause.ts new file mode 100644 index 00000000..4fb048d8 --- /dev/null +++ b/src/safemode/SafeModeCause.ts @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { ResultError } from "@gnuxie/typescript-result"; + +export enum SafeModeReason { + InitializationError = "InitializationError", + ByRequest = "ByRequest", +} + +export type SafeModeCause = + | { reason: SafeModeReason.ByRequest } + | { reason: SafeModeReason.InitializationError; error: ResultError }; diff --git a/src/safemode/commands/RestartDraupnirCommand.ts b/src/safemode/commands/RestartDraupnirCommand.ts new file mode 100644 index 00000000..78191cc2 --- /dev/null +++ b/src/safemode/commands/RestartDraupnirCommand.ts @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 diff --git a/src/safemode/commands/SafeModeCommands.ts b/src/safemode/commands/SafeModeCommands.ts new file mode 100644 index 00000000..bd377a6d --- /dev/null +++ b/src/safemode/commands/SafeModeCommands.ts @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { Result } from "@gnuxie/typescript-result"; +import { + CommandTable, + StandardCommandTable, + TopPresentationSchema, + describeCommand, +} from "@the-draupnir-project/interface-manager"; +import { Ok } from "matrix-protection-suite"; +import { SafeModeInterfaceAdaptor } from "../SafeModeAdaptor"; + +export const SafeModeCommands = new StandardCommandTable("safe mode"); + +export const SafeModeHelpCommand = describeCommand({ + rest: { + name: "command parts", + acceptor: TopPresentationSchema, + }, + summary: "Display this message", + executor: async function ( + _context, + _keywords + ): Promise> { + return Ok(SafeModeCommands); + }, + parameters: [], +}); + +SafeModeInterfaceAdaptor.describeRenderer(SafeModeHelpCommand, { + isAlwaysSupposedToUseDefaultRenderer: true, +}); From c9a0d60eafced8bf4191908243a233ce942445eb Mon Sep 17 00:00:00 2001 From: gnuxie Date: Tue, 17 Sep 2024 17:27:08 +0100 Subject: [PATCH 03/18] Migrate bot mode to use the safe mode toggle, but only for draupnir. --- src/DraupnirBotMode.ts | 206 +++++++++++++++++--------- src/config.ts | 6 + src/index.ts | 11 +- src/safemode/SafeModeToggle.ts | 20 +++ test/integration/mjolnirSetupUtils.ts | 7 +- 5 files changed, 174 insertions(+), 76 deletions(-) create mode 100644 src/safemode/SafeModeToggle.ts diff --git a/src/DraupnirBotMode.ts b/src/DraupnirBotMode.ts index 7d9b6b76..dbf23a74 100644 --- a/src/DraupnirBotMode.ts +++ b/src/DraupnirBotMode.ts @@ -13,6 +13,7 @@ import { DefaultEventDecoder, setGlobalLoggerProvider, RoomStateBackingStore, + ClientsInRoomMap, } from "matrix-protection-suite"; import { BotSDKLogServiceLogger, @@ -20,7 +21,7 @@ import { MatrixSendClient, RoomStateManagerFactory, SafeMatrixEmitter, - resolveRoomReferenceSafe, + joinedRoomsSafe, } from "matrix-protection-suite-for-matrix-bot-sdk"; import { IConfig } from "./config"; import { Draupnir } from "./Draupnir"; @@ -32,7 +33,13 @@ import { isStringRoomID, MatrixRoomReference, StringUserID, + MatrixRoomID, } from "@the-draupnir-project/matrix-basic-types"; +import { Result, isError } from "@gnuxie/typescript-result"; +import { SafeModeToggle } from "./safemode/SafeModeToggle"; +import { SafeModeDraupnir } from "./safemode/DraupnirSafeMode"; +import { ResultError } from "@gnuxie/typescript-result"; +import { SafeModeCause } from "./safemode/SafeModeCause"; setGlobalLoggerProvider(new BotSDKLogServiceLogger()); @@ -40,77 +47,138 @@ export function constructWebAPIs(draupnir: Draupnir): WebAPIs { return new WebAPIs(draupnir.reportManager, draupnir.config); } -/** - * This is a file for providing default concrete implementations - * for all things to bootstrap Draupnir in 'bot mode'. - * However, people should be encouraged to make their own when - * APIs are stable as the protection-suite makes Draupnir - * almost completely modular and customizable. - */ +export class DraupnirBotModeToggle implements SafeModeToggle { + private draupnir: Draupnir | null = null; + private safeModeDraupnir: SafeModeDraupnir | null = null; -export async function makeDraupnirBotModeFromConfig( - client: MatrixSendClient, - matrixEmitter: SafeMatrixEmitter, - config: IConfig, - backingStore?: RoomStateBackingStore -): Promise { - const clientUserId = await client.getUserId(); - if (!isStringUserID(clientUserId)) { - throw new TypeError(`${clientUserId} is not a valid mxid`); - } - if ( - !isStringRoomAlias(config.managementRoom) && - !isStringRoomID(config.managementRoom) + private constructor( + private readonly clientUserID: StringUserID, + private readonly managementRoom: MatrixRoomID, + private readonly clientsInRoomMap: ClientsInRoomMap, + private readonly roomStateManagerFactory: RoomStateManagerFactory, + private readonly draupnirFactory: DraupnirFactory, + private readonly matrixEmitter: SafeMatrixEmitter, + private readonly config: IConfig ) { - throw new TypeError( - `${config.managementRoom} is not a valid room id or alias` + this.matrixEmitter.on("room.invite", (roomID, event) => { + this.clientsInRoomMap.handleTimelineEvent(roomID, event); + }); + this.matrixEmitter.on("room.event", (roomID, event) => { + this.roomStateManagerFactory.handleTimelineEvent(roomID, event); + this.clientsInRoomMap.handleTimelineEvent(roomID, event); + }); + } + public static async create( + client: MatrixSendClient, + matrixEmitter: SafeMatrixEmitter, + config: IConfig, + backingStore?: RoomStateBackingStore + ): Promise { + const clientUserID = await client.getUserId(); + if (!isStringUserID(clientUserID)) { + throw new TypeError(`${clientUserID} is not a valid mxid`); + } + if ( + !isStringRoomAlias(config.managementRoom) && + !isStringRoomID(config.managementRoom) + ) { + throw new TypeError( + `${config.managementRoom} is not a valid room id or alias` + ); + } + const configManagementRoomReference = MatrixRoomReference.fromRoomIDOrAlias( + config.managementRoom + ); + const clientsInRoomMap = new StandardClientsInRoomMap(); + const clientCapabilityFactory = new ClientCapabilityFactory( + clientsInRoomMap, + DefaultEventDecoder + ); + // needed to have accurate join infomration. + ( + await clientsInRoomMap.makeClientRooms(clientUserID, async () => + joinedRoomsSafe(client) + ) + ).expect("Unable to create ClientRooms"); + const clientPlatform = clientCapabilityFactory.makeClientPlatform( + clientUserID, + client + ); + const managementRoom = ( + await clientPlatform + .toRoomResolver() + .resolveRoom(configManagementRoomReference) + ).expect("Unable to resolve Draupnir's management room"); + (await clientPlatform.toRoomJoiner().joinRoom(managementRoom)).expect( + "Unable to join Draupnir's management room" + ); + const clientProvider = async (userID: StringUserID) => { + if (userID !== clientUserID) { + throw new TypeError(`Bot mode shouldn't be requesting any other mxids`); + } + return client; + }; + const roomStateManagerFactory = new RoomStateManagerFactory( + clientsInRoomMap, + clientProvider, + DefaultEventDecoder, + backingStore + ); + const draupnirFactory = new DraupnirFactory( + clientsInRoomMap, + clientCapabilityFactory, + clientProvider, + roomStateManagerFactory + ); + return new DraupnirBotModeToggle( + clientUserID, + managementRoom, + clientsInRoomMap, + roomStateManagerFactory, + draupnirFactory, + matrixEmitter, + config ); } - const configManagementRoomReference = MatrixRoomReference.fromRoomIDOrAlias( - config.managementRoom - ); - const managementRoom = ( - await resolveRoomReferenceSafe(client, configManagementRoomReference) - ).expect("Unable to resolve Draupnir's management room"); - - await client.joinRoom( - managementRoom.toRoomIDOrAlias(), - managementRoom.getViaServers() - ); - const clientsInRoomMap = new StandardClientsInRoomMap(); - const clientProvider = async (userID: StringUserID) => { - if (userID !== clientUserId) { - throw new TypeError(`Bot mode shouldn't be requesting any other mxids`); + public async switchToDraupnir(): Promise> { + if (this.draupnir !== null) { + return ResultError.Result( + `There is a draupnir for ${this.clientUserID} already running` + ); } - return client; - }; - const roomStateManagerFactory = new RoomStateManagerFactory( - clientsInRoomMap, - clientProvider, - DefaultEventDecoder, - backingStore - ); - const clientCapabilityFactory = new ClientCapabilityFactory( - clientsInRoomMap, - DefaultEventDecoder - ); - const draupnirFactory = new DraupnirFactory( - clientsInRoomMap, - clientCapabilityFactory, - clientProvider, - roomStateManagerFactory - ); - const draupnir = await draupnirFactory.makeDraupnir( - clientUserId, - managementRoom, - config - ); - matrixEmitter.on("room.invite", (roomID, event) => { - clientsInRoomMap.handleTimelineEvent(roomID, event); - }); - matrixEmitter.on("room.event", (roomID, event) => { - roomStateManagerFactory.handleTimelineEvent(roomID, event); - clientsInRoomMap.handleTimelineEvent(roomID, event); - }); - return draupnir.expect("Unable to create Draupnir"); + const draupnirResult = await this.draupnirFactory.makeDraupnir( + this.clientUserID, + this.managementRoom, + this.config + ); + if (isError(draupnirResult)) { + return draupnirResult; + } + this.safeModeDraupnir?.stop(); + this.safeModeDraupnir = null; + this.draupnir = draupnirResult.ok; + return draupnirResult; + } + public async switchToSafeMode( + cause: SafeModeCause + ): Promise> { + if (this.safeModeDraupnir !== null) { + return ResultError.Result( + `There is a safe mode draupnir for ${this.clientUserID} already running` + ); + } + const safeModeResult = await this.draupnirFactory.makeSafeModeDraupnir( + this.clientUserID, + this.managementRoom, + this.config, + cause + ); + if (isError(safeModeResult)) { + return safeModeResult; + } + this.draupnir?.stop(); + this.draupnir = null; + this.safeModeDraupnir = safeModeResult.ok; + return safeModeResult; + } } diff --git a/src/config.ts b/src/config.ts index 8888939c..6893df2d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -89,6 +89,9 @@ export interface IConfig { redactReason: string; }; }; + safeMode?: { + bootOnStartupFailure: boolean; + }; health: { healthz: { enabled: boolean; @@ -188,6 +191,9 @@ const defaultConfig: IConfig = { "You have mentioned too many users in this message, so we have had to redact it.", }, }, + safeMode: { + bootOnStartupFailure: false, + }, health: { healthz: { enabled: false, diff --git a/src/index.ts b/src/index.ts index b0fe4609..afe66779 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,10 +24,7 @@ import { import { StoreType } from "@matrix-org/matrix-sdk-crypto-nodejs"; import { read as configRead } from "./config"; import { initializeSentry, patchMatrixClient } from "./utils"; -import { - constructWebAPIs, - makeDraupnirBotModeFromConfig, -} from "./DraupnirBotMode"; +import { DraupnirBotModeToggle, constructWebAPIs } from "./DraupnirBotMode"; import { Draupnir } from "./Draupnir"; import { SafeMatrixEmitterWrapper } from "matrix-protection-suite-for-matrix-bot-sdk"; import { DefaultEventDecoder, Task } from "matrix-protection-suite"; @@ -104,12 +101,16 @@ void (async function () { eventDecoder ) : undefined; - bot = await makeDraupnirBotModeFromConfig( + const toggle = await DraupnirBotModeToggle.create( client, new SafeMatrixEmitterWrapper(client, eventDecoder), config, store ); + + bot = (await toggle.switchToDraupnir()).expect( + "Failed to initialize Draupnir" + ); apis = constructWebAPIs(bot); } catch (err) { console.error( diff --git a/src/safemode/SafeModeToggle.ts b/src/safemode/SafeModeToggle.ts new file mode 100644 index 00000000..3884853f --- /dev/null +++ b/src/safemode/SafeModeToggle.ts @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { Result } from "@gnuxie/typescript-result"; +import { Draupnir } from "../Draupnir"; +import { SafeModeDraupnir } from "./DraupnirSafeMode"; +import { SafeModeCause } from "./SafeModeCause"; + +export interface SafeModeToggle { + /** + * Switch the bot to Draupnir mode. + * We expect that the result represents the entire conversion. + * We expect that the same matrix client is shared between the bots. + * That means that by the command responds with ticks and crosses, + * draupnir will be running or we will still be in safe mode. + */ + switchToDraupnir(): Promise>; + switchToSafeMode(cause: SafeModeCause): Promise>; +} diff --git a/test/integration/mjolnirSetupUtils.ts b/test/integration/mjolnirSetupUtils.ts index c08c07c8..170cab4b 100644 --- a/test/integration/mjolnirSetupUtils.ts +++ b/test/integration/mjolnirSetupUtils.ts @@ -20,7 +20,7 @@ import { overrideRatelimitForUser, registerUser } from "./clientHelper"; import { initializeSentry, patchMatrixClient } from "../../src/utils"; import { IConfig } from "../../src/config"; import { Draupnir } from "../../src/Draupnir"; -import { makeDraupnirBotModeFromConfig } from "../../src/DraupnirBotMode"; +import { DraupnirBotModeToggle } from "../../src/DraupnirBotMode"; import { SafeMatrixEmitter, SafeMatrixEmitterWrapper, @@ -140,12 +140,15 @@ export async function makeMjolnir( await client.getUserId() ); await ensureAliasedRoomExists(client, config.managementRoom); - const mj = await makeDraupnirBotModeFromConfig( + const toggle = await DraupnirBotModeToggle.create( client, new SafeMatrixEmitterWrapper(client, DefaultEventDecoder), config, backingStore ); + const mj = (await toggle.switchToDraupnir()).expect( + "Could not create Draupnir" + ); globalClient = client; globalMjolnir = mj; globalSafeEmitter = new SafeMatrixEmitterWrapper(client, DefaultEventDecoder); From 9959431da87cb3715f3daaacfef456ea145438f4 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Tue, 17 Sep 2024 18:18:13 +0100 Subject: [PATCH 04/18] Pass the `SafeModeToggle` to `Draupnir`. Something that is going to be broken for sure are the guages on the appservice manager, since toggling is controlled by the base manager. I have no idea what these guages are even for except the legacy draupnir4all work. --- src/Draupnir.ts | 6 +++- src/DraupnirBotMode.ts | 3 +- src/draupnirfactory/DraupnirFactory.ts | 7 ++-- .../StandardDraupnirManager.ts | 32 ++++++++++++++++++- 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/Draupnir.ts b/src/Draupnir.ts index 22ff4b3f..c3089f41 100644 --- a/src/Draupnir.ts +++ b/src/Draupnir.ts @@ -67,6 +67,7 @@ import { sendMatrixEventsFromDeadDocument, } from "./commands/interface-manager/MPSMatrixInterfaceAdaptor"; import { makeDraupnirCommandDispatcher } from "./commands/DraupnirCommandDispatcher"; +import { SafeModeToggle } from "./safemode/SafeModeToggle"; const log = new Logger("Draupnir"); @@ -121,6 +122,7 @@ export class Draupnir implements Client, MatrixAdaptorContext { /** Mjolnir has a feature where you can choose to accept invitations from a space and not just the management room. */ public readonly acceptInvitesFromRoom: MatrixRoomID, public readonly acceptInvitesFromRoomIssuer: RoomMembershipRevisionIssuer, + public readonly safeModeToggle: SafeModeToggle, public readonly synapseAdminClient?: SynapseAdminClient ) { this.managementRoomID = this.managementRoom.toRoomIDOrAlias(); @@ -169,7 +171,8 @@ export class Draupnir implements Client, MatrixAdaptorContext { policyRoomManager: PolicyRoomManager, roomMembershipManager: RoomMembershipManager, config: IConfig, - loggableConfigTracker: LoggableConfigTracker + loggableConfigTracker: LoggableConfigTracker, + safeModeToggle: SafeModeToggle ): Promise> { const acceptInvitesFromRoom = await (async () => { if (config.autojoinOnlyIfManager) { @@ -225,6 +228,7 @@ export class Draupnir implements Client, MatrixAdaptorContext { loggableConfigTracker, acceptInvitesFromRoom.ok, acceptInvitesFromRoomIssuer.ok, + safeModeToggle, new SynapseAdminClient(client, clientUserID) ); const loadResult = await protectedRoomsSet.protections.loadProtections( diff --git a/src/DraupnirBotMode.ts b/src/DraupnirBotMode.ts index dbf23a74..ef56ccea 100644 --- a/src/DraupnirBotMode.ts +++ b/src/DraupnirBotMode.ts @@ -149,7 +149,8 @@ export class DraupnirBotModeToggle implements SafeModeToggle { const draupnirResult = await this.draupnirFactory.makeDraupnir( this.clientUserID, this.managementRoom, - this.config + this.config, + this ); if (isError(draupnirResult)) { return draupnirResult; diff --git a/src/draupnirfactory/DraupnirFactory.ts b/src/draupnirfactory/DraupnirFactory.ts index a5d10604..bb3e9ced 100644 --- a/src/draupnirfactory/DraupnirFactory.ts +++ b/src/draupnirfactory/DraupnirFactory.ts @@ -24,6 +24,7 @@ import { } from "@the-draupnir-project/matrix-basic-types"; import { SafeModeDraupnir } from "../safemode/DraupnirSafeMode"; import { SafeModeCause } from "../safemode/SafeModeCause"; +import { SafeModeToggle } from "../safemode/SafeModeToggle"; export class DraupnirFactory { public constructor( @@ -38,7 +39,8 @@ export class DraupnirFactory { public async makeDraupnir( clientUserID: StringUserID, managementRoom: MatrixRoomID, - config: IConfig + config: IConfig, + toggle: SafeModeToggle ): Promise> { const client = await this.clientProvider(clientUserID); const clientRooms = await this.clientsInRoomMap.makeClientRooms( @@ -84,7 +86,8 @@ export class DraupnirFactory { policyRoomManager, roomMembershipManager, config, - configLogTracker + configLogTracker, + toggle ); } diff --git a/src/draupnirfactory/StandardDraupnirManager.ts b/src/draupnirfactory/StandardDraupnirManager.ts index 0cc78a57..2925835c 100644 --- a/src/draupnirfactory/StandardDraupnirManager.ts +++ b/src/draupnirfactory/StandardDraupnirManager.ts @@ -23,6 +23,7 @@ import { } from "@the-draupnir-project/matrix-basic-types"; import { SafeModeDraupnir } from "../safemode/DraupnirSafeMode"; import { SafeModeCause } from "../safemode/SafeModeCause"; +import { SafeModeToggle } from "../safemode/SafeModeToggle"; export class StandardDraupnirManager { private readonly draupnir = new Map(); @@ -33,6 +34,34 @@ export class StandardDraupnirManager { // nothing to do. } + public makeSafeModeToggle( + clientUserID: StringUserID, + managementRoom: MatrixRoomID, + config: IConfig + ): SafeModeToggle { + // We need to alias to make the toggle frankly. + // eslint-disable-next-line @typescript-eslint/no-this-alias + const draupnirManager = this; + const toggle: SafeModeToggle = Object.freeze({ + async switchToSafeMode(cause: SafeModeCause) { + return draupnirManager.makeSafeModeDraupnir( + clientUserID, + managementRoom, + config, + cause + ); + }, + async switchToDraupnir() { + return draupnirManager.makeDraupnir( + clientUserID, + managementRoom, + config + ); + }, + }); + return toggle; + } + public async makeDraupnir( clientUserID: StringUserID, managementRoom: MatrixRoomID, @@ -41,7 +70,8 @@ export class StandardDraupnirManager { const draupnir = await this.draupnirFactory.makeDraupnir( clientUserID, managementRoom, - config + config, + this.makeSafeModeToggle(clientUserID, managementRoom, config) ); if (this.isDraupnirAvailable(clientUserID)) { return ActionError.Result( From 6b1b44259c8ea575792ac19f7166aac0ee760ece Mon Sep 17 00:00:00 2001 From: gnuxie Date: Tue, 17 Sep 2024 18:31:02 +0100 Subject: [PATCH 05/18] Add draupnir command to switch to safe mode. Works, but safe mode isn't hooked up to respond to any events. --- src/commands/DraupnirCommands.ts | 2 ++ src/commands/SafeModeCommand.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/commands/SafeModeCommand.ts diff --git a/src/commands/DraupnirCommands.ts b/src/commands/DraupnirCommands.ts index 9ff0f252..997177c5 100644 --- a/src/commands/DraupnirCommands.ts +++ b/src/commands/DraupnirCommands.ts @@ -47,6 +47,7 @@ import { DraupnirWatchPolicyRoomCommand, } from "./WatchUnwatchCommand"; import { DraupnirTopLevelCommands } from "./DraupnirCommandTable"; +import { DraupnirSafeModeCommand } from "./SafeModeCommand"; // TODO: These commands should all be moved to subdirectories tbh and this // should be split like an index file for each subdirectory. @@ -88,6 +89,7 @@ const DraupnirCommands = new StandardCommandTable("draupnir") .internCommand(DraupnirRoomsRemoveCommand, ["rooms", "remove"]) .internCommand(DraupnirListRulesCommand, ["rules"]) .internCommand(DraupnirRulesMatchingCommand, ["rules", "matching"]) + .internCommand(DraupnirSafeModeCommand, ["safe", "mode"]) .internCommand(DraupnirDisplaynameCommand, ["displayname"]) .internCommand(DraupnirSetPowerLevelCommand, ["powerlevel"]) .internCommand(DraupnirStatusCommand, ["status"]) diff --git a/src/commands/SafeModeCommand.ts b/src/commands/SafeModeCommand.ts new file mode 100644 index 00000000..96cf62fc --- /dev/null +++ b/src/commands/SafeModeCommand.ts @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { describeCommand } from "@the-draupnir-project/interface-manager"; +import { SafeModeDraupnir } from "../safemode/DraupnirSafeMode"; +import { Result } from "@gnuxie/typescript-result"; +import { Draupnir } from "../Draupnir"; +import { SafeModeReason } from "../safemode/SafeModeCause"; +import { DraupnirInterfaceAdaptor } from "./DraupnirCommandPrerequisites"; + +export const DraupnirSafeModeCommand = describeCommand({ + summary: "Enter into safe mode.", + parameters: [], + async executor({ + safeModeToggle, + }: Draupnir): Promise> { + return safeModeToggle.switchToSafeMode({ + reason: SafeModeReason.ByRequest, + }); + }, +}); + +DraupnirInterfaceAdaptor.describeRenderer(DraupnirSafeModeCommand, { + isAlwaysSupposedToUseDefaultRenderer: true, +}); From 28718ca89be5accacd47b6f61cee4bda619c8b2c Mon Sep 17 00:00:00 2001 From: gnuxie Date: Tue, 17 Sep 2024 19:13:34 +0100 Subject: [PATCH 06/18] Remember to start the safe mode bot. --- src/DraupnirBotMode.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/DraupnirBotMode.ts b/src/DraupnirBotMode.ts index ef56ccea..3c1e1e64 100644 --- a/src/DraupnirBotMode.ts +++ b/src/DraupnirBotMode.ts @@ -180,6 +180,7 @@ export class DraupnirBotModeToggle implements SafeModeToggle { this.draupnir?.stop(); this.draupnir = null; this.safeModeDraupnir = safeModeResult.ok; + this.safeModeDraupnir.start(); return safeModeResult; } } From ed7b976404637ffb4c22752b32ec129812e4916b Mon Sep 17 00:00:00 2001 From: gnuxie Date: Tue, 17 Sep 2024 19:15:39 +0100 Subject: [PATCH 07/18] Allow safe mode bot to respond to commands. --- src/Draupnir.ts | 50 ++++---------- src/safemode/DraupnirSafeMode.ts | 28 +++++++- src/safemode/ManagementRoom.ts | 77 ++++++++++++++++++++++ src/safemode/SafeModeAdaptor.ts | 17 +---- src/safemode/commands/SafeModeCommands.ts | 34 ---------- src/safemode/commands/SafeModeCommands.tsx | 62 +++++++++++++++++ 6 files changed, 177 insertions(+), 91 deletions(-) create mode 100644 src/safemode/ManagementRoom.ts delete mode 100644 src/safemode/commands/SafeModeCommands.ts create mode 100644 src/safemode/commands/SafeModeCommands.tsx diff --git a/src/Draupnir.ts b/src/Draupnir.ts index c3089f41..bf0172d1 100644 --- a/src/Draupnir.ts +++ b/src/Draupnir.ts @@ -23,10 +23,8 @@ import { RoomEvent, RoomMembershipManager, RoomMembershipRevisionIssuer, - RoomMessage, RoomStateManager, Task, - TextMessageContent, Value, isError, } from "matrix-protection-suite"; @@ -68,6 +66,7 @@ import { } from "./commands/interface-manager/MPSMatrixInterfaceAdaptor"; import { makeDraupnirCommandDispatcher } from "./commands/DraupnirCommandDispatcher"; import { SafeModeToggle } from "./safemode/SafeModeToggle"; +import { makeCommandDispatcherTimelineListener } from "./safemode/ManagementRoom"; const log = new Logger("Draupnir"); @@ -107,6 +106,13 @@ export class Draupnir implements Client, MatrixAdaptorContext { public readonly capabilityMessageRenderer: RendererMessageCollector; + private readonly commandDispatcherTimelineListener = + makeCommandDispatcherTimelineListener( + this.managementRoom, + this.client, + this.commandDispatcher + ); + private constructor( public readonly client: MatrixSendClient, public readonly clientUserID: StringUserID, @@ -307,42 +313,7 @@ export class Draupnir implements Client, MatrixAdaptorContext { if (roomID !== this.managementRoomID) { return; } - if ( - Value.Check(RoomMessage, event) && - Value.Check(TextMessageContent, event.content) - ) { - if ( - event.content.body === - "** Unable to decrypt: The sender's device has not sent us the keys for this message. **" - ) { - log.info( - `Unable to decrypt an event ${event.event_id} from ${event.sender} in the management room ${this.managementRoom.toPermalink()}.` - ); - void Task( - this.client.unstableApis - .addReactionToEvent(roomID, event.event_id, "⚠") - .then((_) => Ok(undefined)) - ); - void Task( - this.client.unstableApis - .addReactionToEvent(roomID, event.event_id, "UISI") - .then((_) => Ok(undefined)) - ); - void Task( - this.client.unstableApis - .addReactionToEvent(roomID, event.event_id, "🚨") - .then((_) => Ok(undefined)) - ); - return; - } - this.commandDispatcher.handleCommandMessageEvent( - { - event, - roomID, - }, - event.content.body - ); - } + this.commandDispatcherTimelineListener(roomID, event); this.reportManager.handleTimelineEvent(roomID, event); } @@ -352,6 +323,9 @@ export class Draupnir implements Client, MatrixAdaptorContext { * to events. Nor will it start any syncing client. */ public async start(): Promise { + // to avoid handlers getting out of sync on clientRooms and leaking + // when draupnir keeps being started and restarted, we can basically + // clear all listeners each time and add the factory listener back. this.clientRooms.on("timeline", this.timelineEventListener); if (this.reportPoller) { const reportPollSetting = await ReportPoller.getReportPollSetting( diff --git a/src/safemode/DraupnirSafeMode.ts b/src/safemode/DraupnirSafeMode.ts index ca074db7..8f39eb43 100644 --- a/src/safemode/DraupnirSafeMode.ts +++ b/src/safemode/DraupnirSafeMode.ts @@ -18,11 +18,25 @@ import { MatrixSendClient } from "matrix-protection-suite-for-matrix-bot-sdk"; import { MatrixReactionHandler } from "../commands/interface-manager/MatrixReactionHandler"; import { IConfig } from "../config"; import { SafeModeCause } from "./SafeModeCause"; +import { makeSafeModeCommandDispatcher } from "./SafeModeAdaptor"; +import { + ARGUMENT_PROMPT_LISTENER, + DEFAUILT_ARGUMENT_PROMPT_LISTENER, + makeListenerForArgumentPrompt, + makeListenerForPromptDefault, +} from "../commands/interface-manager/MatrixPromptForAccept"; +import { makeCommandDispatcherTimelineListener } from "./ManagementRoom"; export class SafeModeDraupnir implements MatrixAdaptorContext { public reactionHandler: MatrixReactionHandler; private readonly timelineEventListener = this.handleTimelineEvent.bind(this); - + private readonly commandDispatcher = makeSafeModeCommandDispatcher(this); + private readonly commandDispatcherTimelineListener = + makeCommandDispatcherTimelineListener( + this.managementRoom, + this.client, + this.commandDispatcher + ); public constructor( public readonly cause: SafeModeCause, public readonly client: MatrixSendClient, @@ -41,10 +55,18 @@ export class SafeModeDraupnir implements MatrixAdaptorContext { this.clientUserID, this.clientPlatform ); + this.reactionHandler.on( + ARGUMENT_PROMPT_LISTENER, + makeListenerForArgumentPrompt(this.commandDispatcher) + ); + this.reactionHandler.on( + DEFAUILT_ARGUMENT_PROMPT_LISTENER, + makeListenerForPromptDefault(this.commandDispatcher) + ); } - handleTimelineEvent(_roomID: StringRoomID, _event: RoomEvent): void { - throw new Error("Method not implemented."); + handleTimelineEvent(roomID: StringRoomID, event: RoomEvent): void { + this.commandDispatcherTimelineListener(roomID, event); } handleEventReport(_report: EventReport): void { throw new Error("Method not implemented."); diff --git a/src/safemode/ManagementRoom.ts b/src/safemode/ManagementRoom.ts new file mode 100644 index 00000000..8e16a2f5 --- /dev/null +++ b/src/safemode/ManagementRoom.ts @@ -0,0 +1,77 @@ +// Copyright 2022 - 2024 Gnuxie +// Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. +// +// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0 +// +// SPDX-FileAttributionText: +// This modified file incorporates work from mjolnir +// https://github.com/matrix-org/mjolnir +// + +import { MatrixInterfaceCommandDispatcher } from "@the-draupnir-project/interface-manager"; +import { + MatrixRoomID, + StringRoomID, +} from "@the-draupnir-project/matrix-basic-types"; +import { + Logger, + Ok, + RoomEvent, + RoomMessage, + Task, + TextMessageContent, + Value, +} from "matrix-protection-suite"; +import { MatrixSendClient } from "matrix-protection-suite-for-matrix-bot-sdk"; +import { MatrixEventContext } from "../commands/interface-manager/MPSMatrixInterfaceAdaptor"; + +const log = new Logger("ManagementRoom"); + +export function makeCommandDispatcherTimelineListener( + managementRoom: MatrixRoomID, + client: MatrixSendClient, + dispatcher: MatrixInterfaceCommandDispatcher +): (roomID: StringRoomID, event: RoomEvent) => void { + const managementRoomID = managementRoom.toRoomIDOrAlias(); + return function (roomID, event): void { + if (roomID !== managementRoomID) { + return; + } + if ( + Value.Check(RoomMessage, event) && + Value.Check(TextMessageContent, event.content) + ) { + if ( + event.content.body === + "** Unable to decrypt: The sender's device has not sent us the keys for this message. **" + ) { + log.info( + `Unable to decrypt an event ${event.event_id} from ${event.sender} in the management room ${managementRoom.toPermalink()}.` + ); + void Task( + client.unstableApis + .addReactionToEvent(roomID, event.event_id, "⚠") + .then((_) => Ok(undefined)) + ); + void Task( + client.unstableApis + .addReactionToEvent(roomID, event.event_id, "UISI") + .then((_) => Ok(undefined)) + ); + void Task( + client.unstableApis + .addReactionToEvent(roomID, event.event_id, "🚨") + .then((_) => Ok(undefined)) + ); + return; + } + dispatcher.handleCommandMessageEvent( + { + event, + roomID, + }, + event.content.body + ); + } + }; +} diff --git a/src/safemode/SafeModeAdaptor.ts b/src/safemode/SafeModeAdaptor.ts index 6e97a4af..1a9c5c52 100644 --- a/src/safemode/SafeModeAdaptor.ts +++ b/src/safemode/SafeModeAdaptor.ts @@ -5,14 +5,11 @@ import { CommandPrefixExtractor, MatrixInterfaceCommandDispatcher, - StandardAdaptorContextToCommandContextTranslator, - StandardMatrixInterfaceAdaptor, StandardMatrixInterfaceCommandDispatcher, } from "@the-draupnir-project/interface-manager"; import { SafeModeDraupnir } from "./DraupnirSafeMode"; import { MPSCommandDispatcherCallbacks, - MPSMatrixInterfaceAdaptorCallbacks, MatrixEventContext, invocationInformationFromMatrixEventcontext, } from "../commands/interface-manager/MPSMatrixInterfaceAdaptor"; @@ -21,19 +18,7 @@ import { SafeModeCommands, SafeModeHelpCommand, } from "./commands/SafeModeCommands"; - -export const SafeModeContextToCommandContextTranslator = - new StandardAdaptorContextToCommandContextTranslator(); - -export const SafeModeInterfaceAdaptor = new StandardMatrixInterfaceAdaptor< - SafeModeDraupnir, - MatrixEventContext ->( - SafeModeContextToCommandContextTranslator, - invocationInformationFromMatrixEventcontext, - MPSMatrixInterfaceAdaptorCallbacks, - MPSCommandDispatcherCallbacks -); +import { SafeModeInterfaceAdaptor } from "./commands/SafeModeCommands"; function makePrefixExtractor( safeModeDraupnir: SafeModeDraupnir diff --git a/src/safemode/commands/SafeModeCommands.ts b/src/safemode/commands/SafeModeCommands.ts deleted file mode 100644 index bd377a6d..00000000 --- a/src/safemode/commands/SafeModeCommands.ts +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Gnuxie -// -// SPDX-License-Identifier: AFL-3.0 - -import { Result } from "@gnuxie/typescript-result"; -import { - CommandTable, - StandardCommandTable, - TopPresentationSchema, - describeCommand, -} from "@the-draupnir-project/interface-manager"; -import { Ok } from "matrix-protection-suite"; -import { SafeModeInterfaceAdaptor } from "../SafeModeAdaptor"; - -export const SafeModeCommands = new StandardCommandTable("safe mode"); - -export const SafeModeHelpCommand = describeCommand({ - rest: { - name: "command parts", - acceptor: TopPresentationSchema, - }, - summary: "Display this message", - executor: async function ( - _context, - _keywords - ): Promise> { - return Ok(SafeModeCommands); - }, - parameters: [], -}); - -SafeModeInterfaceAdaptor.describeRenderer(SafeModeHelpCommand, { - isAlwaysSupposedToUseDefaultRenderer: true, -}); diff --git a/src/safemode/commands/SafeModeCommands.tsx b/src/safemode/commands/SafeModeCommands.tsx new file mode 100644 index 00000000..59091091 --- /dev/null +++ b/src/safemode/commands/SafeModeCommands.tsx @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { Result } from "@gnuxie/typescript-result"; +import { + CommandTable, + DeadDocumentJSX, + StandardAdaptorContextToCommandContextTranslator, + StandardCommandTable, + StandardMatrixInterfaceAdaptor, + TopPresentationSchema, + describeCommand, +} from "@the-draupnir-project/interface-manager"; +import { Ok, isError } from "matrix-protection-suite"; +import { + MatrixEventContext, + invocationInformationFromMatrixEventcontext, + MPSMatrixInterfaceAdaptorCallbacks, + MPSCommandDispatcherCallbacks, +} from "../../commands/interface-manager/MPSMatrixInterfaceAdaptor"; +import { SafeModeDraupnir } from "../DraupnirSafeMode"; +import { renderTableHelp } from "../../commands/interface-manager/MatrixHelpRenderer"; + +export const SafeModeContextToCommandContextTranslator = + new StandardAdaptorContextToCommandContextTranslator(); + +export const SafeModeInterfaceAdaptor = new StandardMatrixInterfaceAdaptor< + SafeModeDraupnir, + MatrixEventContext +>( + SafeModeContextToCommandContextTranslator, + invocationInformationFromMatrixEventcontext, + MPSMatrixInterfaceAdaptorCallbacks, + MPSCommandDispatcherCallbacks +); + +export const SafeModeCommands = new StandardCommandTable("safe mode"); + +export const SafeModeHelpCommand = describeCommand({ + rest: { + name: "command parts", + acceptor: TopPresentationSchema, + }, + summary: "Display this message", + executor: async function ( + _context, + _keywords + ): Promise> { + return Ok(SafeModeCommands); + }, + parameters: [], +}); + +SafeModeInterfaceAdaptor.describeRenderer(SafeModeHelpCommand, { + JSXRenderer(result) { + if (isError(result)) { + throw new TypeError("This should never fail"); + } + return Ok({renderTableHelp(result.ok)}); + }, +}); From 4f1b78950151ae48312e61c292bca6801b0cca6a Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 18 Sep 2024 12:45:52 +0100 Subject: [PATCH 08/18] Add a status command to safe mode. --- src/commands/SafeModeCommand.ts | 13 +- src/safemode/DraupnirSafeMode.ts | 2 +- src/safemode/SafeModeCause.ts | 3 +- ...daptor.ts => SafeModeCommandDispatcher.ts} | 8 +- src/safemode/commands/HelpCommand.tsx | 46 ++++++ src/safemode/commands/SafeModeAdaptor.ts | 28 ++++ src/safemode/commands/SafeModeCommands.tsx | 63 +------- src/safemode/commands/StatusCommand.tsx | 137 ++++++++++++++++++ 8 files changed, 232 insertions(+), 68 deletions(-) rename src/safemode/{SafeModeAdaptor.ts => SafeModeCommandDispatcher.ts} (89%) create mode 100644 src/safemode/commands/HelpCommand.tsx create mode 100644 src/safemode/commands/SafeModeAdaptor.ts create mode 100644 src/safemode/commands/StatusCommand.tsx diff --git a/src/commands/SafeModeCommand.ts b/src/commands/SafeModeCommand.ts index 96cf62fc..23e32eeb 100644 --- a/src/commands/SafeModeCommand.ts +++ b/src/commands/SafeModeCommand.ts @@ -2,7 +2,10 @@ // // SPDX-License-Identifier: AFL-3.0 -import { describeCommand } from "@the-draupnir-project/interface-manager"; +import { + BasicInvocationInformation, + describeCommand, +} from "@the-draupnir-project/interface-manager"; import { SafeModeDraupnir } from "../safemode/DraupnirSafeMode"; import { Result } from "@gnuxie/typescript-result"; import { Draupnir } from "../Draupnir"; @@ -12,11 +15,13 @@ import { DraupnirInterfaceAdaptor } from "./DraupnirCommandPrerequisites"; export const DraupnirSafeModeCommand = describeCommand({ summary: "Enter into safe mode.", parameters: [], - async executor({ - safeModeToggle, - }: Draupnir): Promise> { + async executor( + { safeModeToggle }: Draupnir, + info: BasicInvocationInformation + ): Promise> { return safeModeToggle.switchToSafeMode({ reason: SafeModeReason.ByRequest, + user: info.commandSender, }); }, }); diff --git a/src/safemode/DraupnirSafeMode.ts b/src/safemode/DraupnirSafeMode.ts index 8f39eb43..4c5a9364 100644 --- a/src/safemode/DraupnirSafeMode.ts +++ b/src/safemode/DraupnirSafeMode.ts @@ -18,7 +18,7 @@ import { MatrixSendClient } from "matrix-protection-suite-for-matrix-bot-sdk"; import { MatrixReactionHandler } from "../commands/interface-manager/MatrixReactionHandler"; import { IConfig } from "../config"; import { SafeModeCause } from "./SafeModeCause"; -import { makeSafeModeCommandDispatcher } from "./SafeModeAdaptor"; +import { makeSafeModeCommandDispatcher } from "./SafeModeCommandDispatcher"; import { ARGUMENT_PROMPT_LISTENER, DEFAUILT_ARGUMENT_PROMPT_LISTENER, diff --git a/src/safemode/SafeModeCause.ts b/src/safemode/SafeModeCause.ts index 4fb048d8..19e94122 100644 --- a/src/safemode/SafeModeCause.ts +++ b/src/safemode/SafeModeCause.ts @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AFL-3.0 import { ResultError } from "@gnuxie/typescript-result"; +import { StringUserID } from "@the-draupnir-project/matrix-basic-types"; export enum SafeModeReason { InitializationError = "InitializationError", @@ -10,5 +11,5 @@ export enum SafeModeReason { } export type SafeModeCause = - | { reason: SafeModeReason.ByRequest } + | { reason: SafeModeReason.ByRequest; user: StringUserID } | { reason: SafeModeReason.InitializationError; error: ResultError }; diff --git a/src/safemode/SafeModeAdaptor.ts b/src/safemode/SafeModeCommandDispatcher.ts similarity index 89% rename from src/safemode/SafeModeAdaptor.ts rename to src/safemode/SafeModeCommandDispatcher.ts index 1a9c5c52..b0ae6d46 100644 --- a/src/safemode/SafeModeAdaptor.ts +++ b/src/safemode/SafeModeCommandDispatcher.ts @@ -14,11 +14,9 @@ import { invocationInformationFromMatrixEventcontext, } from "../commands/interface-manager/MPSMatrixInterfaceAdaptor"; import { userLocalpart } from "@the-draupnir-project/matrix-basic-types"; -import { - SafeModeCommands, - SafeModeHelpCommand, -} from "./commands/SafeModeCommands"; -import { SafeModeInterfaceAdaptor } from "./commands/SafeModeCommands"; +import { SafeModeCommands } from "./commands/SafeModeCommands"; +import { SafeModeHelpCommand } from "./commands/HelpCommand"; +import { SafeModeInterfaceAdaptor } from "./commands/SafeModeAdaptor"; function makePrefixExtractor( safeModeDraupnir: SafeModeDraupnir diff --git a/src/safemode/commands/HelpCommand.tsx b/src/safemode/commands/HelpCommand.tsx new file mode 100644 index 00000000..748e1fbc --- /dev/null +++ b/src/safemode/commands/HelpCommand.tsx @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { SafeModeCommands } from "./SafeModeCommands"; +import { SafeModeInterfaceAdaptor } from "./SafeModeAdaptor"; + +import { Result } from "@gnuxie/typescript-result"; +import { + DeadDocumentJSX, + describeCommand, + TopPresentationSchema, + CommandTable, +} from "@the-draupnir-project/interface-manager"; +import { Ok, isError } from "matrix-protection-suite"; +import { renderTableHelp } from "../../commands/interface-manager/MatrixHelpRenderer"; +import { safeModeHeader } from "./StatusCommand"; + +export const SafeModeHelpCommand = describeCommand({ + rest: { + name: "command parts", + acceptor: TopPresentationSchema, + }, + summary: "Display this message", + executor: async function ( + _context, + _keywords + ): Promise> { + return Ok(SafeModeCommands); + }, + parameters: [], +}); + +SafeModeInterfaceAdaptor.describeRenderer(SafeModeHelpCommand, { + JSXRenderer(result) { + if (isError(result)) { + throw new TypeError("This should never fail"); + } + return Ok( + + {safeModeHeader()} + {renderTableHelp(result.ok)} + + ); + }, +}); diff --git a/src/safemode/commands/SafeModeAdaptor.ts b/src/safemode/commands/SafeModeAdaptor.ts new file mode 100644 index 00000000..46b0480c --- /dev/null +++ b/src/safemode/commands/SafeModeAdaptor.ts @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { + StandardAdaptorContextToCommandContextTranslator, + StandardMatrixInterfaceAdaptor, +} from "@the-draupnir-project/interface-manager"; +import { + MatrixEventContext, + invocationInformationFromMatrixEventcontext, + MPSMatrixInterfaceAdaptorCallbacks, + MPSCommandDispatcherCallbacks, +} from "../../commands/interface-manager/MPSMatrixInterfaceAdaptor"; +import { SafeModeDraupnir } from "../DraupnirSafeMode"; + +export const SafeModeContextToCommandContextTranslator = + new StandardAdaptorContextToCommandContextTranslator(); + +export const SafeModeInterfaceAdaptor = new StandardMatrixInterfaceAdaptor< + SafeModeDraupnir, + MatrixEventContext +>( + SafeModeContextToCommandContextTranslator, + invocationInformationFromMatrixEventcontext, + MPSMatrixInterfaceAdaptorCallbacks, + MPSCommandDispatcherCallbacks +); diff --git a/src/safemode/commands/SafeModeCommands.tsx b/src/safemode/commands/SafeModeCommands.tsx index 59091091..6e7b60c7 100644 --- a/src/safemode/commands/SafeModeCommands.tsx +++ b/src/safemode/commands/SafeModeCommands.tsx @@ -2,61 +2,10 @@ // // SPDX-License-Identifier: AFL-3.0 -import { Result } from "@gnuxie/typescript-result"; -import { - CommandTable, - DeadDocumentJSX, - StandardAdaptorContextToCommandContextTranslator, - StandardCommandTable, - StandardMatrixInterfaceAdaptor, - TopPresentationSchema, - describeCommand, -} from "@the-draupnir-project/interface-manager"; -import { Ok, isError } from "matrix-protection-suite"; -import { - MatrixEventContext, - invocationInformationFromMatrixEventcontext, - MPSMatrixInterfaceAdaptorCallbacks, - MPSCommandDispatcherCallbacks, -} from "../../commands/interface-manager/MPSMatrixInterfaceAdaptor"; -import { SafeModeDraupnir } from "../DraupnirSafeMode"; -import { renderTableHelp } from "../../commands/interface-manager/MatrixHelpRenderer"; +import { StandardCommandTable } from "@the-draupnir-project/interface-manager"; +import { SafeModeHelpCommand } from "./HelpCommand"; +import { SafeModeStatusCommand } from "./StatusCommand"; -export const SafeModeContextToCommandContextTranslator = - new StandardAdaptorContextToCommandContextTranslator(); - -export const SafeModeInterfaceAdaptor = new StandardMatrixInterfaceAdaptor< - SafeModeDraupnir, - MatrixEventContext ->( - SafeModeContextToCommandContextTranslator, - invocationInformationFromMatrixEventcontext, - MPSMatrixInterfaceAdaptorCallbacks, - MPSCommandDispatcherCallbacks -); - -export const SafeModeCommands = new StandardCommandTable("safe mode"); - -export const SafeModeHelpCommand = describeCommand({ - rest: { - name: "command parts", - acceptor: TopPresentationSchema, - }, - summary: "Display this message", - executor: async function ( - _context, - _keywords - ): Promise> { - return Ok(SafeModeCommands); - }, - parameters: [], -}); - -SafeModeInterfaceAdaptor.describeRenderer(SafeModeHelpCommand, { - JSXRenderer(result) { - if (isError(result)) { - throw new TypeError("This should never fail"); - } - return Ok({renderTableHelp(result.ok)}); - }, -}); +export const SafeModeCommands = new StandardCommandTable("safe mode") + .internCommand(SafeModeHelpCommand, ["draupnir", "help"]) + .internCommand(SafeModeStatusCommand, ["draupnir", "status"]); diff --git a/src/safemode/commands/StatusCommand.tsx b/src/safemode/commands/StatusCommand.tsx new file mode 100644 index 00000000..2f0d0458 --- /dev/null +++ b/src/safemode/commands/StatusCommand.tsx @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { Result, ResultError } from "@gnuxie/typescript-result"; +import { + DeadDocumentJSX, + DocumentNode, + describeCommand, +} from "@the-draupnir-project/interface-manager"; +import { ActionException, Ok, isError } from "matrix-protection-suite"; +import { SafeModeDraupnir } from "../DraupnirSafeMode"; +import { SafeModeCause, SafeModeReason } from "../SafeModeCause"; +import { + DOCUMENTATION_URL, + PACKAGE_JSON, + SOFTWARE_VERSION, +} from "../../config"; +import { SafeModeInterfaceAdaptor } from "./SafeModeAdaptor"; + +export function safeModeHeader(): DocumentNode { + return ( + + ⚠️ Draupnir is in safe mode (see status command) ⚠️ +
+
+ ); +} + +function renderSafeModeCauseError(error: ResultError): DocumentNode { + if (error instanceof ActionException) { + return ( + + Draupnir is in safe mode because Draupnir failed to start. +
+ {error.mostRelevantElaboration} +
+ Details can be found by providing the reference{" "} + {error.uuid} + to an administrator. +
{error.toReadableString()}
+
+ ); + } else { + return ( + + Draupnir is in safe mode because Draupnir failed to start. +
+ {error.mostRelevantElaboration} +
{error.toReadableString()}
+
+ ); + } +} +function renderSafeModeCause(safeModeCause: SafeModeCause): DocumentNode { + if (safeModeCause.reason === SafeModeReason.ByRequest) { + return ( + + Draupnir is in safe mode by request of {safeModeCause.user}. + + ); + } else { + return renderSafeModeCauseError(safeModeCause.error); + } +} + +export interface SafeModeStatusInfo { + safeModeCause: SafeModeCause; + documentationURL: string; + version: string; + repository: string; +} + +export function renderSafeModeStatusInfo( + info: SafeModeStatusInfo, + { showDocumentationURL = true }: { showDocumentationURL?: boolean } = {} +): DocumentNode { + return ( + + ⚠️ Draupnir is in safe mode ⚠️ + +
+ {renderSafeModeCause(info.safeModeCause)} +
+
+
+ Version: + {info.version} +
+ Repository: + {info.repository} +
+ {showDocumentationURL ? ( + + Documentation: {" "} + {info.documentationURL} + + ) : ( + + )} +
+
+ To attempt to restart, use !draupnir restart +
+ ); +} + +export function safeModeStatusInfo( + safeModeDraupnir: SafeModeDraupnir +): SafeModeStatusInfo { + return { + safeModeCause: safeModeDraupnir.cause, + documentationURL: DOCUMENTATION_URL, + version: SOFTWARE_VERSION, + repository: PACKAGE_JSON["repository"] ?? "Unknown", + }; +} + +export const SafeModeStatusCommand = describeCommand({ + summary: + "Display the status of safe mode, including the reason Draupnir started in safe mode.", + parameters: [], + async executor( + safeModeDraupnir: SafeModeDraupnir + ): Promise> { + return Ok(safeModeStatusInfo(safeModeDraupnir)); + }, +}); + +SafeModeInterfaceAdaptor.describeRenderer(SafeModeStatusCommand, { + JSXRenderer(result) { + if (isError(result)) { + return Ok(undefined); + } + return Ok({renderSafeModeStatusInfo(result.ok)}); + }, +}); From de6e4d7416e793ef34e6340e380e200a178e0c7c Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 18 Sep 2024 15:28:05 +0100 Subject: [PATCH 09/18] Add restart command to safe mode. Currently integration tests will be broken because we took control over "who starts Draupnir?" away and gave it to the `SafeModeToggle`. So we need to fix that. --- src/DraupnirBotMode.ts | 5 ++++- src/draupnirfactory/DraupnirFactory.ts | 4 +++- .../StandardDraupnirManager.ts | 13 +++--------- src/index.ts | 1 - src/safemode/DraupnirSafeMode.ts | 2 ++ .../commands/RestartDraupnirCommand.ts | 20 +++++++++++++++++++ src/safemode/commands/SafeModeCommands.tsx | 4 +++- test/integration/manualLaunchScript.ts | 1 - 8 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/DraupnirBotMode.ts b/src/DraupnirBotMode.ts index 3c1e1e64..e09ea440 100644 --- a/src/DraupnirBotMode.ts +++ b/src/DraupnirBotMode.ts @@ -14,6 +14,7 @@ import { setGlobalLoggerProvider, RoomStateBackingStore, ClientsInRoomMap, + Task, } from "matrix-protection-suite"; import { BotSDKLogServiceLogger, @@ -158,6 +159,7 @@ export class DraupnirBotModeToggle implements SafeModeToggle { this.safeModeDraupnir?.stop(); this.safeModeDraupnir = null; this.draupnir = draupnirResult.ok; + void Task(this.draupnir.start()); return draupnirResult; } public async switchToSafeMode( @@ -172,7 +174,8 @@ export class DraupnirBotModeToggle implements SafeModeToggle { this.clientUserID, this.managementRoom, this.config, - cause + cause, + this ); if (isError(safeModeResult)) { return safeModeResult; diff --git a/src/draupnirfactory/DraupnirFactory.ts b/src/draupnirfactory/DraupnirFactory.ts index bb3e9ced..f12ace19 100644 --- a/src/draupnirfactory/DraupnirFactory.ts +++ b/src/draupnirfactory/DraupnirFactory.ts @@ -95,7 +95,8 @@ export class DraupnirFactory { clientUserID: StringUserID, managementRoom: MatrixRoomID, config: IConfig, - cause: SafeModeCause + cause: SafeModeCause, + toggle: SafeModeToggle ): Promise> { const client = await this.clientProvider(clientUserID); const clientRooms = await this.clientsInRoomMap.makeClientRooms( @@ -117,6 +118,7 @@ export class DraupnirFactory { clientPlatform, managementRoom, clientRooms.ok, + toggle, config ) ); diff --git a/src/draupnirfactory/StandardDraupnirManager.ts b/src/draupnirfactory/StandardDraupnirManager.ts index 2925835c..c811cb2f 100644 --- a/src/draupnirfactory/StandardDraupnirManager.ts +++ b/src/draupnirfactory/StandardDraupnirManager.ts @@ -8,12 +8,7 @@ // https://github.com/matrix-org/mjolnir // -import { - ActionError, - ActionResult, - Task, - isError, -} from "matrix-protection-suite"; +import { ActionError, ActionResult, isError } from "matrix-protection-suite"; import { IConfig } from "../config"; import { DraupnirFactory } from "./DraupnirFactory"; import { Draupnir } from "../Draupnir"; @@ -86,9 +81,6 @@ export class StandardDraupnirManager { ); return draupnir; } - // FIXME: This is a little more than suspect that there are no handlers if starting fails? - // unclear to me what can fail though. - void Task(draupnir.ok.start()); this.draupnir.set(clientUserID, draupnir.ok); this.failedDraupnir.delete(clientUserID); return draupnir; @@ -109,7 +101,8 @@ export class StandardDraupnirManager { clientUserID, managementRoom, config, - cause + cause, + this.makeSafeModeToggle(clientUserID, managementRoom, config) ); if (isError(safeModeDraupnir)) { this.reportUnstartedDraupnir( diff --git a/src/index.ts b/src/index.ts index afe66779..6968f70f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -119,7 +119,6 @@ void (async function () { throw err; } try { - await bot.start(); await config.RUNTIME.client.start(); void Task(bot.startupComplete()); await apis.start(); diff --git a/src/safemode/DraupnirSafeMode.ts b/src/safemode/DraupnirSafeMode.ts index 4c5a9364..4721b2eb 100644 --- a/src/safemode/DraupnirSafeMode.ts +++ b/src/safemode/DraupnirSafeMode.ts @@ -26,6 +26,7 @@ import { makeListenerForPromptDefault, } from "../commands/interface-manager/MatrixPromptForAccept"; import { makeCommandDispatcherTimelineListener } from "./ManagementRoom"; +import { SafeModeToggle } from "./SafeModeToggle"; export class SafeModeDraupnir implements MatrixAdaptorContext { public reactionHandler: MatrixReactionHandler; @@ -44,6 +45,7 @@ export class SafeModeDraupnir implements MatrixAdaptorContext { public readonly clientPlatform: ClientPlatform, public readonly managementRoom: MatrixRoomID, private readonly clientRooms: ClientRooms, + public readonly safeModeToggle: SafeModeToggle, public readonly config: IConfig //private readonly roomStateManager: RoomStateManager, //private readonly policyRoomManager: PolicyRoomManager, diff --git a/src/safemode/commands/RestartDraupnirCommand.ts b/src/safemode/commands/RestartDraupnirCommand.ts index 78191cc2..687a5d2d 100644 --- a/src/safemode/commands/RestartDraupnirCommand.ts +++ b/src/safemode/commands/RestartDraupnirCommand.ts @@ -1,3 +1,23 @@ // SPDX-FileCopyrightText: 2024 Gnuxie // // SPDX-License-Identifier: AFL-3.0 + +import { describeCommand } from "@the-draupnir-project/interface-manager"; +import { Draupnir } from "../../Draupnir"; +import { SafeModeDraupnir } from "../DraupnirSafeMode"; +import { Result } from "@gnuxie/typescript-result"; +import { SafeModeInterfaceAdaptor } from "./SafeModeAdaptor"; + +export const SafeModeRestartCommand = describeCommand({ + summary: "Restart Draupnir, quitting safe mode.", + parameters: [], + async executor({ + safeModeToggle, + }: SafeModeDraupnir): Promise> { + return safeModeToggle.switchToDraupnir(); + }, +}); + +SafeModeInterfaceAdaptor.describeRenderer(SafeModeRestartCommand, { + isAlwaysSupposedToUseDefaultRenderer: true, +}); diff --git a/src/safemode/commands/SafeModeCommands.tsx b/src/safemode/commands/SafeModeCommands.tsx index 6e7b60c7..c7ab6426 100644 --- a/src/safemode/commands/SafeModeCommands.tsx +++ b/src/safemode/commands/SafeModeCommands.tsx @@ -5,7 +5,9 @@ import { StandardCommandTable } from "@the-draupnir-project/interface-manager"; import { SafeModeHelpCommand } from "./HelpCommand"; import { SafeModeStatusCommand } from "./StatusCommand"; +import { SafeModeRestartCommand } from "./RestartDraupnirCommand"; export const SafeModeCommands = new StandardCommandTable("safe mode") .internCommand(SafeModeHelpCommand, ["draupnir", "help"]) - .internCommand(SafeModeStatusCommand, ["draupnir", "status"]); + .internCommand(SafeModeStatusCommand, ["draupnir", "status"]) + .internCommand(SafeModeRestartCommand, ["draupnir", "restart"]); diff --git a/test/integration/manualLaunchScript.ts b/test/integration/manualLaunchScript.ts index 199270a7..9993b2c6 100644 --- a/test/integration/manualLaunchScript.ts +++ b/test/integration/manualLaunchScript.ts @@ -29,7 +29,6 @@ void (async () => { ) ); console.info(`management room ${mjolnir.managementRoom.toPermalink()}`); - await mjolnir.start(); const apis = constructWebAPIs(mjolnir); await draupnirClient()?.start(); await apis.start(); From bd209848a39c00fc23975274ef8ef82f6814f4de Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 18 Sep 2024 15:37:45 +0100 Subject: [PATCH 10/18] Fix integration test fixtures now safe mode toggle controls draupnir. --- test/integration/fixtures.ts | 12 +----------- test/integration/manualLaunchScript.ts | 9 ++++----- test/integration/mjolnirSetupUtils.ts | 15 ++++++++++++++- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index efaca9a5..5402f4d8 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -46,22 +46,12 @@ export const mochaHooks = { this.timeout(30000); const config = (this.config = configRead()); this.managementRoomAlias = config.managementRoom; - this.draupnir = await makeMjolnir(config); + this.draupnir = await makeMjolnir(config, { eraseAccountData: true }); const draupnirMatrixClient = draupnirClient(); if (draupnirMatrixClient === null) { throw new TypeError(`setup code is broken`); } config.RUNTIME.client = draupnirMatrixClient; - await Promise.all([ - this.draupnir.client.setAccountData( - MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE, - { rooms: [] } - ), - this.draupnir.client.setAccountData( - MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE, - { references: [] } - ), - ]); await this.draupnir.start(); this.apis = constructWebAPIs(this.draupnir); await this.apis.start(); diff --git a/test/integration/manualLaunchScript.ts b/test/integration/manualLaunchScript.ts index 9993b2c6..fcddf5dd 100644 --- a/test/integration/manualLaunchScript.ts +++ b/test/integration/manualLaunchScript.ts @@ -21,13 +21,12 @@ import { DefaultEventDecoder, Task } from "matrix-protection-suite"; void (async () => { const config = configRead(); - const mjolnir = await makeMjolnir( - config, - new SqliteRoomStateBackingStore( + const mjolnir = await makeMjolnir(config, { + backingStore: new SqliteRoomStateBackingStore( path.join(config.dataPath, "room-state-backing-store.db"), DefaultEventDecoder - ) - ); + ), + }); console.info(`management room ${mjolnir.managementRoom.toPermalink()}`); const apis = constructWebAPIs(mjolnir); await draupnirClient()?.start(); diff --git a/test/integration/mjolnirSetupUtils.ts b/test/integration/mjolnirSetupUtils.ts index 170cab4b..b0dac89e 100644 --- a/test/integration/mjolnirSetupUtils.ts +++ b/test/integration/mjolnirSetupUtils.ts @@ -27,6 +27,8 @@ import { } from "matrix-protection-suite-for-matrix-bot-sdk"; import { DefaultEventDecoder, + MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE, + MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE, RoomStateBackingStore, } from "matrix-protection-suite"; import { WebAPIs } from "../../src/webapis/WebAPIs"; @@ -121,7 +123,10 @@ let globalSafeEmitter: SafeMatrixEmitter | undefined; */ export async function makeMjolnir( config: IConfig, - backingStore?: RoomStateBackingStore + { + backingStore, + eraseAccountData, + }: { backingStore?: RoomStateBackingStore; eraseAccountData?: boolean } = {} ): Promise { await configureMjolnir(config); LogService.setLogger(new RichConsoleLogger()); @@ -135,6 +140,14 @@ export async function makeMjolnir( config.pantalaimon.username, config.pantalaimon.password ); + if (eraseAccountData) { + await Promise.all([ + client.setAccountData(MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE, { rooms: [] }), + client.setAccountData(MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE, { + references: [], + }), + ]); + } await overrideRatelimitForUser( config.homeserverUrl, await client.getUserId() From 4db7ce3872c003dc622a960846dbc58fbd7c09e6 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 18 Sep 2024 16:04:49 +0100 Subject: [PATCH 11/18] Find a way to print draupnir status on startup from the toggle. without disrupting e2ee. --- src/DraupnirBotMode.ts | 12 ++++++++++-- src/index.ts | 3 ++- src/safemode/SafeModeToggle.ts | 9 +++++++-- test/integration/mjolnirSetupUtils.ts | 7 ++++--- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/DraupnirBotMode.ts b/src/DraupnirBotMode.ts index e09ea440..3e672078 100644 --- a/src/DraupnirBotMode.ts +++ b/src/DraupnirBotMode.ts @@ -37,7 +37,10 @@ import { MatrixRoomID, } from "@the-draupnir-project/matrix-basic-types"; import { Result, isError } from "@gnuxie/typescript-result"; -import { SafeModeToggle } from "./safemode/SafeModeToggle"; +import { + SafeModeToggle, + SafeModeToggleOptions, +} from "./safemode/SafeModeToggle"; import { SafeModeDraupnir } from "./safemode/DraupnirSafeMode"; import { ResultError } from "@gnuxie/typescript-result"; import { SafeModeCause } from "./safemode/SafeModeCause"; @@ -141,7 +144,9 @@ export class DraupnirBotModeToggle implements SafeModeToggle { config ); } - public async switchToDraupnir(): Promise> { + public async switchToDraupnir( + options?: SafeModeToggleOptions + ): Promise> { if (this.draupnir !== null) { return ResultError.Result( `There is a draupnir for ${this.clientUserID} already running` @@ -160,6 +165,9 @@ export class DraupnirBotModeToggle implements SafeModeToggle { this.safeModeDraupnir = null; this.draupnir = draupnirResult.ok; void Task(this.draupnir.start()); + if (options?.sendStatusOnStart) { + void Task(this.draupnir.startupComplete()); + } return draupnirResult; } public async switchToSafeMode( diff --git a/src/index.ts b/src/index.ts index 6968f70f..bd06055f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -108,7 +108,8 @@ void (async function () { store ); - bot = (await toggle.switchToDraupnir()).expect( + // 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" ); apis = constructWebAPIs(bot); diff --git a/src/safemode/SafeModeToggle.ts b/src/safemode/SafeModeToggle.ts index 3884853f..ddc831fc 100644 --- a/src/safemode/SafeModeToggle.ts +++ b/src/safemode/SafeModeToggle.ts @@ -7,6 +7,8 @@ import { Draupnir } from "../Draupnir"; import { SafeModeDraupnir } from "./DraupnirSafeMode"; import { SafeModeCause } from "./SafeModeCause"; +export type SafeModeToggleOptions = { sendStatusOnStart?: boolean }; + export interface SafeModeToggle { /** * Switch the bot to Draupnir mode. @@ -15,6 +17,9 @@ export interface SafeModeToggle { * That means that by the command responds with ticks and crosses, * draupnir will be running or we will still be in safe mode. */ - switchToDraupnir(): Promise>; - switchToSafeMode(cause: SafeModeCause): Promise>; + switchToDraupnir(options?: SafeModeToggleOptions): Promise>; + switchToSafeMode( + cause: SafeModeCause, + options?: SafeModeToggleOptions + ): Promise>; } diff --git a/test/integration/mjolnirSetupUtils.ts b/test/integration/mjolnirSetupUtils.ts index b0dac89e..f11e0289 100644 --- a/test/integration/mjolnirSetupUtils.ts +++ b/test/integration/mjolnirSetupUtils.ts @@ -159,9 +159,10 @@ export async function makeMjolnir( config, backingStore ); - const mj = (await toggle.switchToDraupnir()).expect( - "Could not create Draupnir" - ); + // we don't want to send status on startup incase we want to test e2ee from the manual launch script. + const mj = ( + await toggle.switchToDraupnir({ sendStatusOnStart: false }) + ).expect("Could not create Draupnir"); globalClient = client; globalMjolnir = mj; globalSafeEmitter = new SafeMatrixEmitterWrapper(client, DefaultEventDecoder); From d82a0b0731cd35bc0ebc5cb10c2d18292a484a95 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 18 Sep 2024 16:19:47 +0100 Subject: [PATCH 12/18] Print status information when booting from and to safe mode. --- src/DraupnirBotMode.ts | 6 ++++- src/commands/SafeModeCommand.ts | 11 +++++---- .../interface-manager/MatrixHelpRenderer.tsx | 4 ++++ src/safemode/DraupnirSafeMode.ts | 23 ++++++++++++++++++- .../commands/RestartDraupnirCommand.ts | 2 +- 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/DraupnirBotMode.ts b/src/DraupnirBotMode.ts index 3e672078..3bfbaa54 100644 --- a/src/DraupnirBotMode.ts +++ b/src/DraupnirBotMode.ts @@ -171,7 +171,8 @@ export class DraupnirBotModeToggle implements SafeModeToggle { return draupnirResult; } public async switchToSafeMode( - cause: SafeModeCause + cause: SafeModeCause, + options?: SafeModeToggleOptions ): Promise> { if (this.safeModeDraupnir !== null) { return ResultError.Result( @@ -192,6 +193,9 @@ export class DraupnirBotModeToggle implements SafeModeToggle { this.draupnir = null; this.safeModeDraupnir = safeModeResult.ok; this.safeModeDraupnir.start(); + if (options?.sendStatusOnStart) { + this.safeModeDraupnir.sendStartupComplete(); + } return safeModeResult; } } diff --git a/src/commands/SafeModeCommand.ts b/src/commands/SafeModeCommand.ts index 23e32eeb..648c2653 100644 --- a/src/commands/SafeModeCommand.ts +++ b/src/commands/SafeModeCommand.ts @@ -19,10 +19,13 @@ export const DraupnirSafeModeCommand = describeCommand({ { safeModeToggle }: Draupnir, info: BasicInvocationInformation ): Promise> { - return safeModeToggle.switchToSafeMode({ - reason: SafeModeReason.ByRequest, - user: info.commandSender, - }); + return safeModeToggle.switchToSafeMode( + { + reason: SafeModeReason.ByRequest, + user: info.commandSender, + }, + { sendStatusOnStart: true } + ); }, }); diff --git a/src/commands/interface-manager/MatrixHelpRenderer.tsx b/src/commands/interface-manager/MatrixHelpRenderer.tsx index 3a38a39c..34e6bde4 100644 --- a/src/commands/interface-manager/MatrixHelpRenderer.tsx +++ b/src/commands/interface-manager/MatrixHelpRenderer.tsx @@ -370,3 +370,7 @@ export function renderTableHelp(table: CommandTable): DocumentNode { ); } + +export function wrapInRoot(node: DocumentNode): DocumentNode { + return {node}; +} diff --git a/src/safemode/DraupnirSafeMode.ts b/src/safemode/DraupnirSafeMode.ts index 4721b2eb..e2aeacc3 100644 --- a/src/safemode/DraupnirSafeMode.ts +++ b/src/safemode/DraupnirSafeMode.ts @@ -7,8 +7,12 @@ import { ClientRooms, EventReport, RoomEvent, + Task, } from "matrix-protection-suite"; -import { MatrixAdaptorContext } from "../commands/interface-manager/MPSMatrixInterfaceAdaptor"; +import { + MatrixAdaptorContext, + sendMatrixEventsFromDeadDocument, +} from "../commands/interface-manager/MPSMatrixInterfaceAdaptor"; import { StringUserID, StringRoomID, @@ -27,6 +31,12 @@ import { } from "../commands/interface-manager/MatrixPromptForAccept"; import { makeCommandDispatcherTimelineListener } from "./ManagementRoom"; import { SafeModeToggle } from "./SafeModeToggle"; +import { Result } from "@gnuxie/typescript-result"; +import { + renderSafeModeStatusInfo, + safeModeStatusInfo, +} from "./commands/StatusCommand"; +import { wrapInRoot } from "../commands/interface-manager/MatrixHelpRenderer"; export class SafeModeDraupnir implements MatrixAdaptorContext { public reactionHandler: MatrixReactionHandler; @@ -90,4 +100,15 @@ export class SafeModeDraupnir implements MatrixAdaptorContext { public stop(): void { this.clientRooms.off("timeline", this.timelineEventListener); } + + public sendStartupComplete(): void { + void Task( + sendMatrixEventsFromDeadDocument( + this.clientPlatform.toRoomMessageSender(), + this.commandRoomID, + wrapInRoot(renderSafeModeStatusInfo(safeModeStatusInfo(this))), + {} + ) as Promise> + ); + } } diff --git a/src/safemode/commands/RestartDraupnirCommand.ts b/src/safemode/commands/RestartDraupnirCommand.ts index 687a5d2d..e40048a2 100644 --- a/src/safemode/commands/RestartDraupnirCommand.ts +++ b/src/safemode/commands/RestartDraupnirCommand.ts @@ -14,7 +14,7 @@ export const SafeModeRestartCommand = describeCommand({ async executor({ safeModeToggle, }: SafeModeDraupnir): Promise> { - return safeModeToggle.switchToDraupnir(); + return safeModeToggle.switchToDraupnir({ sendStatusOnStart: true }); }, }); From a92c95a0dd89718a61ef952dc0522ba79a903c19 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 18 Sep 2024 18:08:36 +0100 Subject: [PATCH 13/18] 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(), From 35ccf94794062dbe3478cb142f10feb5b319dfb6 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 18 Sep 2024 18:29:49 +0100 Subject: [PATCH 14/18] Make integration test harness use the BotModeToggle. --- src/DraupnirBotMode.ts | 13 +++++++------ test/integration/fixtures.ts | 14 ++++++-------- test/integration/manualLaunchScript.ts | 17 +++++++++-------- test/integration/mjolnirSetupUtils.ts | 22 +++++++++++++++------- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/DraupnirBotMode.ts b/src/DraupnirBotMode.ts index 82509fda..370c5139 100644 --- a/src/DraupnirBotMode.ts +++ b/src/DraupnirBotMode.ts @@ -16,7 +16,6 @@ import { ClientsInRoomMap, Task, Logger, - Ok, ActionException, ActionExceptionKind, } from "matrix-protection-suite"; @@ -68,7 +67,9 @@ export function constructWebAPIs(draupnir: Draupnir): WebAPIs { interface BotModeTogle extends SafeModeToggle { encryptionInitialized(): Promise; stopEverything(): void; - startFromScratch(options?: SafeModeToggleOptions): Promise>; + startFromScratch( + options?: SafeModeToggleOptions + ): Promise>; } export class DraupnirBotModeToggle implements BotModeTogle { @@ -235,7 +236,7 @@ export class DraupnirBotModeToggle implements BotModeTogle { public async startFromScratch( options?: SafeModeToggleOptions - ): Promise> { + ): Promise> { const draupnirResult = await this.switchToDraupnir(options ?? {}); if (isError(draupnirResult)) { if (this.config.safeMode?.bootOnStartupFailure) { @@ -243,18 +244,18 @@ export class DraupnirBotModeToggle implements BotModeTogle { "Failed to start draupnir, switching to safe mode as configured", draupnirResult.error ); - return (await this.switchToSafeMode( + return await this.switchToSafeMode( { reason: SafeModeReason.InitializationError, error: draupnirResult.error, }, options ?? {} - )) as Result; + ); } else { return draupnirResult; } } - return Ok(undefined); + return draupnirResult; } public async encryptionInitialized(): Promise { diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index 5402f4d8..b8a182a6 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -12,13 +12,13 @@ import { MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE, MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE, } from "matrix-protection-suite"; -import { constructWebAPIs } from "../../src/DraupnirBotMode"; import { read as configRead } from "../../src/config"; import { patchMatrixClient } from "../../src/utils"; import { DraupnirTestContext, + draupnir, draupnirClient, - makeMjolnir, + makeBotModeToggle, teardownManagementRoom, } from "./mjolnirSetupUtils"; import { MatrixRoomReference } from "@the-draupnir-project/matrix-basic-types"; @@ -46,25 +46,23 @@ export const mochaHooks = { this.timeout(30000); const config = (this.config = configRead()); this.managementRoomAlias = config.managementRoom; - this.draupnir = await makeMjolnir(config, { eraseAccountData: true }); + this.toggle = await makeBotModeToggle(config, { eraseAccountData: true }); + this.draupnir = draupnir(); const draupnirMatrixClient = draupnirClient(); if (draupnirMatrixClient === null) { throw new TypeError(`setup code is broken`); } config.RUNTIME.client = draupnirMatrixClient; - await this.draupnir.start(); - this.apis = constructWebAPIs(this.draupnir); - await this.apis.start(); await draupnirClient()?.start(); + await this.toggle.encryptionInitialized(); console.log("mochaHooks.beforeEach DONE"); }, ], afterEach: [ async function (this: DraupnirTestContext) { this.timeout(10000); - this.apis?.stop(); + this.toggle?.stopEverything(); draupnirClient()?.stop(); - this.draupnir?.stop(); // remove alias from management room and leave it. if (this.draupnir !== undefined) { diff --git a/test/integration/manualLaunchScript.ts b/test/integration/manualLaunchScript.ts index fcddf5dd..f3e27585 100644 --- a/test/integration/manualLaunchScript.ts +++ b/test/integration/manualLaunchScript.ts @@ -12,24 +12,25 @@ * This file is used to launch mjolnir for manual testing, creating a user and management room automatically if it doesn't already exist. */ -import { draupnirClient, makeMjolnir } from "./mjolnirSetupUtils"; +import { + draupnir, + draupnirClient, + makeBotModeToggle, +} from "./mjolnirSetupUtils"; import { read as configRead } from "../../src/config"; -import { constructWebAPIs } from "../../src/DraupnirBotMode"; import { SqliteRoomStateBackingStore } from "../../src/backingstore/better-sqlite3/SqliteRoomStateBackingStore"; import path from "path"; -import { DefaultEventDecoder, Task } from "matrix-protection-suite"; +import { DefaultEventDecoder } from "matrix-protection-suite"; void (async () => { const config = configRead(); - const mjolnir = await makeMjolnir(config, { + const toggle = await makeBotModeToggle(config, { backingStore: new SqliteRoomStateBackingStore( path.join(config.dataPath, "room-state-backing-store.db"), DefaultEventDecoder ), }); - console.info(`management room ${mjolnir.managementRoom.toPermalink()}`); - const apis = constructWebAPIs(mjolnir); + console.info(`management room ${draupnir().managementRoom.toPermalink()}`); await draupnirClient()?.start(); - await apis.start(); - void Task(mjolnir.startupComplete()); + await toggle.encryptionInitialized(); })(); diff --git a/test/integration/mjolnirSetupUtils.ts b/test/integration/mjolnirSetupUtils.ts index f11e0289..a6fb269c 100644 --- a/test/integration/mjolnirSetupUtils.ts +++ b/test/integration/mjolnirSetupUtils.ts @@ -31,7 +31,7 @@ import { MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE, RoomStateBackingStore, } from "matrix-protection-suite"; -import { WebAPIs } from "../../src/webapis/WebAPIs"; +import { SafeModeDraupnir } from "../../src/safemode/DraupnirSafeMode"; patchMatrixClient(); @@ -51,7 +51,7 @@ export type SafeMochaContext = Pick< export interface DraupnirTestContext extends SafeMochaContext { draupnir?: Draupnir; managementRoomAlias?: string; - apis?: WebAPIs; + toggle?: DraupnirBotModeToggle; config: IConfig; } @@ -102,7 +102,10 @@ async function configureMjolnir(config: IConfig) { } } -export function draupnir(): Draupnir | null { +export function draupnir(): Draupnir { + if (globalMjolnir === null) { + throw new TypeError("Setup code didn't run before you called `draupnir()`"); + } return globalMjolnir; } export function draupnirClient(): MatrixClient | null { @@ -121,13 +124,13 @@ let globalSafeEmitter: SafeMatrixEmitter | undefined; /** * Return a test instance of Mjolnir. */ -export async function makeMjolnir( +export async function makeBotModeToggle( config: IConfig, { backingStore, eraseAccountData, }: { backingStore?: RoomStateBackingStore; eraseAccountData?: boolean } = {} -): Promise { +): Promise { await configureMjolnir(config); LogService.setLogger(new RichConsoleLogger()); LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG)); @@ -161,12 +164,17 @@ export async function makeMjolnir( ); // we don't want to send status on startup incase we want to test e2ee from the manual launch script. const mj = ( - await toggle.switchToDraupnir({ sendStatusOnStart: false }) + await toggle.startFromScratch({ sendStatusOnStart: false }) ).expect("Could not create Draupnir"); + if (mj instanceof SafeModeDraupnir) { + throw new TypeError( + "Setup code is wrong, shouldn't be booting into safe mode" + ); + } globalClient = client; globalMjolnir = mj; globalSafeEmitter = new SafeMatrixEmitterWrapper(client, DefaultEventDecoder); - return mj; + return toggle; } /** From 0895c58882606cc35b44d21166db3181eadd7301 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 18 Sep 2024 18:47:13 +0100 Subject: [PATCH 15/18] Allow manual launch script to use safe mode. --- test/integration/manualLaunchScript.ts | 8 ++------ test/integration/mjolnirSetupUtils.ts | 14 +++++++++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/test/integration/manualLaunchScript.ts b/test/integration/manualLaunchScript.ts index f3e27585..313ddfc3 100644 --- a/test/integration/manualLaunchScript.ts +++ b/test/integration/manualLaunchScript.ts @@ -12,11 +12,7 @@ * This file is used to launch mjolnir for manual testing, creating a user and management room automatically if it doesn't already exist. */ -import { - draupnir, - draupnirClient, - makeBotModeToggle, -} from "./mjolnirSetupUtils"; +import { draupnirClient, makeBotModeToggle } from "./mjolnirSetupUtils"; import { read as configRead } from "../../src/config"; import { SqliteRoomStateBackingStore } from "../../src/backingstore/better-sqlite3/SqliteRoomStateBackingStore"; import path from "path"; @@ -29,8 +25,8 @@ void (async () => { path.join(config.dataPath, "room-state-backing-store.db"), DefaultEventDecoder ), + allowSafeMode: true, }); - console.info(`management room ${draupnir().managementRoom.toPermalink()}`); await draupnirClient()?.start(); await toggle.encryptionInitialized(); })(); diff --git a/test/integration/mjolnirSetupUtils.ts b/test/integration/mjolnirSetupUtils.ts index a6fb269c..0b962f19 100644 --- a/test/integration/mjolnirSetupUtils.ts +++ b/test/integration/mjolnirSetupUtils.ts @@ -129,7 +129,12 @@ export async function makeBotModeToggle( { backingStore, eraseAccountData, - }: { backingStore?: RoomStateBackingStore; eraseAccountData?: boolean } = {} + allowSafeMode, + }: { + backingStore?: RoomStateBackingStore; + eraseAccountData?: boolean; + allowSafeMode?: boolean; + } = {} ): Promise { await configureMjolnir(config); LogService.setLogger(new RichConsoleLogger()); @@ -166,13 +171,16 @@ export async function makeBotModeToggle( const mj = ( await toggle.startFromScratch({ sendStatusOnStart: false }) ).expect("Could not create Draupnir"); - if (mj instanceof SafeModeDraupnir) { + if (mj instanceof SafeModeDraupnir && !allowSafeMode) { throw new TypeError( "Setup code is wrong, shouldn't be booting into safe mode" ); } globalClient = client; - globalMjolnir = mj; + if (mj instanceof Draupnir) { + globalMjolnir = mj; + } + console.info(`management room ${mj.managementRoom.toPermalink()}`); globalSafeEmitter = new SafeMatrixEmitterWrapper(client, DefaultEventDecoder); return toggle; } From 334aaa7233667216533cd18c5dfc9a9139b0f87a Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 18 Sep 2024 19:53:58 +0100 Subject: [PATCH 16/18] Give safe mode config option a better name. --- src/DraupnirBotMode.ts | 2 +- src/config.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/DraupnirBotMode.ts b/src/DraupnirBotMode.ts index 370c5139..30427c4f 100644 --- a/src/DraupnirBotMode.ts +++ b/src/DraupnirBotMode.ts @@ -239,7 +239,7 @@ export class DraupnirBotModeToggle implements BotModeTogle { ): Promise> { const draupnirResult = await this.switchToDraupnir(options ?? {}); if (isError(draupnirResult)) { - if (this.config.safeMode?.bootOnStartupFailure) { + if (this.config.safeMode?.bootIntoOnStartupFailure) { log.error( "Failed to start draupnir, switching to safe mode as configured", draupnirResult.error diff --git a/src/config.ts b/src/config.ts index 6893df2d..668cc0ce 100644 --- a/src/config.ts +++ b/src/config.ts @@ -90,7 +90,7 @@ export interface IConfig { }; }; safeMode?: { - bootOnStartupFailure: boolean; + bootIntoOnStartupFailure: boolean; }; health: { healthz: { @@ -192,7 +192,7 @@ const defaultConfig: IConfig = { }, }, safeMode: { - bootOnStartupFailure: false, + bootIntoOnStartupFailure: false, }, health: { healthz: { From b52948b353f54841d4190325d905ed6765e5bb4e Mon Sep 17 00:00:00 2001 From: gnuxie Date: Thu, 19 Sep 2024 12:27:39 +0100 Subject: [PATCH 17/18] Forgot to start Draupnir listening in appservice. We need to look at Draupnir's .start method and make it `void`. The report poller should just throw if it fails without being caught by `Task`. We also need to test the `toggle` in appservice in integration tests. --- src/draupnirfactory/StandardDraupnirManager.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/draupnirfactory/StandardDraupnirManager.ts b/src/draupnirfactory/StandardDraupnirManager.ts index c811cb2f..562de992 100644 --- a/src/draupnirfactory/StandardDraupnirManager.ts +++ b/src/draupnirfactory/StandardDraupnirManager.ts @@ -8,7 +8,12 @@ // https://github.com/matrix-org/mjolnir // -import { ActionError, ActionResult, isError } from "matrix-protection-suite"; +import { + ActionError, + ActionResult, + Task, + isError, +} from "matrix-protection-suite"; import { IConfig } from "../config"; import { DraupnirFactory } from "./DraupnirFactory"; import { Draupnir } from "../Draupnir"; @@ -83,6 +88,7 @@ export class StandardDraupnirManager { } this.draupnir.set(clientUserID, draupnir.ok); this.failedDraupnir.delete(clientUserID); + void Task(draupnir.ok.start()); return draupnir; } From f1a6c8cfbf3e6bed38123a81f73aa4d1952a0b55 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Thu, 19 Sep 2024 12:46:57 +0100 Subject: [PATCH 18/18] Change `Draupnir['start']` to be synchronous. --- src/Draupnir.ts | 6 +++--- src/DraupnirBotMode.ts | 2 +- src/draupnirfactory/StandardDraupnirManager.ts | 9 ++------- src/report/ReportPoller.ts | 12 ++++++++++++ 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Draupnir.ts b/src/Draupnir.ts index bf0172d1..07024c0e 100644 --- a/src/Draupnir.ts +++ b/src/Draupnir.ts @@ -322,17 +322,17 @@ export class Draupnir implements Client, MatrixAdaptorContext { * This will not start the appservice from listening and responding * to events. Nor will it start any syncing client. */ - public async start(): Promise { + public start(): void { // to avoid handlers getting out of sync on clientRooms and leaking // when draupnir keeps being started and restarted, we can basically // clear all listeners each time and add the factory listener back. this.clientRooms.on("timeline", this.timelineEventListener); if (this.reportPoller) { - const reportPollSetting = await ReportPoller.getReportPollSetting( + // allow this to crash draupnir if it fails, since we need to know. + void this.reportPoller.startFromStoredSetting( this.client, this.managementRoomOutput ); - this.reportPoller.start(reportPollSetting); } } diff --git a/src/DraupnirBotMode.ts b/src/DraupnirBotMode.ts index 30427c4f..654c4617 100644 --- a/src/DraupnirBotMode.ts +++ b/src/DraupnirBotMode.ts @@ -184,7 +184,7 @@ export class DraupnirBotModeToggle implements BotModeTogle { return draupnirResult; } this.draupnir = draupnirResult.ok; - void Task(this.draupnir.start()); + this.draupnir.start(); if (options?.sendStatusOnStart) { void Task(this.draupnir.startupComplete()); try { diff --git a/src/draupnirfactory/StandardDraupnirManager.ts b/src/draupnirfactory/StandardDraupnirManager.ts index 562de992..254ece75 100644 --- a/src/draupnirfactory/StandardDraupnirManager.ts +++ b/src/draupnirfactory/StandardDraupnirManager.ts @@ -8,12 +8,7 @@ // https://github.com/matrix-org/mjolnir // -import { - ActionError, - ActionResult, - Task, - isError, -} from "matrix-protection-suite"; +import { ActionError, ActionResult, isError } from "matrix-protection-suite"; import { IConfig } from "../config"; import { DraupnirFactory } from "./DraupnirFactory"; import { Draupnir } from "../Draupnir"; @@ -88,7 +83,7 @@ export class StandardDraupnirManager { } this.draupnir.set(clientUserID, draupnir.ok); this.failedDraupnir.delete(clientUserID); - void Task(draupnir.ok.start()); + draupnir.ok.start(); return draupnir; } diff --git a/src/report/ReportPoller.ts b/src/report/ReportPoller.ts index 3fc9f893..463089d3 100644 --- a/src/report/ReportPoller.ts +++ b/src/report/ReportPoller.ts @@ -207,6 +207,18 @@ export class ReportPoller { } return reportPollSetting; } + + public async startFromStoredSetting( + client: MatrixSendClient, + managementRoomOutput: ManagementRoomOutput + ): Promise { + const reportPollSetting = await ReportPoller.getReportPollSetting( + client, + managementRoomOutput + ); + this.start(reportPollSetting); + } + public start({ from: startFrom }: ReportPollSetting) { if (this.timeout === null) { this.from = startFrom;