mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-05-10 17:55:18 +00:00
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:
+20
-42
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.`
|
||||
);
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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>>;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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"]);
|
||||
@@ -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>);
|
||||
},
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user