mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-10-24 09:52:21 +00:00
Merge pull request #588 from the-draupnir-project/gnuxie/config-editor safe mode recovery
Safe Mode account data config recovery.
This commit is contained in:
23 files changed
+679
-106
No files matched your search
@@ -211,6 +211,23 @@ protections:
|
||||
roomStateBackingStore:
|
||||
enabled: false
|
||||
|
||||
# Safe mode provides recovery options for some failure modes when Draupnir
|
||||
# fails to start. For example, if the bot fails to resolve a room alias in
|
||||
# a watched list, or if the server has parted from a protected room and can't
|
||||
# find a way back in. Safe mode will provide different options to recover from
|
||||
# these. Such as unprotecting the room or unwatching the policy list.
|
||||
# By default Draupnir will boot into safe mode only when the failure mode
|
||||
# is recoverable.
|
||||
# It may be desirable to prevent the bot from starting into safe mode if you have
|
||||
# a pager system when Draupnir is down, as Draupnir could prevent your monitoring
|
||||
# system from identifying a failure to start.
|
||||
#safeMode:
|
||||
# # The option for entering safe mode when Draupnir fails to start up.
|
||||
# # - "RecoveryOnly" will only start the bot in safe mode when there are recovery options available. This is the default.
|
||||
# # - "Never" will never start the bot in safe mode when Draupnir fails to start normally.
|
||||
# # - "Always" will always start the bot in safe mode when Draupnir fails to start normally.
|
||||
# bootOption: RecoveryOnly
|
||||
|
||||
# Options for advanced monitoring of the health of the bot.
|
||||
health:
|
||||
# healthz options. These options are best for use in container environments
|
||||
|
||||
+3
-3
@@ -56,7 +56,7 @@
|
||||
"@sentry/node": "^7.17.2",
|
||||
"@sentry/tracing": "^7.17.2",
|
||||
"@sinclair/typebox": "0.32.34",
|
||||
"@the-draupnir-project/interface-manager": "2.4.0",
|
||||
"@the-draupnir-project/interface-manager": "2.4.1",
|
||||
"@the-draupnir-project/matrix-basic-types": "^0.2.0",
|
||||
"await-lock": "^2.2.2",
|
||||
"better-sqlite3": "^9.4.3",
|
||||
@@ -69,8 +69,8 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"matrix-appservice-bridge": "^9.0.1",
|
||||
"matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@1.4.0",
|
||||
"matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@1.4.0",
|
||||
"matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@1.5.1",
|
||||
"matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@1.5.1",
|
||||
"parse-duration": "^1.0.2",
|
||||
"pg": "^8.8.0",
|
||||
"shell-quote": "^1.7.3",
|
||||
|
||||
+10
-2
@@ -164,11 +164,19 @@ export class Draupnir implements Client, MatrixAdaptorContext {
|
||||
}
|
||||
this.reactionHandler.on(
|
||||
ARGUMENT_PROMPT_LISTENER,
|
||||
makeListenerForArgumentPrompt(this.commandDispatcher)
|
||||
makeListenerForArgumentPrompt(
|
||||
this.commandRoomID,
|
||||
this.commandDispatcher,
|
||||
this.reactionHandler
|
||||
)
|
||||
);
|
||||
this.reactionHandler.on(
|
||||
DEFAUILT_ARGUMENT_PROMPT_LISTENER,
|
||||
makeListenerForPromptDefault(this.commandDispatcher)
|
||||
makeListenerForPromptDefault(
|
||||
this.commandRoomID,
|
||||
this.commandDispatcher,
|
||||
this.reactionHandler
|
||||
)
|
||||
);
|
||||
this.capabilityMessageRenderer = new DraupnirRendererMessageCollector(
|
||||
this.clientPlatform.toRoomMessageSender(),
|
||||
|
||||
+58
-23
@@ -18,6 +18,7 @@ import {
|
||||
Logger,
|
||||
ActionException,
|
||||
ActionExceptionKind,
|
||||
ConfigRecoverableError,
|
||||
} from "matrix-protection-suite";
|
||||
import {
|
||||
BotSDKLogServiceLogger,
|
||||
@@ -39,14 +40,16 @@ import {
|
||||
StringUserID,
|
||||
MatrixRoomID,
|
||||
} from "@the-draupnir-project/matrix-basic-types";
|
||||
import { Result, isError } from "@gnuxie/typescript-result";
|
||||
import { Err, Ok, Result, isError } from "@gnuxie/typescript-result";
|
||||
import {
|
||||
DraupnirRestartError,
|
||||
SafeModeToggle,
|
||||
SafeModeToggleOptions,
|
||||
} from "./safemode/SafeModeToggle";
|
||||
import { SafeModeDraupnir } from "./safemode/DraupnirSafeMode";
|
||||
import { ResultError } from "@gnuxie/typescript-result";
|
||||
import { SafeModeCause, SafeModeReason } from "./safemode/SafeModeCause";
|
||||
import { SafeModeBootOption } from "./safemode/BootOption";
|
||||
|
||||
setGlobalLoggerProvider(new BotSDKLogServiceLogger());
|
||||
|
||||
@@ -70,6 +73,10 @@ interface BotModeTogle extends SafeModeToggle {
|
||||
startFromScratch(
|
||||
options?: SafeModeToggleOptions
|
||||
): Promise<Result<Draupnir | SafeModeDraupnir>>;
|
||||
maybeRecoverWithSafeMode(
|
||||
error: ResultError,
|
||||
options?: SafeModeToggleOptions
|
||||
): Promise<Result<SafeModeDraupnir>>;
|
||||
}
|
||||
|
||||
export class DraupnirBotModeToggle implements BotModeTogle {
|
||||
@@ -168,12 +175,13 @@ export class DraupnirBotModeToggle implements BotModeTogle {
|
||||
}
|
||||
public async switchToDraupnir(
|
||||
options?: SafeModeToggleOptions
|
||||
): Promise<Result<Draupnir>> {
|
||||
): Promise<Result<Draupnir, DraupnirRestartError | ResultError>> {
|
||||
if (this.draupnir !== null) {
|
||||
return ResultError.Result(
|
||||
`There is a draupnir for ${this.clientUserID} already running`
|
||||
);
|
||||
}
|
||||
this.stopSafeModeDraupnir();
|
||||
const draupnirResult = await this.draupnirFactory.makeDraupnir(
|
||||
this.clientUserID,
|
||||
this.managementRoom,
|
||||
@@ -181,7 +189,18 @@ export class DraupnirBotModeToggle implements BotModeTogle {
|
||||
this
|
||||
);
|
||||
if (isError(draupnirResult)) {
|
||||
return draupnirResult;
|
||||
const safeModeResult = await this.maybeRecoverWithSafeMode(
|
||||
draupnirResult.error,
|
||||
options
|
||||
);
|
||||
if (isError(safeModeResult)) {
|
||||
return safeModeResult;
|
||||
} else {
|
||||
return DraupnirRestartError.Result(
|
||||
"Draupnir failed to start, switching to safe mode.",
|
||||
{ safeModeDraupnir: safeModeResult.ok }
|
||||
);
|
||||
}
|
||||
}
|
||||
this.draupnir = draupnirResult.ok;
|
||||
this.draupnir.start();
|
||||
@@ -192,7 +211,7 @@ export class DraupnirBotModeToggle implements BotModeTogle {
|
||||
await this.webAPIs.start();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
this.stopDraupnir();
|
||||
await this.stopDraupnir();
|
||||
log.error("Failed to start webAPIs", e);
|
||||
return ActionException.Result("Failed to start webAPIs", {
|
||||
exceptionKind: ActionExceptionKind.Unknown,
|
||||
@@ -203,7 +222,6 @@ export class DraupnirBotModeToggle implements BotModeTogle {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.stopSafeModeDraupnir();
|
||||
return draupnirResult;
|
||||
}
|
||||
public async switchToSafeMode(
|
||||
@@ -225,7 +243,7 @@ export class DraupnirBotModeToggle implements BotModeTogle {
|
||||
if (isError(safeModeResult)) {
|
||||
return safeModeResult;
|
||||
}
|
||||
this.stopDraupnir();
|
||||
await this.stopDraupnir();
|
||||
this.safeModeDraupnir = safeModeResult.ok;
|
||||
this.safeModeDraupnir.start();
|
||||
if (options?.sendStatusOnStart) {
|
||||
@@ -239,18 +257,8 @@ export class DraupnirBotModeToggle implements BotModeTogle {
|
||||
): Promise<Result<Draupnir | SafeModeDraupnir>> {
|
||||
const draupnirResult = await this.switchToDraupnir(options ?? {});
|
||||
if (isError(draupnirResult)) {
|
||||
if (this.config.safeMode?.bootIntoOnStartupFailure) {
|
||||
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 ?? {}
|
||||
);
|
||||
if (draupnirResult.error instanceof DraupnirRestartError) {
|
||||
return Ok(draupnirResult.error.safeModeDraupnir);
|
||||
} else {
|
||||
return draupnirResult;
|
||||
}
|
||||
@@ -258,6 +266,33 @@ export class DraupnirBotModeToggle implements BotModeTogle {
|
||||
return draupnirResult;
|
||||
}
|
||||
|
||||
public async maybeRecoverWithSafeMode(
|
||||
error: ResultError,
|
||||
options?: SafeModeToggleOptions | undefined
|
||||
): Promise<Result<SafeModeDraupnir>> {
|
||||
switch (this.config.safeMode?.bootOption) {
|
||||
case SafeModeBootOption.Never:
|
||||
return Err(error);
|
||||
case SafeModeBootOption.RecoveryOnly:
|
||||
if (!(error instanceof ConfigRecoverableError)) {
|
||||
return Err(error);
|
||||
}
|
||||
// fallthrough
|
||||
default:
|
||||
log.error(
|
||||
"Failed to start draupnir, switching to safe mode as configured",
|
||||
error
|
||||
);
|
||||
return await this.switchToSafeMode(
|
||||
{
|
||||
reason: SafeModeReason.InitializationError,
|
||||
error: error,
|
||||
},
|
||||
options ?? {}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async encryptionInitialized(): Promise<void> {
|
||||
if (this.draupnir !== null) {
|
||||
try {
|
||||
@@ -265,7 +300,7 @@ export class DraupnirBotModeToggle implements BotModeTogle {
|
||||
await this.webAPIs.start();
|
||||
await this.draupnir.startupComplete();
|
||||
} catch (e) {
|
||||
this.stopEverything();
|
||||
await this.stopEverything();
|
||||
throw e;
|
||||
}
|
||||
} else if (this.safeModeDraupnir !== null) {
|
||||
@@ -273,10 +308,10 @@ export class DraupnirBotModeToggle implements BotModeTogle {
|
||||
}
|
||||
}
|
||||
|
||||
private stopDraupnir(): void {
|
||||
private async stopDraupnir(): Promise<void> {
|
||||
this.draupnir?.stop();
|
||||
this.draupnir = null;
|
||||
this.webAPIs?.stop();
|
||||
await this.webAPIs?.stop();
|
||||
this.webAPIs = null;
|
||||
}
|
||||
|
||||
@@ -285,8 +320,8 @@ export class DraupnirBotModeToggle implements BotModeTogle {
|
||||
this.safeModeDraupnir = null;
|
||||
}
|
||||
|
||||
public stopEverything(): void {
|
||||
this.stopDraupnir();
|
||||
public async stopEverything(): Promise<void> {
|
||||
await this.stopDraupnir();
|
||||
this.stopSafeModeDraupnir();
|
||||
}
|
||||
}
|
||||
@@ -75,11 +75,19 @@ export class AppserviceCommandHandler {
|
||||
);
|
||||
this.reactionHandler.on(
|
||||
ARGUMENT_PROMPT_LISTENER,
|
||||
makeListenerForArgumentPrompt(this.commandDispatcher)
|
||||
makeListenerForArgumentPrompt(
|
||||
this.adminRoomID,
|
||||
this.commandDispatcher,
|
||||
this.reactionHandler
|
||||
)
|
||||
);
|
||||
this.reactionHandler.on(
|
||||
DEFAUILT_ARGUMENT_PROMPT_LISTENER,
|
||||
makeListenerForPromptDefault(this.commandDispatcher)
|
||||
makeListenerForPromptDefault(
|
||||
this.adminRoomID,
|
||||
this.commandDispatcher,
|
||||
this.reactionHandler
|
||||
)
|
||||
);
|
||||
this.JSInterfaceDispatcher = makeAppserviceJSCommandDispatcher(
|
||||
this.appserviceContext
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Logger, Value, isError } from "matrix-protection-suite";
|
||||
import { Logger, Task, Value, isError } from "matrix-protection-suite";
|
||||
import {
|
||||
MatrixReactionHandler,
|
||||
ReactionListener,
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
readCommand,
|
||||
} from "@the-draupnir-project/interface-manager";
|
||||
import { Ok, Result } from "@gnuxie/typescript-result";
|
||||
import { StringRoomID } from "@the-draupnir-project/matrix-basic-types";
|
||||
|
||||
const log = new Logger("MatrixPromptForAccept");
|
||||
|
||||
@@ -49,7 +50,8 @@ function continueCommandAcceptingPrompt(
|
||||
eventContext: MatrixEventContext,
|
||||
promptContext: PromptContext,
|
||||
serializedPrompt: string,
|
||||
commandDispatcher: MatrixInterfaceCommandDispatcher<MatrixEventContext>
|
||||
commandDispatcher: MatrixInterfaceCommandDispatcher<MatrixEventContext>,
|
||||
reactionHandler: MatrixReactionHandler
|
||||
): void {
|
||||
const stream = new StandardPresentationArgumentStream(
|
||||
readCommand(
|
||||
@@ -61,14 +63,25 @@ function continueCommandAcceptingPrompt(
|
||||
)
|
||||
);
|
||||
commandDispatcher.handleCommandFromPresentationStream(eventContext, stream);
|
||||
void Task(
|
||||
reactionHandler.completePrompt(
|
||||
eventContext.roomID,
|
||||
eventContext.event.event_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const DEFAUILT_ARGUMENT_PROMPT_LISTENER =
|
||||
"ge.applied-langua.ge.draupnir.default_argument_prompt";
|
||||
export function makeListenerForPromptDefault(
|
||||
commandDispatcher: MatrixInterfaceCommandDispatcher<MatrixEventContext>
|
||||
commandRoomID: StringRoomID,
|
||||
commandDispatcher: MatrixInterfaceCommandDispatcher<MatrixEventContext>,
|
||||
reactionHandler: MatrixReactionHandler
|
||||
): ReactionListener {
|
||||
return function (reactionKey, item, context, reactionMap, annotatedEvent) {
|
||||
if (annotatedEvent.room_id !== commandRoomID) {
|
||||
return;
|
||||
}
|
||||
if (item !== "ok") {
|
||||
return;
|
||||
}
|
||||
@@ -84,7 +97,8 @@ export function makeListenerForPromptDefault(
|
||||
{ event: annotatedEvent, roomID: annotatedEvent.room_id },
|
||||
promptContext.ok,
|
||||
item,
|
||||
commandDispatcher
|
||||
commandDispatcher,
|
||||
reactionHandler
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -92,9 +106,14 @@ export function makeListenerForPromptDefault(
|
||||
export const ARGUMENT_PROMPT_LISTENER =
|
||||
"ge.applied-langua.ge.draupnir.argument_prompt";
|
||||
export function makeListenerForArgumentPrompt(
|
||||
commandDispatcher: MatrixInterfaceCommandDispatcher<MatrixEventContext>
|
||||
commandRoomID: StringRoomID,
|
||||
commandDispatcher: MatrixInterfaceCommandDispatcher<MatrixEventContext>,
|
||||
reactionHandler: MatrixReactionHandler
|
||||
): ReactionListener {
|
||||
return function (reactionKey, item, context, reactionMap, annotatedEvent) {
|
||||
if (annotatedEvent.room_id !== commandRoomID) {
|
||||
return;
|
||||
}
|
||||
const promptContext = Value.Decode(PromptContext, context);
|
||||
if (isError(promptContext)) {
|
||||
log.error(
|
||||
@@ -107,7 +126,8 @@ export function makeListenerForArgumentPrompt(
|
||||
{ event: annotatedEvent, roomID: annotatedEvent.room_id },
|
||||
promptContext.ok,
|
||||
item,
|
||||
commandDispatcher
|
||||
commandDispatcher,
|
||||
reactionHandler
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
+4
-2
@@ -13,6 +13,7 @@ import { load } from "js-yaml";
|
||||
import { MatrixClient, LogService } from "matrix-bot-sdk";
|
||||
import Config from "config";
|
||||
import path from "path";
|
||||
import { SafeModeBootOption } from "./safemode/BootOption";
|
||||
|
||||
/**
|
||||
* The configuration, as read from production.yaml
|
||||
@@ -90,7 +91,7 @@ export interface IConfig {
|
||||
};
|
||||
};
|
||||
safeMode?: {
|
||||
bootIntoOnStartupFailure: boolean;
|
||||
bootOption: SafeModeBootOption;
|
||||
};
|
||||
health: {
|
||||
healthz: {
|
||||
@@ -192,7 +193,7 @@ const defaultConfig: IConfig = {
|
||||
},
|
||||
},
|
||||
safeMode: {
|
||||
bootIntoOnStartupFailure: false,
|
||||
bootOption: SafeModeBootOption.RecoveryOnly,
|
||||
},
|
||||
health: {
|
||||
healthz: {
|
||||
@@ -287,6 +288,7 @@ export function getProvisionedMjolnirConfig(managementRoomId: string): IConfig {
|
||||
"automaticallyRedactForReasons",
|
||||
"protectAllJoinedRooms",
|
||||
"backgroundDelayMS",
|
||||
"safeMode",
|
||||
];
|
||||
const configTemplate = read(); // we use the standard bot config as a template for every provisioned mjolnir.
|
||||
const unusedKeys = Object.keys(configTemplate).filter(
|
||||
|
||||
@@ -16,14 +16,11 @@ import {
|
||||
MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE,
|
||||
MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE,
|
||||
MissingProtectionCB,
|
||||
MjolnirEnabledProtectionsEvent,
|
||||
MjolnirEnabledProtectionsEventType,
|
||||
MjolnirPolicyRoomsConfig,
|
||||
MjolnirProtectedRoomsConfig,
|
||||
MjolnirProtectedRoomsEvent,
|
||||
MjolnirProtectionSettingsEventType,
|
||||
MjolnirProtectionsConfig,
|
||||
MjolnirWatchedPolicyRoomsEvent,
|
||||
Ok,
|
||||
PolicyListConfig,
|
||||
PolicyRoomManager,
|
||||
@@ -42,7 +39,7 @@ import {
|
||||
isError,
|
||||
} from "matrix-protection-suite";
|
||||
import {
|
||||
BotSDKMatrixAccountData,
|
||||
BotSDKAccountDataConfigBackend,
|
||||
BotSDKMatrixStateData,
|
||||
MatrixSendClient,
|
||||
} from "matrix-protection-suite-for-matrix-bot-sdk";
|
||||
@@ -64,10 +61,9 @@ async function makePolicyListConfig(
|
||||
roomJoiner: RoomJoiner
|
||||
): Promise<ActionResult<PolicyListConfig>> {
|
||||
const result = await MjolnirPolicyRoomsConfig.createFromStore(
|
||||
new BotSDKMatrixAccountData(
|
||||
MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE,
|
||||
MjolnirWatchedPolicyRoomsEvent,
|
||||
client
|
||||
new BotSDKAccountDataConfigBackend(
|
||||
client,
|
||||
MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE
|
||||
),
|
||||
policyRoomManager,
|
||||
roomJoiner
|
||||
@@ -81,10 +77,9 @@ async function makeProtectedRoomsConfig(
|
||||
loggableConfigTracker: LoggableConfigTracker
|
||||
): Promise<ActionResult<ProtectedRoomsConfig>> {
|
||||
return await MjolnirProtectedRoomsConfig.createFromStore(
|
||||
new BotSDKMatrixAccountData(
|
||||
MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE,
|
||||
MjolnirProtectedRoomsEvent,
|
||||
client
|
||||
new BotSDKAccountDataConfigBackend(
|
||||
client,
|
||||
MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE
|
||||
),
|
||||
roomResolver,
|
||||
loggableConfigTracker
|
||||
@@ -116,10 +111,9 @@ async function makeProtectionsManager(
|
||||
return result;
|
||||
}
|
||||
const protectionsConfigResult = await MjolnirProtectionsConfig.create(
|
||||
new BotSDKMatrixAccountData<MjolnirEnabledProtectionsEvent>(
|
||||
MjolnirEnabledProtectionsEventType,
|
||||
MjolnirEnabledProtectionsEvent,
|
||||
client
|
||||
new BotSDKAccountDataConfigBackend(
|
||||
client,
|
||||
MjolnirEnabledProtectionsEventType
|
||||
),
|
||||
loggableConfigTracker,
|
||||
{
|
||||
|
||||
@@ -8,7 +8,13 @@
|
||||
// https://github.com/matrix-org/mjolnir
|
||||
// </text>
|
||||
|
||||
import { ActionError, ActionResult, isError } from "matrix-protection-suite";
|
||||
import {
|
||||
ActionError,
|
||||
ActionResult,
|
||||
ConfigRecoverableError,
|
||||
Logger,
|
||||
isError,
|
||||
} from "matrix-protection-suite";
|
||||
import { IConfig } from "../config";
|
||||
import { DraupnirFactory } from "./DraupnirFactory";
|
||||
import { Draupnir } from "../Draupnir";
|
||||
@@ -17,8 +23,15 @@ import {
|
||||
MatrixRoomID,
|
||||
} from "@the-draupnir-project/matrix-basic-types";
|
||||
import { SafeModeDraupnir } from "../safemode/DraupnirSafeMode";
|
||||
import { SafeModeCause } from "../safemode/SafeModeCause";
|
||||
import { SafeModeToggle } from "../safemode/SafeModeToggle";
|
||||
import { SafeModeCause, SafeModeReason } from "../safemode/SafeModeCause";
|
||||
import {
|
||||
DraupnirRestartError,
|
||||
SafeModeToggle,
|
||||
} from "../safemode/SafeModeToggle";
|
||||
import { ResultError } from "@gnuxie/typescript-result";
|
||||
import { SafeModeBootOption } from "../safemode/BootOption";
|
||||
|
||||
const log = new Logger("StandardDraupnirManager");
|
||||
|
||||
export class StandardDraupnirManager {
|
||||
private readonly draupnir = new Map<StringUserID, Draupnir>();
|
||||
@@ -61,7 +74,8 @@ export class StandardDraupnirManager {
|
||||
clientUserID: StringUserID,
|
||||
managementRoom: MatrixRoomID,
|
||||
config: IConfig
|
||||
): Promise<ActionResult<Draupnir>> {
|
||||
): Promise<ActionResult<Draupnir, DraupnirRestartError | ResultError>> {
|
||||
this.safeModeDraupnir.delete(clientUserID);
|
||||
const draupnir = await this.draupnirFactory.makeDraupnir(
|
||||
clientUserID,
|
||||
managementRoom,
|
||||
@@ -79,11 +93,41 @@ export class StandardDraupnirManager {
|
||||
draupnir.error,
|
||||
clientUserID
|
||||
);
|
||||
return draupnir;
|
||||
switch (config.safeMode?.bootOption) {
|
||||
case SafeModeBootOption.Never:
|
||||
return draupnir;
|
||||
case SafeModeBootOption.RecoveryOnly:
|
||||
if (!(draupnir.error instanceof ConfigRecoverableError)) {
|
||||
return draupnir;
|
||||
}
|
||||
// fallthrough
|
||||
default: {
|
||||
log.error(
|
||||
`Failed to start draupnir ${clientUserID}, switching to safe mode as configured`,
|
||||
draupnir.error
|
||||
);
|
||||
const safeModeResult = await this.makeSafeModeDraupnir(
|
||||
clientUserID,
|
||||
managementRoom,
|
||||
config,
|
||||
{
|
||||
reason: SafeModeReason.InitializationError,
|
||||
error: draupnir.error,
|
||||
}
|
||||
);
|
||||
if (isError(safeModeResult)) {
|
||||
return safeModeResult;
|
||||
} else {
|
||||
return DraupnirRestartError.Result(
|
||||
`Failed to start draupnir ${clientUserID}, switching to safe mode as configured`,
|
||||
{ safeModeDraupnir: safeModeResult.ok }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.draupnir.set(clientUserID, draupnir.ok);
|
||||
this.failedDraupnir.delete(clientUserID);
|
||||
this.safeModeDraupnir.delete(clientUserID);
|
||||
draupnir.ok.start();
|
||||
return draupnir;
|
||||
}
|
||||
|
||||
+2
-2
@@ -111,7 +111,7 @@ void (async function () {
|
||||
console.error(
|
||||
`Failed to setup mjolnir from the config ${config.dataPath}: ${err}`
|
||||
);
|
||||
bot?.stopEverything();
|
||||
await bot?.stopEverything();
|
||||
throw err;
|
||||
}
|
||||
try {
|
||||
@@ -120,7 +120,7 @@ void (async function () {
|
||||
healthz.isHealthy = true;
|
||||
} catch (err) {
|
||||
console.error(`Mjolnir failed to start: ${err}`);
|
||||
bot.stopEverything();
|
||||
await bot.stopEverything();
|
||||
throw err;
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,9 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
export enum SafeModeBootOption {
|
||||
RecoveryOnly = "RecoveryOnly",
|
||||
AllErrors = "AllErrors",
|
||||
Never = "Never",
|
||||
}
|
||||
@@ -9,10 +9,7 @@ import {
|
||||
RoomEvent,
|
||||
Task,
|
||||
} from "matrix-protection-suite";
|
||||
import {
|
||||
MatrixAdaptorContext,
|
||||
sendMatrixEventsFromDeadDocument,
|
||||
} from "../commands/interface-manager/MPSMatrixInterfaceAdaptor";
|
||||
import { MatrixAdaptorContext } from "../commands/interface-manager/MPSMatrixInterfaceAdaptor";
|
||||
import {
|
||||
StringUserID,
|
||||
StringRoomID,
|
||||
@@ -40,6 +37,7 @@ import {
|
||||
safeModeStatusInfo,
|
||||
} from "./commands/StatusCommand";
|
||||
import { wrapInRoot } from "../commands/interface-manager/MatrixHelpRenderer";
|
||||
import { sendAndAnnotateWithRecoveryOptions } from "./commands/RecoverCommand";
|
||||
|
||||
export class SafeModeDraupnir implements MatrixAdaptorContext {
|
||||
public reactionHandler: MatrixReactionHandler;
|
||||
@@ -73,16 +71,29 @@ export class SafeModeDraupnir implements MatrixAdaptorContext {
|
||||
);
|
||||
this.reactionHandler.on(
|
||||
ARGUMENT_PROMPT_LISTENER,
|
||||
makeListenerForArgumentPrompt(this.commandDispatcher)
|
||||
makeListenerForArgumentPrompt(
|
||||
this.commandRoomID,
|
||||
this.commandDispatcher,
|
||||
this.reactionHandler
|
||||
)
|
||||
);
|
||||
this.reactionHandler.on(
|
||||
DEFAUILT_ARGUMENT_PROMPT_LISTENER,
|
||||
makeListenerForPromptDefault(this.commandDispatcher)
|
||||
makeListenerForPromptDefault(
|
||||
this.commandRoomID,
|
||||
this.commandDispatcher,
|
||||
this.reactionHandler
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
handleTimelineEvent(roomID: StringRoomID, event: RoomEvent): void {
|
||||
this.commandDispatcherTimelineListener(roomID, event);
|
||||
void Task(
|
||||
(async () => {
|
||||
await this.reactionHandler.handleEvent(roomID, event);
|
||||
})()
|
||||
);
|
||||
}
|
||||
handleEventReport(_report: EventReport): void {
|
||||
throw new Error("Method not implemented.");
|
||||
@@ -107,12 +118,11 @@ export class SafeModeDraupnir implements MatrixAdaptorContext {
|
||||
|
||||
public startupComplete(): void {
|
||||
void Task(
|
||||
sendMatrixEventsFromDeadDocument(
|
||||
this.clientPlatform.toRoomMessageSender(),
|
||||
this.commandRoomID,
|
||||
sendAndAnnotateWithRecoveryOptions(
|
||||
this,
|
||||
wrapInRoot(renderSafeModeStatusInfo(safeModeStatusInfo(this))),
|
||||
{}
|
||||
) as Promise<Result<void>>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
import {
|
||||
DeadDocumentJSX,
|
||||
DocumentNode,
|
||||
} from "@the-draupnir-project/interface-manager";
|
||||
import { ConfigRecoverableError } from "matrix-protection-suite";
|
||||
import { SafeModeCause, SafeModeReason } from "./SafeModeCause";
|
||||
|
||||
export function renderRecoveryOptions(cause: SafeModeCause): DocumentNode {
|
||||
const recoveryOptions =
|
||||
cause.reason === SafeModeReason.ByRequest
|
||||
? []
|
||||
: cause.error instanceof ConfigRecoverableError
|
||||
? cause.error.recoveryOptions
|
||||
: [];
|
||||
if (recoveryOptions.length === 0) {
|
||||
return (
|
||||
<fragment>
|
||||
No recovery options are available for this failure mode.
|
||||
</fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<fragment>
|
||||
Recovery options are available for this failure mode:
|
||||
<ol>
|
||||
{recoveryOptions.map((option) => (
|
||||
<li>{option.description}</li>
|
||||
))}
|
||||
</ol>
|
||||
</fragment>
|
||||
);
|
||||
}
|
||||
@@ -2,13 +2,29 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
import { Result } from "@gnuxie/typescript-result";
|
||||
import { Err, Result, ResultError } from "@gnuxie/typescript-result";
|
||||
import { Draupnir } from "../Draupnir";
|
||||
import { SafeModeDraupnir } from "./DraupnirSafeMode";
|
||||
import { SafeModeCause } from "./SafeModeCause";
|
||||
|
||||
export type SafeModeToggleOptions = { sendStatusOnStart?: boolean };
|
||||
|
||||
export class DraupnirRestartError extends ResultError {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly safeModeDraupnir: SafeModeDraupnir
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public static Result(
|
||||
message: string,
|
||||
options: { safeModeDraupnir: SafeModeDraupnir }
|
||||
): Result<never> {
|
||||
return Err(new DraupnirRestartError(message, options.safeModeDraupnir));
|
||||
}
|
||||
}
|
||||
|
||||
export interface SafeModeToggle {
|
||||
/**
|
||||
* Switch the bot to Draupnir mode.
|
||||
@@ -17,7 +33,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(options?: SafeModeToggleOptions): Promise<Result<Draupnir>>;
|
||||
switchToDraupnir(
|
||||
options?: SafeModeToggleOptions
|
||||
): Promise<Result<Draupnir, DraupnirRestartError | ResultError>>;
|
||||
switchToSafeMode(
|
||||
cause: SafeModeCause,
|
||||
options?: SafeModeToggleOptions
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
import { Ok, Result, ResultError, isError } from "@gnuxie/typescript-result";
|
||||
import {
|
||||
DeadDocumentJSX,
|
||||
DocumentNode,
|
||||
StringPresentationType,
|
||||
describeCommand,
|
||||
tuple,
|
||||
} from "@the-draupnir-project/interface-manager";
|
||||
import { SafeModeDraupnir } from "../DraupnirSafeMode";
|
||||
import { SafeModeReason } from "../SafeModeCause";
|
||||
import {
|
||||
ConfigRecoverableError,
|
||||
ConfigRecoveryOption,
|
||||
RoomEvent,
|
||||
} from "matrix-protection-suite";
|
||||
import { SafeModeInterfaceAdaptor } from "./SafeModeAdaptor";
|
||||
import { sendMatrixEventsFromDeadDocument } from "../../commands/interface-manager/MPSMatrixInterfaceAdaptor";
|
||||
import { ARGUMENT_PROMPT_LISTENER } from "../../commands/interface-manager/MatrixPromptForAccept";
|
||||
import { MatrixReactionHandler } from "../../commands/interface-manager/MatrixReactionHandler";
|
||||
|
||||
export const SafeModeRecoverCommand = describeCommand({
|
||||
summary: "Select an availale recovery option to enact.",
|
||||
parameters: tuple({
|
||||
name: "recovery option",
|
||||
acceptor: StringPresentationType,
|
||||
description: "The recovery option to enact, e.g. 1.",
|
||||
}),
|
||||
async executor(
|
||||
safeModeDraupnir: SafeModeDraupnir,
|
||||
_info,
|
||||
_keywords,
|
||||
_rest,
|
||||
optionDesignator
|
||||
): Promise<Result<ConfigRecoveryOption>> {
|
||||
const optionNumber = (() => {
|
||||
try {
|
||||
return Ok(parseInt(optionDesignator));
|
||||
} catch {
|
||||
return ResultError.Result(
|
||||
`Unable to parse the recovery option from ${optionDesignator}`
|
||||
);
|
||||
}
|
||||
})();
|
||||
if (isError(optionNumber)) {
|
||||
return optionNumber;
|
||||
}
|
||||
const options = getRecoveryOptions(safeModeDraupnir);
|
||||
const selectedOption = options[optionNumber.ok - 1];
|
||||
if (selectedOption === undefined) {
|
||||
return ResultError.Result(
|
||||
`No recovery option with the number ${optionNumber.ok} exists.`
|
||||
);
|
||||
}
|
||||
const recoveryResult = await selectedOption.recover();
|
||||
if (isError(recoveryResult)) {
|
||||
return recoveryResult;
|
||||
}
|
||||
return Ok(selectedOption);
|
||||
},
|
||||
});
|
||||
|
||||
SafeModeInterfaceAdaptor.describeRenderer(SafeModeRecoverCommand, {
|
||||
JSXRenderer(result) {
|
||||
if (isError(result)) {
|
||||
return Ok(undefined);
|
||||
}
|
||||
return Ok(
|
||||
<root>
|
||||
<fragment>
|
||||
The recovery following recovery option has sucessfully been applied:
|
||||
<br />
|
||||
{result.ok.description}
|
||||
<br />
|
||||
You may now restart Draupnir with <code>!draupnir restart</code>
|
||||
</fragment>
|
||||
</root>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export function getRecoveryOptions(
|
||||
safeModeDraupnir: SafeModeDraupnir
|
||||
): ConfigRecoveryOption[] {
|
||||
return safeModeDraupnir.cause.reason === SafeModeReason.ByRequest
|
||||
? []
|
||||
: safeModeDraupnir.cause.error instanceof ConfigRecoverableError
|
||||
? safeModeDraupnir.cause.error.recoveryOptions
|
||||
: [];
|
||||
}
|
||||
|
||||
export async function sendAndAnnotateWithRecoveryOptions(
|
||||
safeModeDraupnir: SafeModeDraupnir,
|
||||
document: DocumentNode,
|
||||
{ replyToEvent }: { replyToEvent?: RoomEvent }
|
||||
): Promise<Result<void>> {
|
||||
const reactionMap = MatrixReactionHandler.createItemizedReactionMap(
|
||||
getRecoveryOptions(safeModeDraupnir).map((_option, index) =>
|
||||
String(index + 1)
|
||||
)
|
||||
);
|
||||
const sendResult = await sendMatrixEventsFromDeadDocument(
|
||||
safeModeDraupnir.clientPlatform.toRoomMessageSender(),
|
||||
safeModeDraupnir.commandRoomID,
|
||||
document,
|
||||
{
|
||||
replyToEvent,
|
||||
additionalContent: safeModeDraupnir.reactionHandler.createAnnotation(
|
||||
ARGUMENT_PROMPT_LISTENER,
|
||||
reactionMap,
|
||||
{
|
||||
command_designator: ["draupnir", "recover"],
|
||||
read_items: [],
|
||||
}
|
||||
),
|
||||
}
|
||||
);
|
||||
if (isError(sendResult)) {
|
||||
return sendResult;
|
||||
}
|
||||
if (sendResult.ok[0] === undefined) {
|
||||
throw new TypeError(`Something is really wrong with the code`);
|
||||
}
|
||||
await safeModeDraupnir.reactionHandler.addReactionsToEvent(
|
||||
safeModeDraupnir.client,
|
||||
safeModeDraupnir.commandRoomID,
|
||||
sendResult.ok[0],
|
||||
reactionMap
|
||||
);
|
||||
return Ok(undefined);
|
||||
}
|
||||
@@ -6,8 +6,10 @@ import { StandardCommandTable } from "@the-draupnir-project/interface-manager";
|
||||
import { SafeModeHelpCommand } from "./HelpCommand";
|
||||
import { SafeModeStatusCommand } from "./StatusCommand";
|
||||
import { SafeModeRestartCommand } from "./RestartDraupnirCommand";
|
||||
import { SafeModeRecoverCommand } from "./RecoverCommand";
|
||||
|
||||
export const SafeModeCommands = new StandardCommandTable("safe mode")
|
||||
.internCommand(SafeModeHelpCommand, ["draupnir", "help"])
|
||||
.internCommand(SafeModeStatusCommand, ["draupnir", "status"])
|
||||
.internCommand(SafeModeRecoverCommand, ["draupnir", "recover"])
|
||||
.internCommand(SafeModeRestartCommand, ["draupnir", "restart"]);
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
SOFTWARE_VERSION,
|
||||
} from "../../config";
|
||||
import { SafeModeInterfaceAdaptor } from "./SafeModeAdaptor";
|
||||
import { renderRecoveryOptions } from "../RecoveryOptions";
|
||||
import { sendAndAnnotateWithRecoveryOptions } from "./RecoverCommand";
|
||||
|
||||
export function safeModeHeader(): DocumentNode {
|
||||
return (
|
||||
@@ -84,6 +86,8 @@ export function renderSafeModeStatusInfo(
|
||||
<br />
|
||||
</span>
|
||||
<br />
|
||||
{renderRecoveryOptions(info.safeModeCause)}
|
||||
<br />
|
||||
<b>Version: </b>
|
||||
<code>{info.version}</code>
|
||||
<br />
|
||||
@@ -128,10 +132,14 @@ export const SafeModeStatusCommand = describeCommand({
|
||||
});
|
||||
|
||||
SafeModeInterfaceAdaptor.describeRenderer(SafeModeStatusCommand, {
|
||||
JSXRenderer(result) {
|
||||
if (isError(result)) {
|
||||
async arbritraryRenderer(context, eventContext, commandResult) {
|
||||
if (isError(commandResult)) {
|
||||
return Ok(undefined);
|
||||
}
|
||||
return Ok(<root>{renderSafeModeStatusInfo(result.ok)}</root>);
|
||||
return await sendAndAnnotateWithRecoveryOptions(
|
||||
context,
|
||||
<root>{renderSafeModeStatusInfo(commandResult.ok)}</root>,
|
||||
{ replyToEvent: eventContext.event }
|
||||
);
|
||||
},
|
||||
});
|
||||
+37
-16
@@ -17,7 +17,9 @@ import {
|
||||
StringRoomID,
|
||||
StringEventID,
|
||||
} from "@the-draupnir-project/matrix-basic-types";
|
||||
import { Task } from "matrix-protection-suite";
|
||||
import { Logger, Task } from "matrix-protection-suite";
|
||||
|
||||
const log = new Logger("WebAPIs");
|
||||
|
||||
/**
|
||||
* A common prefix for all web-exposed APIs.
|
||||
@@ -41,18 +43,22 @@ export class WebAPIs {
|
||||
/**
|
||||
* Start accepting requests to the Web API.
|
||||
*/
|
||||
public async start() {
|
||||
public async start(): Promise<void> {
|
||||
if (!this.config.web.enabled) {
|
||||
return;
|
||||
}
|
||||
this.httpServer = this.webController.listen(
|
||||
this.config.web.port,
|
||||
this.config.web.address
|
||||
);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
this.httpServer = this.webController.listen(
|
||||
this.config.web.port,
|
||||
this.config.web.address,
|
||||
() => {
|
||||
resolve(undefined);
|
||||
}
|
||||
);
|
||||
});
|
||||
// configure /report API.
|
||||
if (this.config.web.abuseReporting.enabled) {
|
||||
console.log(`configuring ${API_PREFIX}/report/:room_id/:event_id...`);
|
||||
log.info(`configuring ${API_PREFIX}/report/:room_id/:event_id...`);
|
||||
this.webController.options(
|
||||
`${API_PREFIX}/report/:room_id/:event_id`,
|
||||
(request, response) => {
|
||||
@@ -70,7 +76,7 @@ export class WebAPIs {
|
||||
this.webController.post(
|
||||
`${API_PREFIX}/report/:room_id/:event_id`,
|
||||
(request, response) => {
|
||||
console.debug(
|
||||
log.debug(
|
||||
`Received a message on ${API_PREFIX}/report/:room_id/:event_id`,
|
||||
request.params
|
||||
);
|
||||
@@ -91,16 +97,31 @@ export class WebAPIs {
|
||||
);
|
||||
}
|
||||
);
|
||||
console.log(
|
||||
`configuring ${API_PREFIX}/report/:room_id/:event_id... DONE`
|
||||
);
|
||||
log.info(`configuring ${API_PREFIX}/report/:room_id/:event_id... DONE`);
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
public async stop(): Promise<void> {
|
||||
if (this.httpServer) {
|
||||
console.log("Stopping WebAPIs.");
|
||||
this.httpServer.close();
|
||||
log.info("Stopping WebAPIs.");
|
||||
await new Promise((resolve, reject) => {
|
||||
if (this.httpServer === undefined) {
|
||||
throw new TypeError("There is some kind of weird race going on here");
|
||||
}
|
||||
this.httpServer.close((error) => {
|
||||
if (error === undefined) {
|
||||
resolve(undefined);
|
||||
} else if (
|
||||
"code" in error &&
|
||||
error.code === "ERR_SERVER_NOT_RUNNING"
|
||||
) {
|
||||
resolve(undefined);
|
||||
} else {
|
||||
log.error("Error when stopping WebAPIs", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
this.httpServer = undefined;
|
||||
}
|
||||
}
|
||||
@@ -226,7 +247,7 @@ export class WebAPIs {
|
||||
// Match the spec behavior of `/report`: return 200 and an empty JSON.
|
||||
response.status(200).json({});
|
||||
} catch (ex) {
|
||||
console.warn("Error responding to an abuse report", roomID, eventID, ex);
|
||||
log.warn("Error responding to an abuse report", roomID, eventID, ex);
|
||||
response.status(503);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
import { StringUserID } from "@the-draupnir-project/matrix-basic-types";
|
||||
import { MjolnirAppService } from "../../../src/appservice/AppService";
|
||||
import { newTestUser } from "../../integration/clientHelper";
|
||||
import { StandardProvisionHelper } from "../utils/ProvisionHelper";
|
||||
import { setupHarness } from "../utils/harness";
|
||||
import { testRecoverAndRestart } from "../../integration/commands/recoverCommandDetail";
|
||||
|
||||
interface Context extends Mocha.Context {
|
||||
appservice?: MjolnirAppService;
|
||||
}
|
||||
|
||||
describe("Test safe mode recovery on a provisioned Draupnir", function () {
|
||||
beforeEach(async function (this: Context) {
|
||||
this.appservice = await setupHarness();
|
||||
});
|
||||
afterEach(function (this: Context) {
|
||||
if (this.appservice) {
|
||||
return this.appservice.close();
|
||||
} else {
|
||||
console.warn("Missing Appservice in this context, so cannot stop it.");
|
||||
return Promise.resolve(); // TS7030: Not all code paths return a value.
|
||||
}
|
||||
});
|
||||
it("Provisioned draupnir can be recovered from safe mode.", async function (this: Context) {
|
||||
const appservice = this.appservice;
|
||||
if (appservice === undefined) {
|
||||
throw new TypeError(`Test setup failed`);
|
||||
}
|
||||
const provisionHelper = new StandardProvisionHelper(appservice);
|
||||
const moderator = await newTestUser(appservice.config.homeserver.url, {
|
||||
name: { contains: "moderator" },
|
||||
});
|
||||
const moderatorUserID = (await moderator.getUserId()) as StringUserID;
|
||||
const initialDraupnir = (
|
||||
await provisionHelper.provisionDraupnir(moderatorUserID)
|
||||
).expect("Failed to provision a draupnir for the test");
|
||||
await testRecoverAndRestart(moderatorUserID, initialDraupnir);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
import {
|
||||
MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE,
|
||||
MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE,
|
||||
MjolnirEnabledProtectionsEventType,
|
||||
MjolnirProtectedRoomsEvent,
|
||||
} from "matrix-protection-suite";
|
||||
import { Draupnir } from "../../../src/Draupnir";
|
||||
import {
|
||||
StringRoomID,
|
||||
StringUserID,
|
||||
} from "@the-draupnir-project/matrix-basic-types";
|
||||
import { isOk } from "@gnuxie/typescript-result";
|
||||
import { SafeModeDraupnir } from "../../../src/safemode/DraupnirSafeMode";
|
||||
import { DraupnirRestartError } from "../../../src/safemode/SafeModeToggle";
|
||||
import { MatrixSendClient } from "matrix-protection-suite-for-matrix-bot-sdk";
|
||||
|
||||
async function clobberProtectedRooms(client: MatrixSendClient): Promise<void> {
|
||||
const existingAccountData =
|
||||
await client.getAccountData<MjolnirProtectedRoomsEvent>(
|
||||
MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE
|
||||
);
|
||||
existingAccountData.rooms.push("clobbered" as StringRoomID);
|
||||
await client.setAccountData(
|
||||
MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE,
|
||||
existingAccountData
|
||||
);
|
||||
}
|
||||
|
||||
async function clobberWatchedLists(client: MatrixSendClient): Promise<void> {
|
||||
await client.setAccountData(MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE, {
|
||||
references: "cheese",
|
||||
});
|
||||
}
|
||||
|
||||
async function clobberEnabledProtections(
|
||||
client: MatrixSendClient
|
||||
): Promise<void> {
|
||||
await client.setAccountData(MjolnirEnabledProtectionsEventType, {
|
||||
enabled: 34,
|
||||
});
|
||||
}
|
||||
|
||||
async function goToSafeMode(
|
||||
sender: StringUserID,
|
||||
draupnir: Draupnir
|
||||
): Promise<SafeModeDraupnir> {
|
||||
return (
|
||||
await draupnir.sendTextCommand<SafeModeDraupnir>(
|
||||
sender,
|
||||
"!draupnir safe mode"
|
||||
)
|
||||
).expect("Failed to switch to safe mode to setup the test");
|
||||
}
|
||||
|
||||
async function recoverAndRestart(
|
||||
sender: StringUserID,
|
||||
initialDraupnir: SafeModeDraupnir
|
||||
): Promise<Draupnir> {
|
||||
const badRestartResult = await initialDraupnir.sendTextCommand(
|
||||
sender,
|
||||
"!draupnir restart"
|
||||
);
|
||||
if (isOk(badRestartResult)) {
|
||||
throw new TypeError(
|
||||
"The restat command should have failed because we clobbered account data."
|
||||
);
|
||||
}
|
||||
const safeModeDraupnirWithRecoveryOptions = (() => {
|
||||
if (badRestartResult.error instanceof DraupnirRestartError) {
|
||||
return badRestartResult.error.safeModeDraupnir;
|
||||
} else {
|
||||
throw new TypeError("Expected a DraupnirRestartError");
|
||||
}
|
||||
})();
|
||||
(
|
||||
await safeModeDraupnirWithRecoveryOptions.sendTextCommand(
|
||||
sender,
|
||||
"!draupnir recover 1"
|
||||
)
|
||||
).expect("Failed to recover the draupnir");
|
||||
return (
|
||||
await safeModeDraupnirWithRecoveryOptions.sendTextCommand<Draupnir>(
|
||||
sender,
|
||||
"!draupnir restart"
|
||||
)
|
||||
).expect("Failed to restart the draupnir after recovery was attempted");
|
||||
}
|
||||
|
||||
type ClobberEffectDescription = {
|
||||
effect: (client: MatrixSendClient) => Promise<void>;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export async function testClobberEffect(
|
||||
sender: StringUserID,
|
||||
initialDraupnir: Draupnir,
|
||||
effect: ClobberEffectDescription
|
||||
): Promise<Draupnir> {
|
||||
console.log(`Testing clobber effect: ${effect.description}`);
|
||||
const initialSafeModeDraupnir = await goToSafeMode(sender, initialDraupnir);
|
||||
await effect.effect(initialSafeModeDraupnir.client);
|
||||
return await recoverAndRestart(sender, initialSafeModeDraupnir);
|
||||
}
|
||||
|
||||
async function testAllClobberEffects(
|
||||
sender: StringUserID,
|
||||
draupnir: Draupnir,
|
||||
effects: ClobberEffectDescription[]
|
||||
): Promise<void> {
|
||||
for (const effect of effects) {
|
||||
await testClobberEffect(sender, draupnir, effect);
|
||||
}
|
||||
}
|
||||
|
||||
export const ClobberEffects = [
|
||||
{
|
||||
effect: clobberProtectedRooms,
|
||||
description: "ProtectedRooms",
|
||||
},
|
||||
{
|
||||
effect: clobberWatchedLists,
|
||||
description: "WatchedLists",
|
||||
},
|
||||
{
|
||||
effect: clobberEnabledProtections,
|
||||
description: "EnabledProtections",
|
||||
},
|
||||
];
|
||||
|
||||
export async function testRecoverAndRestart(
|
||||
sender: StringUserID,
|
||||
draupnir: Draupnir
|
||||
): Promise<void> {
|
||||
await testAllClobberEffects(sender, draupnir, ClobberEffects);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
import { StringUserID } from "@the-draupnir-project/matrix-basic-types";
|
||||
import { newTestUser } from "../clientHelper";
|
||||
import { DraupnirTestContext, draupnir } from "../mjolnirSetupUtils";
|
||||
import { testRecoverAndRestart } from "./recoverCommandDetail";
|
||||
|
||||
describe("We should be able to restart and recover draupnir when it has bad account data", function () {
|
||||
it("Recovering protected rooms", async function (this: DraupnirTestContext) {
|
||||
const moderator = await newTestUser(this.config.homeserverUrl, {
|
||||
name: { contains: "moderator" },
|
||||
});
|
||||
await draupnir().client.inviteUser(
|
||||
await moderator.getUserId(),
|
||||
draupnir().managementRoomID
|
||||
);
|
||||
await moderator.joinRoom(draupnir().managementRoomID);
|
||||
await testRecoverAndRestart(
|
||||
(await moderator.getUserId()) as StringUserID,
|
||||
draupnir()
|
||||
);
|
||||
} as unknown as Mocha.AsyncFunc);
|
||||
});
|
||||
@@ -61,7 +61,7 @@ export const mochaHooks = {
|
||||
afterEach: [
|
||||
async function (this: DraupnirTestContext) {
|
||||
this.timeout(10000);
|
||||
this.toggle?.stopEverything();
|
||||
await this.toggle?.stopEverything();
|
||||
draupnirClient()?.stop();
|
||||
|
||||
// remove alias from management room and leave it.
|
||||
|
||||
@@ -256,10 +256,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"
|
||||
integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==
|
||||
|
||||
"@the-draupnir-project/interface-manager@2.4.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@the-draupnir-project/interface-manager/-/interface-manager-2.4.0.tgz#7734309e8cdc709d4f4a63557ac6b192dcb188a0"
|
||||
integrity sha512-0iKB5nHHUBRMysKOw77/YmPaSrA6d7YtrMZQd7fAE0eqNTZLgHu3vS0fnBQ1AcVM2016Io512up3WYTUVqAqkA==
|
||||
"@the-draupnir-project/interface-manager@2.4.1":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@the-draupnir-project/interface-manager/-/interface-manager-2.4.1.tgz#fc93eac495cf89fee0c87786b405a2fbae826b49"
|
||||
integrity sha512-7L9QzsH/BAgdPSoNaB78ENJKAqbD4sqA0J7LmAUDiM7ygLS1yVrrI2VRN9aCdguVcbuD//fnOCJhSlfL2vr6YQ==
|
||||
dependencies:
|
||||
"@gnuxie/super-cool-stream" "^0.2.1"
|
||||
"@gnuxie/typescript-result" "^1.0.0"
|
||||
@@ -2526,17 +2526,17 @@ matrix-appservice@^2.0.0:
|
||||
request-promise "^4.2.6"
|
||||
sanitize-html "^2.8.0"
|
||||
|
||||
"matrix-protection-suite-for-matrix-bot-sdk@npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite-for-matrix-bot-sdk/-/matrix-protection-suite-for-matrix-bot-sdk-1.4.0.tgz#0745358810e03959344e791733e6ccb7e811689d"
|
||||
integrity sha512-xGkBaBdZ3u5Z27HqG2IEPxQ8Z5slFtWCihnFzocPBtRcLDJMFXnoF1+bpWElnPomDQtw/u0gvDwSC+f4pmTqyQ==
|
||||
"matrix-protection-suite-for-matrix-bot-sdk@npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@1.5.1":
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite-for-matrix-bot-sdk/-/matrix-protection-suite-for-matrix-bot-sdk-1.5.1.tgz#ff01f3c81a28b7ea7391d9df06cac0613ad61b50"
|
||||
integrity sha512-cPG46fPDlROSsGPY79XZpjp6AIUBvk2rGZkPUr1Cbxl1FIntOzDNIKePLu3CEEAY4RppPDmmjsm0bmRt6zbYBA==
|
||||
dependencies:
|
||||
"@gnuxie/typescript-result" "^1.0.0"
|
||||
|
||||
"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-1.4.0.tgz#0cce6f9dec22cf41b8ffc26861e5ae5c7cf463db"
|
||||
integrity sha512-/1+L/Tf/fJx9aeNvyIfF4dqkS6YtTkRLMvSiqFNIpCq3JxOwyM2nqVBBNG5rbVPPdrxGlaFP+GuPxAUm54SdSA==
|
||||
"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@1.5.1":
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-1.5.1.tgz#243068169f8ff71e1e3fe5ae61d53c08995de520"
|
||||
integrity sha512-A4xaONgW4asZ22cZeZl+Rk4lWWd44WOWFOoZjRFSmve8ismU+HQQ+PA2jLacfARvlqi/puqiYAb6VN7GIbtz/w==
|
||||
dependencies:
|
||||
"@gnuxie/typescript-result" "^1.0.0"
|
||||
await-lock "^2.2.2"
|
||||
|
||||
Reference in new issue
Block a user