mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-05-26 02:14:02 +00:00
330 lines
10 KiB
TypeScript
330 lines
10 KiB
TypeScript
// Copyright 2022 - 2023 Gnuxie <Gnuxie@protonmail.com>
|
|
// Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// SPDX-FileAttributionText: <text>
|
|
// This modified file incorporates work from mjolnir
|
|
// https://github.com/matrix-org/mjolnir
|
|
// </text>
|
|
|
|
import {
|
|
StandardClientsInRoomMap,
|
|
DefaultEventDecoder,
|
|
ClientsInRoomMap,
|
|
Task,
|
|
Logger,
|
|
ActionException,
|
|
ActionExceptionKind,
|
|
ConfigRecoverableError,
|
|
} from "matrix-protection-suite";
|
|
import {
|
|
ClientCapabilityFactory,
|
|
MatrixSendClient,
|
|
RoomStateManagerFactory,
|
|
SafeMatrixEmitter,
|
|
joinedRoomsSafe,
|
|
} from "matrix-protection-suite-for-matrix-bot-sdk";
|
|
import { IConfig } from "./config";
|
|
import { Draupnir } from "./Draupnir";
|
|
import { DraupnirFactory } from "./draupnirfactory/DraupnirFactory";
|
|
import { WebAPIs } from "./webapis/WebAPIs";
|
|
import {
|
|
isStringUserID,
|
|
isStringRoomAlias,
|
|
isStringRoomID,
|
|
MatrixRoomReference,
|
|
StringUserID,
|
|
MatrixRoomID,
|
|
} from "@the-draupnir-project/matrix-basic-types";
|
|
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";
|
|
import { TopLevelStores } from "./backingstore/DraupnirStores";
|
|
import { patchMatrixClient } from "./utils";
|
|
|
|
const log = new Logger("DraupnirBotMode");
|
|
|
|
export function constructWebAPIs(draupnir: Draupnir): WebAPIs {
|
|
return new WebAPIs(
|
|
draupnir.reportManager,
|
|
draupnir.config,
|
|
draupnir.synapseHTTPAntispam
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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, AsyncDisposable {
|
|
encryptionInitialized(): Promise<void>;
|
|
startFromScratch(
|
|
options?: SafeModeToggleOptions
|
|
): Promise<Result<Draupnir | SafeModeDraupnir>>;
|
|
maybeRecoverWithSafeMode(
|
|
error: ResultError,
|
|
options?: SafeModeToggleOptions
|
|
): Promise<Result<SafeModeDraupnir>>;
|
|
}
|
|
|
|
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
|
|
) {
|
|
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);
|
|
});
|
|
this.matrixEmitter.on("room.leave", (roomID, event) => {
|
|
this.roomStateManagerFactory.handleTimelineEvent(roomID, event);
|
|
this.clientsInRoomMap.handleTimelineEvent(roomID, event);
|
|
});
|
|
}
|
|
public static async create(
|
|
client: MatrixSendClient,
|
|
matrixEmitter: SafeMatrixEmitter,
|
|
config: IConfig,
|
|
stores: TopLevelStores
|
|
): Promise<DraupnirBotModeToggle> {
|
|
patchMatrixClient();
|
|
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,
|
|
stores.roomStateBackingStore,
|
|
stores.hashStore
|
|
);
|
|
const draupnirFactory = new DraupnirFactory(
|
|
clientsInRoomMap,
|
|
clientCapabilityFactory,
|
|
clientProvider,
|
|
roomStateManagerFactory,
|
|
stores
|
|
);
|
|
return new DraupnirBotModeToggle(
|
|
clientUserID,
|
|
managementRoom,
|
|
clientsInRoomMap,
|
|
roomStateManagerFactory,
|
|
draupnirFactory,
|
|
matrixEmitter,
|
|
config
|
|
);
|
|
}
|
|
public async switchToDraupnir(
|
|
options?: SafeModeToggleOptions
|
|
): 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,
|
|
this.config,
|
|
this
|
|
);
|
|
if (isError(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();
|
|
if (options?.sendStatusOnStart) {
|
|
void Task(this.draupnir.startupComplete());
|
|
try {
|
|
this.webAPIs = constructWebAPIs(this.draupnir);
|
|
await this.webAPIs.start();
|
|
} catch (e) {
|
|
if (e instanceof Error) {
|
|
await 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.");
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
await 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 (draupnirResult.error instanceof DraupnirRestartError) {
|
|
return Ok(draupnirResult.error.safeModeDraupnir);
|
|
} else {
|
|
return draupnirResult;
|
|
}
|
|
}
|
|
return draupnirResult;
|
|
}
|
|
|
|
public async maybeRecoverWithSafeMode(
|
|
error: ResultError,
|
|
options?: SafeModeToggleOptions
|
|
): 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) {
|
|
this.webAPIs = constructWebAPIs(this.draupnir);
|
|
await this.webAPIs.start();
|
|
await this.draupnir.startupComplete();
|
|
} else if (this.safeModeDraupnir !== null) {
|
|
this.safeModeDraupnir.startupComplete();
|
|
}
|
|
}
|
|
|
|
private async stopDraupnir(): Promise<void> {
|
|
this.draupnir?.stop();
|
|
this.draupnir = null;
|
|
await this.webAPIs?.stop();
|
|
this.webAPIs = null;
|
|
}
|
|
|
|
private stopSafeModeDraupnir(): void {
|
|
this.safeModeDraupnir?.stop();
|
|
this.safeModeDraupnir = null;
|
|
}
|
|
|
|
public async [Symbol.asyncDispose](): Promise<void> {
|
|
await this.stopDraupnir();
|
|
this.stopSafeModeDraupnir();
|
|
}
|
|
}
|