Merge pull request #565 from the-draupnir-project/gnuxie/safe-mode-part-1

Initial safe mode (without editor or recovery options)

https://github.com/the-draupnir-project/planning/issues/26
This commit is contained in:
Gnuxie
2024-09-19 15:00:55 +01:00
committed by GitHub
24 changed files with 1052 additions and 215 deletions
+20 -42
View File
@@ -23,10 +23,8 @@ import {
RoomEvent,
RoomMembershipManager,
RoomMembershipRevisionIssuer,
RoomMessage,
RoomStateManager,
Task,
TextMessageContent,
Value,
isError,
} from "matrix-protection-suite";
@@ -67,6 +65,8 @@ import {
sendMatrixEventsFromDeadDocument,
} 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");
@@ -106,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,
@@ -121,6 +128,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 +177,8 @@ export class Draupnir implements Client, MatrixAdaptorContext {
policyRoomManager: PolicyRoomManager,
roomMembershipManager: RoomMembershipManager,
config: IConfig,
loggableConfigTracker: LoggableConfigTracker
loggableConfigTracker: LoggableConfigTracker,
safeModeToggle: SafeModeToggle
): Promise<ActionResult<Draupnir>> {
const acceptInvitesFromRoom = await (async () => {
if (config.autojoinOnlyIfManager) {
@@ -225,6 +234,7 @@ export class Draupnir implements Client, MatrixAdaptorContext {
loggableConfigTracker,
acceptInvitesFromRoom.ok,
acceptInvitesFromRoomIssuer.ok,
safeModeToggle,
new SynapseAdminClient(client, clientUserID)
);
const loadResult = await protectedRoomsSet.protections.loadProtections(
@@ -303,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);
}
@@ -347,14 +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<void> {
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);
}
}
+243 -67
View File
@@ -13,6 +13,11 @@ import {
DefaultEventDecoder,
setGlobalLoggerProvider,
RoomStateBackingStore,
ClientsInRoomMap,
Task,
Logger,
ActionException,
ActionExceptionKind,
} from "matrix-protection-suite";
import {
BotSDKLogServiceLogger,
@@ -20,7 +25,7 @@ import {
MatrixSendClient,
RoomStateManagerFactory,
SafeMatrixEmitter,
resolveRoomReferenceSafe,
joinedRoomsSafe,
} from "matrix-protection-suite-for-matrix-bot-sdk";
import { IConfig } from "./config";
import { Draupnir } from "./Draupnir";
@@ -32,85 +37,256 @@ import {
isStringRoomID,
MatrixRoomReference,
StringUserID,
MatrixRoomID,
} from "@the-draupnir-project/matrix-basic-types";
import { Result, isError } from "@gnuxie/typescript-result";
import {
SafeModeToggle,
SafeModeToggleOptions,
} from "./safemode/SafeModeToggle";
import { SafeModeDraupnir } from "./safemode/DraupnirSafeMode";
import { ResultError } from "@gnuxie/typescript-result";
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);
}
/**
* 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.
* 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<void>;
stopEverything(): void;
startFromScratch(
options?: SafeModeToggleOptions
): Promise<Result<Draupnir | SafeModeDraupnir>>;
}
export async function makeDraupnirBotModeFromConfig(
client: MatrixSendClient,
matrixEmitter: SafeMatrixEmitter,
config: IConfig,
backingStore?: RoomStateBackingStore
): Promise<Draupnir> {
const clientUserId = await client.getUserId();
if (!isStringUserID(clientUserId)) {
throw new TypeError(`${clientUserId} is not a valid mxid`);
}
if (
!isStringRoomAlias(config.managementRoom) &&
!isStringRoomID(config.managementRoom)
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,
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<DraupnirBotModeToggle> {
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(
options?: SafeModeToggleOptions
): Promise<Result<Draupnir>> {
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,
this
);
if (isError(draupnirResult)) {
return draupnirResult;
}
this.draupnir = draupnirResult.ok;
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(
cause: SafeModeCause,
options?: SafeModeToggleOptions
): Promise<Result<SafeModeDraupnir>> {
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,
this
);
if (isError(safeModeResult)) {
return safeModeResult;
}
this.stopDraupnir();
this.safeModeDraupnir = safeModeResult.ok;
this.safeModeDraupnir.start();
if (options?.sendStatusOnStart) {
this.safeModeDraupnir.startupComplete();
}
return safeModeResult;
}
public async startFromScratch(
options?: SafeModeToggleOptions
): 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 ?? {}
);
} else {
return draupnirResult;
}
}
return draupnirResult;
}
public async encryptionInitialized(): Promise<void> {
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();
}
}
+1 -2
View File
@@ -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);
@@ -288,7 +287,7 @@ export class AppServiceDraupnirManager {
mjolnirRecord: MjolnirRecord
): Promise<ActionResult<void>> {
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.`
);
+2
View File
@@ -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"])
+34
View File
@@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
//
// SPDX-License-Identifier: AFL-3.0
import {
BasicInvocationInformation,
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,
info: BasicInvocationInformation
): Promise<Result<SafeModeDraupnir>> {
return safeModeToggle.switchToSafeMode(
{
reason: SafeModeReason.ByRequest,
user: info.commandSender,
},
{ sendStatusOnStart: true }
);
},
});
DraupnirInterfaceAdaptor.describeRenderer(DraupnirSafeModeCommand, {
isAlwaysSupposedToUseDefaultRenderer: true,
});
@@ -370,3 +370,7 @@ export function renderTableHelp(table: CommandTable): DocumentNode {
</fragment>
);
}
export function wrapInRoot(node: DocumentNode): DocumentNode {
return <root>{node}</root>;
}
+6
View File
@@ -89,6 +89,9 @@ export interface IConfig {
redactReason: string;
};
};
safeMode?: {
bootIntoOnStartupFailure: 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: {
bootIntoOnStartupFailure: false,
},
health: {
healthz: {
enabled: false,
+41 -2
View File
@@ -5,6 +5,7 @@
import {
ActionResult,
ClientsInRoomMap,
Ok,
StandardLoggableConfigTracker,
isError,
} from "matrix-protection-suite";
@@ -21,6 +22,9 @@ import {
StringUserID,
MatrixRoomID,
} 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(
@@ -35,7 +39,8 @@ export class DraupnirFactory {
public async makeDraupnir(
clientUserID: StringUserID,
managementRoom: MatrixRoomID,
config: IConfig
config: IConfig,
toggle: SafeModeToggle
): Promise<ActionResult<Draupnir>> {
const client = await this.clientProvider(clientUserID);
const clientRooms = await this.clientsInRoomMap.makeClientRooms(
@@ -81,7 +86,41 @@ export class DraupnirFactory {
policyRoomManager,
roomMembershipManager,
config,
configLogTracker
configLogTracker,
toggle
);
}
public async makeSafeModeDraupnir(
clientUserID: StringUserID,
managementRoom: MatrixRoomID,
config: IConfig,
cause: SafeModeCause,
toggle: SafeModeToggle
): Promise<ActionResult<SafeModeDraupnir>> {
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,
toggle,
config
)
);
}
}
+85 -43
View File
@@ -8,12 +8,7 @@
// https://github.com/matrix-org/mjolnir
// </text>
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";
@@ -21,16 +16,47 @@ import {
StringUserID,
MatrixRoomID,
} 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 readyDraupnirs = new Map<StringUserID, Draupnir>();
private readonly listeningDraupnirs = new Map<StringUserID, Draupnir>();
private readonly failedDraupnirs = new Map<StringUserID, UnstartedDraupnir>();
private readonly draupnir = new Map<StringUserID, Draupnir>();
private readonly failedDraupnir = new Map<StringUserID, UnstartedDraupnir>();
private readonly safeModeDraupnir = new Map<StringUserID, SafeModeDraupnir>();
public constructor(protected readonly draupnirFactory: DraupnirFactory) {
// 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,
@@ -39,13 +65,10 @@ export class StandardDraupnirManager {
const draupnir = await this.draupnirFactory.makeDraupnir(
clientUserID,
managementRoom,
config
config,
this.makeSafeModeToggle(clientUserID, 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.isDraupnirAvailable(clientUserID)) {
return ActionError.Result(
`There is a draupnir for ${clientUserID} already running`
);
@@ -58,21 +81,55 @@ export class StandardDraupnirManager {
);
return draupnir;
}
this.readyDraupnirs.set(clientUserID, draupnir.ok);
this.failedDraupnirs.delete(clientUserID);
this.draupnir.set(clientUserID, draupnir.ok);
this.failedDraupnir.delete(clientUserID);
draupnir.ok.start();
return draupnir;
}
public isDraupnirReady(draupnirClientID: StringUserID): boolean {
return this.readyDraupnirs.has(draupnirClientID);
public async makeSafeModeDraupnir(
clientUserID: StringUserID,
managementRoom: MatrixRoomID,
config: IConfig,
cause: SafeModeCause
): Promise<ActionResult<SafeModeDraupnir>> {
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,
this.makeSafeModeToggle(clientUserID, managementRoom, config)
);
if (isError(safeModeDraupnir)) {
this.reportUnstartedDraupnir(
DraupnirFailType.InitializationError,
safeModeDraupnir.error,
clientUserID
);
return safeModeDraupnir;
}
safeModeDraupnir.ok.start();
this.safeModeDraupnir.set(clientUserID, safeModeDraupnir.ok);
return safeModeDraupnir;
}
public isDraupnirListening(draupnirClientID: StringUserID): boolean {
return this.listeningDraupnirs.has(draupnirClientID);
/**
* 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 {
return this.failedDraupnirs.has(draupnirClientID);
return this.failedDraupnir.has(draupnirClientID);
}
public reportUnstartedDraupnir(
@@ -80,50 +137,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);
}
}
}
+12 -18
View File
@@ -9,9 +9,7 @@
// </text>
import * as path from "path";
import { Healthz } from "./health/healthz";
import {
LogLevel,
LogService,
@@ -24,14 +22,9 @@ 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 { 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 () {
@@ -54,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
@@ -104,29 +96,31 @@ void (async function () {
eventDecoder
)
: undefined;
bot = await makeDraupnirBotModeFromConfig(
bot = await DraupnirBotModeToggle.create(
client,
new SafeMatrixEmitterWrapper(client, eventDecoder),
config,
store
);
apis = constructWebAPIs(bot);
// We don't want to send the status on start, as we need to initialize e2ee first (using client.start);
(await bot.startFromScratch({ sendStatusOnStart: false })).expect(
"Failed to start Draupnir"
);
} catch (err) {
console.error(
`Failed to setup mjolnir from the config ${config.dataPath}: ${err}`
);
bot?.stopEverything();
throw err;
}
try {
await bot.start();
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;
}
})();
+12
View File
@@ -207,6 +207,18 @@ export class ReportPoller {
}
return reportPollSetting;
}
public async startFromStoredSetting(
client: MatrixSendClient,
managementRoomOutput: ManagementRoomOutput
): Promise<void> {
const reportPollSetting = await ReportPoller.getReportPollSetting(
client,
managementRoomOutput
);
this.start(reportPollSetting);
}
public start({ from: startFrom }: ReportPollSetting) {
if (this.timeout === null) {
this.from = startFrom;
+114
View File
@@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
//
// SPDX-License-Identifier: AFL-3.0
import {
ClientPlatform,
ClientRooms,
EventReport,
RoomEvent,
Task,
} from "matrix-protection-suite";
import {
MatrixAdaptorContext,
sendMatrixEventsFromDeadDocument,
} 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";
import { makeSafeModeCommandDispatcher } from "./SafeModeCommandDispatcher";
import {
ARGUMENT_PROMPT_LISTENER,
DEFAUILT_ARGUMENT_PROMPT_LISTENER,
makeListenerForArgumentPrompt,
makeListenerForPromptDefault,
} 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;
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,
public readonly clientUserID: StringUserID,
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,
//private readonly roomMembershipManager: RoomMembershipManager,
) {
this.reactionHandler = new MatrixReactionHandler(
managementRoom.toRoomIDOrAlias(),
client,
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 {
this.commandDispatcherTimelineListener(roomID, event);
}
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);
}
public startupComplete(): void {
void Task(
sendMatrixEventsFromDeadDocument(
this.clientPlatform.toRoomMessageSender(),
this.commandRoomID,
wrapInRoot(renderSafeModeStatusInfo(safeModeStatusInfo(this))),
{}
) as Promise<Result<void>>
);
}
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright 2022 - 2024 Gnuxie <Gnuxie@protonmail.com>
// Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0
//
// SPDX-FileAttributionText: <text>
// This modified file incorporates work from mjolnir
// https://github.com/matrix-org/mjolnir
// </text>
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<MatrixEventContext>
): (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
);
}
};
}
+15
View File
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
//
// 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",
ByRequest = "ByRequest",
}
export type SafeModeCause =
| { reason: SafeModeReason.ByRequest; user: StringUserID }
| { reason: SafeModeReason.InitializationError; error: ResultError };
+57
View File
@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
//
// SPDX-License-Identifier: AFL-3.0
import {
CommandPrefixExtractor,
MatrixInterfaceCommandDispatcher,
StandardMatrixInterfaceCommandDispatcher,
} from "@the-draupnir-project/interface-manager";
import { SafeModeDraupnir } from "./DraupnirSafeMode";
import {
MPSCommandDispatcherCallbacks,
MatrixEventContext,
invocationInformationFromMatrixEventcontext,
} from "../commands/interface-manager/MPSMatrixInterfaceAdaptor";
import { userLocalpart } from "@the-draupnir-project/matrix-basic-types";
import { SafeModeCommands } from "./commands/SafeModeCommands";
import { SafeModeHelpCommand } from "./commands/HelpCommand";
import { SafeModeInterfaceAdaptor } from "./commands/SafeModeAdaptor";
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<MatrixEventContext> {
return new StandardMatrixInterfaceCommandDispatcher(
SafeModeInterfaceAdaptor,
safeModeDraupnir,
SafeModeCommands,
SafeModeHelpCommand,
invocationInformationFromMatrixEventcontext,
{
...MPSCommandDispatcherCallbacks,
prefixExtractor: makePrefixExtractor(safeModeDraupnir),
}
);
}
+25
View File
@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
//
// 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 type SafeModeToggleOptions = { sendStatusOnStart?: boolean };
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(options?: SafeModeToggleOptions): Promise<Result<Draupnir>>;
switchToSafeMode(
cause: SafeModeCause,
options?: SafeModeToggleOptions
): Promise<Result<SafeModeDraupnir>>;
}
+46
View File
@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
//
// 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<Result<CommandTable>> {
return Ok(SafeModeCommands);
},
parameters: [],
});
SafeModeInterfaceAdaptor.describeRenderer(SafeModeHelpCommand, {
JSXRenderer(result) {
if (isError(result)) {
throw new TypeError("This should never fail");
}
return Ok(
<root>
{safeModeHeader()}
{renderTableHelp(result.ok)}
</root>
);
},
});
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
//
// 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<Result<Draupnir>> {
return safeModeToggle.switchToDraupnir({ sendStatusOnStart: true });
},
});
SafeModeInterfaceAdaptor.describeRenderer(SafeModeRestartCommand, {
isAlwaysSupposedToUseDefaultRenderer: true,
});
+28
View File
@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
//
// 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<SafeModeDraupnir>();
export const SafeModeInterfaceAdaptor = new StandardMatrixInterfaceAdaptor<
SafeModeDraupnir,
MatrixEventContext
>(
SafeModeContextToCommandContextTranslator,
invocationInformationFromMatrixEventcontext,
MPSMatrixInterfaceAdaptorCallbacks,
MPSCommandDispatcherCallbacks
);
@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
//
// SPDX-License-Identifier: AFL-3.0
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(SafeModeRestartCommand, ["draupnir", "restart"]);
+137
View File
@@ -0,0 +1,137 @@
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
//
// 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 (
<fragment>
<span> Draupnir is in safe mode (see status command) </span>
<br />
</fragment>
);
}
function renderSafeModeCauseError(error: ResultError): DocumentNode {
if (error instanceof ActionException) {
return (
<fragment>
Draupnir is in safe mode because Draupnir failed to start.
<br />
{error.mostRelevantElaboration}
<br />
Details can be found by providing the reference{" "}
<code>{error.uuid}</code>
to an administrator.
<pre>{error.toReadableString()}</pre>
</fragment>
);
} else {
return (
<fragment>
Draupnir is in safe mode because Draupnir failed to start.
<br />
{error.mostRelevantElaboration}
<pre>{error.toReadableString()}</pre>
</fragment>
);
}
}
function renderSafeModeCause(safeModeCause: SafeModeCause): DocumentNode {
if (safeModeCause.reason === SafeModeReason.ByRequest) {
return (
<fragment>
Draupnir is in safe mode by request of {safeModeCause.user}.
</fragment>
);
} 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 (
<fragment>
Draupnir is in safe mode
<span>
<br />
{renderSafeModeCause(info.safeModeCause)}
<br />
</span>
<br />
<b>Version: </b>
<code>{info.version}</code>
<br />
<b>Repository: </b>
<code>{info.repository}</code>
<br />
{showDocumentationURL ? (
<fragment>
<b>Documentation: </b>{" "}
<a href={info.documentationURL}>{info.documentationURL}</a>
</fragment>
) : (
<fragment></fragment>
)}
<br />
<br />
To attempt to restart, use <code>!draupnir restart</code>
</fragment>
);
}
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<Result<SafeModeStatusInfo>> {
return Ok(safeModeStatusInfo(safeModeDraupnir));
},
});
SafeModeInterfaceAdaptor.describeRenderer(SafeModeStatusCommand, {
JSXRenderer(result) {
if (isError(result)) {
return Ok(undefined);
}
return Ok(<root>{renderSafeModeStatusInfo(result.ok)}</root>);
},
});
+6 -18
View File
@@ -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,35 +46,23 @@ export const mochaHooks = {
this.timeout(30000);
const config = (this.config = configRead());
this.managementRoomAlias = config.managementRoom;
this.draupnir = await makeMjolnir(config);
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 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();
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) {
+8 -13
View File
@@ -12,26 +12,21 @@
* 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 { 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,
new SqliteRoomStateBackingStore(
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()}`);
await mjolnir.start();
const apis = constructWebAPIs(mjolnir);
),
allowSafeMode: true,
});
await draupnirClient()?.start();
await apis.start();
void Task(mjolnir.startupComplete());
await toggle.encryptionInitialized();
})();
+43 -10
View File
@@ -20,16 +20,18 @@ 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,
} 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";
import { SafeModeDraupnir } from "../../src/safemode/DraupnirSafeMode";
patchMatrixClient();
@@ -49,7 +51,7 @@ export type SafeMochaContext = Pick<
export interface DraupnirTestContext extends SafeMochaContext {
draupnir?: Draupnir;
managementRoomAlias?: string;
apis?: WebAPIs;
toggle?: DraupnirBotModeToggle;
config: IConfig;
}
@@ -100,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 {
@@ -119,10 +124,18 @@ let globalSafeEmitter: SafeMatrixEmitter | undefined;
/**
* Return a test instance of Mjolnir.
*/
export async function makeMjolnir(
export async function makeBotModeToggle(
config: IConfig,
backingStore?: RoomStateBackingStore
): Promise<Draupnir> {
{
backingStore,
eraseAccountData,
allowSafeMode,
}: {
backingStore?: RoomStateBackingStore;
eraseAccountData?: boolean;
allowSafeMode?: boolean;
} = {}
): Promise<DraupnirBotModeToggle> {
await configureMjolnir(config);
LogService.setLogger(new RichConsoleLogger());
LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG));
@@ -135,21 +148,41 @@ 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()
);
await ensureAliasedRoomExists(client, config.managementRoom);
const mj = await makeDraupnirBotModeFromConfig(
const toggle = await DraupnirBotModeToggle.create(
client,
new SafeMatrixEmitterWrapper(client, DefaultEventDecoder),
config,
backingStore
);
// we don't want to send status on startup incase we want to test e2ee from the manual launch script.
const mj = (
await toggle.startFromScratch({ sendStatusOnStart: false })
).expect("Could not create Draupnir");
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 mj;
return toggle;
}
/**