mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-09-30 06:27:57 +00:00
introduce draupnir factory and manager.
These are used to create draupnirs and then inform draupnirs of new events. The draupnir manager is also used to start/stop draupnirs for the appservice
This commit is contained in:
+4
-30
@@ -26,43 +26,17 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
MjolnirPolicyRoomsConfig,
|
||||
PolicyListConfig,
|
||||
PolicyRoomManager,
|
||||
ProtectedRoomsConfig,
|
||||
ResolveRoom,
|
||||
MjolnirProtectedRoomsConfig,
|
||||
StandardProtectedRoomsSet,
|
||||
isError,
|
||||
RoomStateManager,
|
||||
MjolnirProtectionsConfig,
|
||||
MjolnirEnabledProtectionsEvent,
|
||||
MjolnirEnabledProtectionsEventType,
|
||||
MatrixRoomID,
|
||||
MjolnirProtectionSettingsEventType,
|
||||
StandardSetMembership,
|
||||
RoomMembershipManager,
|
||||
SetMembership,
|
||||
StringUserID,
|
||||
ProtectedRoomsSet,
|
||||
MatrixRoomReference,
|
||||
isStringUserID,
|
||||
isStringRoomAlias,
|
||||
isStringRoomID,
|
||||
SetRoomState,
|
||||
StandardSetRoomState,
|
||||
StandardClientRooms,
|
||||
StandardClientsInRoomMap,
|
||||
StandardEventDecoder,
|
||||
DefaultEventDecoder,
|
||||
} from "matrix-protection-suite";
|
||||
import {
|
||||
BotSDKMatrixAccountData,
|
||||
BotSDKMatrixStateData,
|
||||
BotSDKMjolnirProtectedRoomsStore,
|
||||
BotSDKMjolnirWatchedPolicyRoomsStore,
|
||||
DefaultStateTrackingMeta,
|
||||
ManagerManager,
|
||||
MatrixSendClient,
|
||||
RoomStateManagerFactory,
|
||||
SafeMatrixEmitter,
|
||||
@@ -114,13 +88,13 @@ export async function makeDraupnirBotModeFromConfig(
|
||||
clientProvider,
|
||||
roomStateManagerFactory
|
||||
);
|
||||
const clientRooms = await draupnirFactory.makeDraupnirClientRooms(
|
||||
const draupnir = await draupnirFactory.makeDraupnir(
|
||||
clientUserId,
|
||||
managementRoom.ok,
|
||||
config
|
||||
);
|
||||
if (isError(clientRooms)) {
|
||||
throw clientRooms.error;
|
||||
if (isError(draupnir)) {
|
||||
throw draupnir.error;
|
||||
}
|
||||
|
||||
return draupnir.ok;
|
||||
}
|
||||
|
||||
+35
-16
@@ -1,9 +1,10 @@
|
||||
import request from "request";
|
||||
import express from "express";
|
||||
import * as bodyParser from "body-parser";
|
||||
import { MjolnirManager } from "./MjolnirManager";
|
||||
import * as http from "http";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { AppServiceDraupnirManager } from "./AppServiceDraupnirManager";
|
||||
import { isError, isStringUserID } from "matrix-protection-suite";
|
||||
|
||||
const log = new Logger("Api");
|
||||
/**
|
||||
@@ -15,7 +16,7 @@ export class Api {
|
||||
|
||||
constructor(
|
||||
private homeserver: string,
|
||||
private mjolnirManager: MjolnirManager,
|
||||
private mjolnirManager: AppServiceDraupnirManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -88,16 +89,18 @@ export class Api {
|
||||
response.status(401).send("unauthorised");
|
||||
return;
|
||||
}
|
||||
if (!isStringUserID(userId)) {
|
||||
response.status(400).send("invalid user mxid");
|
||||
return;
|
||||
}
|
||||
|
||||
const mjolnirId = req.body["mxid"];
|
||||
if (mjolnirId === undefined) {
|
||||
if (mjolnirId === undefined || !isStringUserID(mjolnirId)) {
|
||||
response.status(400).send("invalid request");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: getMjolnir can fail if the ownerId doesn't match the requesting userId.
|
||||
// https://github.com/matrix-org/mjolnir/issues/408
|
||||
const mjolnir = this.mjolnirManager.getMjolnir(mjolnirId, userId);
|
||||
const mjolnir = await this.mjolnirManager.getRunningDraupnir(mjolnirId, userId);
|
||||
if (mjolnir === undefined) {
|
||||
response.status(400).send("unknown mjolnir mxid");
|
||||
return;
|
||||
@@ -122,8 +125,12 @@ export class Api {
|
||||
response.status(401).send("unauthorised");
|
||||
return;
|
||||
}
|
||||
if (!isStringUserID(userId)) {
|
||||
response.status(400).send("invalid user mxid");
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = this.mjolnirManager.getOwnedMjolnirs(userId)
|
||||
const existing = this.mjolnirManager.getOwnedDraupnir(userId)
|
||||
response.status(200).json(existing);
|
||||
}
|
||||
|
||||
@@ -151,12 +158,20 @@ export class Api {
|
||||
response.status(401).send("unauthorised");
|
||||
return;
|
||||
}
|
||||
if (!isStringUserID(userId)) {
|
||||
response.status(400).send("invalid user mxid");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: provisionNewMjolnir will throw if it fails...
|
||||
// https://github.com/matrix-org/mjolnir/issues/408
|
||||
const [mjolnirId, managementRoom] = await this.mjolnirManager.provisionNewMjolnir(userId);
|
||||
|
||||
response.status(200).json({ mxid: mjolnirId, roomId: managementRoom });
|
||||
const record = await this.mjolnirManager.provisionNewDraupnir(userId);
|
||||
if (isError(record)) {
|
||||
response.status(500).send(record.error.message);
|
||||
return;
|
||||
}
|
||||
response.status(200).json({
|
||||
mxid: this.mjolnirManager.draupnirMXID(record.ok),
|
||||
roomId: record.ok.management_room
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,9 +192,13 @@ export class Api {
|
||||
response.status(401).send("unauthorised");
|
||||
return;
|
||||
}
|
||||
if (!isStringUserID(userId)) {
|
||||
response.status(400).send("invalid user mxid");
|
||||
return;
|
||||
}
|
||||
|
||||
const mjolnirId = req.body["mxid"];
|
||||
if (mjolnirId === undefined) {
|
||||
if (mjolnirId === undefined || !isStringUserID(mjolnirId)) {
|
||||
response.status(400).send("invalid request");
|
||||
return;
|
||||
}
|
||||
@@ -192,14 +211,14 @@ export class Api {
|
||||
|
||||
// TODO: getMjolnir can fail if the ownerId doesn't match the requesting userId.
|
||||
// https://github.com/matrix-org/mjolnir/issues/408
|
||||
const mjolnir = this.mjolnirManager.getMjolnir(mjolnirId, userId);
|
||||
const mjolnir = await this.mjolnirManager.getRunningDraupnir(mjolnirId, userId);
|
||||
if (mjolnir === undefined) {
|
||||
response.status(400).send("unknown mjolnir mxid");
|
||||
return;
|
||||
}
|
||||
|
||||
await mjolnir.joinRoom(roomId);
|
||||
await mjolnir.addProtectedRoom(roomId);
|
||||
await mjolnir.client.joinRoom(roomId);
|
||||
await mjolnir.protectedRoomsSet.protectedRoomsConfig.addRoom(roomId);
|
||||
|
||||
response.status(200).json({});
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { AppServiceRegistration, Bridge, Request, WeakEvent, MatrixUser, Logger, setBridgeVersion, PrometheusMetrics } from "matrix-appservice-bridge";
|
||||
import { MjolnirManager } from ".//MjolnirManager";
|
||||
import { DataStore } from ".//datastore";
|
||||
import { PgDataStore } from "./postgres/PgDataStore";
|
||||
import { Api } from "./Api";
|
||||
@@ -35,8 +34,9 @@ import { AccessControl } from "./AccessControl";
|
||||
import { AppserviceCommandHandler } from "./bot/AppserviceCommandHandler";
|
||||
import { SOFTWARE_VERSION } from "../config";
|
||||
import { Registry } from 'prom-client';
|
||||
import { resolveRoomReferenceSafe } from "matrix-protection-suite-for-matrix-bot-sdk";
|
||||
import { isError } from "matrix-protection-suite";
|
||||
import { DefaultStateTrackingMeta, RoomStateManagerFactory, resolveRoomReferenceSafe } from "matrix-protection-suite-for-matrix-bot-sdk";
|
||||
import { DefaultEventDecoder, StandardClientsInRoomMap, StringUserID, isError } from "matrix-protection-suite";
|
||||
import { AppServiceDraupnirManager } from "./AppServiceDraupnirManager";
|
||||
|
||||
const log = new Logger("AppService");
|
||||
/**
|
||||
@@ -55,12 +55,12 @@ export class MjolnirAppService {
|
||||
private constructor(
|
||||
public readonly config: IConfig,
|
||||
public readonly bridge: Bridge,
|
||||
public readonly mjolnirManager: MjolnirManager,
|
||||
public readonly draupnirManager: AppServiceDraupnirManager,
|
||||
public readonly accessControl: AccessControl,
|
||||
private readonly dataStore: DataStore,
|
||||
private readonly prometheusMetrics: PrometheusMetrics
|
||||
) {
|
||||
this.api = new Api(config.homeserver.url, mjolnirManager);
|
||||
this.api = new Api(config.homeserver.url, draupnirManager);
|
||||
this.commands = new AppserviceCommandHandler(this);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,19 @@ export class MjolnirAppService {
|
||||
if (isError(accessControlRoom)) {
|
||||
throw accessControlRoom.error;
|
||||
}
|
||||
const accessControl = await AccessControl.setupAccessControlForRoom(accessControlRoom, bridge);
|
||||
const clientsInRoomMap = new StandardClientsInRoomMap();
|
||||
const clientProvider = async (clientUserID: StringUserID) => bridge.getIntent(clientUserID).matrixClient;
|
||||
const roomStateManagerFactory = new RoomStateManagerFactory(
|
||||
clientsInRoomMap,
|
||||
clientProvider,
|
||||
DefaultEventDecoder,
|
||||
DefaultStateTrackingMeta
|
||||
);
|
||||
const appserviceBotPolicyRoomManager = await roomStateManagerFactory.getPolicyRoomManager(bridge.getBot().getUserId() as StringUserID);
|
||||
const accessControl = await AccessControl.setupAccessControlForRoom(accessControlRoom.ok, appserviceBotPolicyRoomManager, bridge);
|
||||
if (isError(accessControl)) {
|
||||
throw accessControl.error;
|
||||
}
|
||||
// Activate /metrics endpoint for Prometheus
|
||||
|
||||
// This should happen automatically but in testing this didn't happen in the docker image
|
||||
@@ -105,12 +117,13 @@ export class MjolnirAppService {
|
||||
labels: ["status", "uuid"],
|
||||
});
|
||||
|
||||
const mjolnirManager = await MjolnirManager.makeMjolnirManager(dataStore, bridge, accessControl, instanceCountGauge);
|
||||
const serverName = config.homeserver.domain;
|
||||
const mjolnirManager = await AppServiceDraupnirManager.makeDraupnirManager(serverName, dataStore, bridge, accessControl.ok, roomStateManagerFactory, instanceCountGauge);
|
||||
const appService = new MjolnirAppService(
|
||||
config,
|
||||
bridge,
|
||||
mjolnirManager,
|
||||
accessControl,
|
||||
accessControl.ok,
|
||||
dataStore,
|
||||
prometheus
|
||||
);
|
||||
@@ -156,7 +169,7 @@ export class MjolnirAppService {
|
||||
if ('invite' === mxEvent.content['membership'] && mxEvent.state_key === this.bridge.botUserId) {
|
||||
log.info(`${mxEvent.sender} has sent an invitation to the appservice bot ${this.bridge.botUserId}, attempting to provision them a mjolnir`);
|
||||
try {
|
||||
await this.mjolnirManager.provisionNewMjolnir(mxEvent.sender)
|
||||
await this.draupnirManager.provisionNewDraupnir(mxEvent.sender as StringUserID)
|
||||
} catch (e: any) {
|
||||
log.error(`Failed to provision a mjolnir for ${mxEvent.sender} after they invited ${this.bridge.botUserId}:`, e);
|
||||
// continue, we still want to reject this invitation.
|
||||
@@ -169,7 +182,7 @@ export class MjolnirAppService {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.mjolnirManager.onEvent(request);
|
||||
this.draupnirManager.onEvent(request);
|
||||
this.commands.handleEvent(mxEvent);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Copyright (C) 2022-2024 Gnuxie <Gnuxie@protonmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is modified and is NOT licensed under the Apache License.
|
||||
* This modified file incorperates work from mjolnir
|
||||
* https://github.com/matrix-org/mjolnir
|
||||
* which included the following license notice:
|
||||
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*
|
||||
* However, this file is modified and the modifications in this file
|
||||
* are NOT distributed, contributed, committed, or licensed under the Apache License.
|
||||
*/
|
||||
|
||||
import { Bridge, Intent, Logger } from "matrix-appservice-bridge";
|
||||
import { getProvisionedMjolnirConfig } from "../config";
|
||||
import { MatrixClient } from "matrix-bot-sdk";
|
||||
import { DataStore, MjolnirRecord } from "./datastore";
|
||||
import { AccessControl } from "./AccessControl";
|
||||
import { randomUUID } from "crypto";
|
||||
import { Gauge } from "prom-client";
|
||||
import { decrementGaugeValue, incrementGaugeValue } from "../utils";
|
||||
import { Access, ActionError, ActionException, ActionExceptionKind, ActionResult, MatrixRoomReference, Ok, PropagationType, StringRoomID, StringUserID, Task, isError, isStringRoomID, userLocalpart } from "matrix-protection-suite";
|
||||
import { Draupnir } from "../Draupnir";
|
||||
import { RoomStateManagerFactory } from "matrix-protection-suite-for-matrix-bot-sdk";
|
||||
import { DraupnirFailType, StandardDraupnirManager, UnstartedDraupnir } from "../draupnirfactory/StandardDraupnirManager";
|
||||
import { DraupnirFactory } from "../draupnirfactory/DraupnirFactory";
|
||||
|
||||
const log = new Logger('AppServiceDraupnirManager');
|
||||
|
||||
|
||||
/**
|
||||
* The DraupnirManager is responsible for:
|
||||
* * Provisioning new draupnir instances.
|
||||
* * Starting draupnir when the appservice is brought online.
|
||||
* * Informing draupnir about new events.
|
||||
*/
|
||||
export class AppServiceDraupnirManager {
|
||||
|
||||
private readonly baseManager: StandardDraupnirManager;
|
||||
|
||||
private constructor(
|
||||
private readonly serverName: string,
|
||||
private readonly dataStore: DataStore,
|
||||
private readonly bridge: Bridge,
|
||||
private readonly accessControl: AccessControl,
|
||||
private readonly roomStateManagerFactory: RoomStateManagerFactory,
|
||||
private readonly instanceCountGauge: Gauge<"status" | "uuid">
|
||||
) {
|
||||
const clientProvider = this.bridge.getIntent.bind(this.bridge);
|
||||
const draupnirFactory = new DraupnirFactory(
|
||||
clientProvider,
|
||||
this.roomStateManagerFactory
|
||||
);
|
||||
this.baseManager = new StandardDraupnirManager(
|
||||
draupnirFactory,
|
||||
roomStateManagerFactory.clientsInRoomMap
|
||||
);
|
||||
}
|
||||
|
||||
public draupnirMXID(mjolnirRecord: MjolnirRecord): StringUserID {
|
||||
return `${mjolnirRecord.local_part}:${this.serverName}` as StringUserID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the draupnir manager from the datastore and the access control.
|
||||
* @param dataStore The data store interface that has the details for provisioned draupnirs.
|
||||
* @param bridge The bridge abstraction that encapsulates details about the appservice.
|
||||
* @param accessControl Who has access to the bridge.
|
||||
* @returns A new mjolnir manager.
|
||||
*/
|
||||
public static async makeDraupnirManager(
|
||||
serverName: string,
|
||||
dataStore: DataStore,
|
||||
bridge: Bridge,
|
||||
accessControl: AccessControl,
|
||||
roomStateManagerFactory: RoomStateManagerFactory,
|
||||
instanceCountGauge: Gauge<"status" | "uuid">
|
||||
): Promise<AppServiceDraupnirManager> {
|
||||
const draupnirManager = new AppServiceDraupnirManager(serverName, dataStore, bridge, accessControl, roomStateManagerFactory, instanceCountGauge);
|
||||
await draupnirManager.startDraupnirs(await dataStore.list());
|
||||
return draupnirManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new mjolnir for a user.
|
||||
* @param requestingUserID The user that is requesting this mjolnir and who will own it.
|
||||
* @param managementRoomId An existing matrix room to act as the management room.
|
||||
* @param client A client for the appservice virtual user that the new mjolnir should use.
|
||||
* @returns A new managed mjolnir.
|
||||
*/
|
||||
public async makeInstance(localPart: string, requestingUserID: StringUserID, managementRoomID: StringRoomID, client: MatrixClient): Promise<ActionResult<Draupnir>> {
|
||||
const mxid = await client.getUserId() as StringUserID;
|
||||
const managedDraupnir = await this.baseManager.makeDraupnir(
|
||||
mxid,
|
||||
MatrixRoomReference.fromRoomID(managementRoomID),
|
||||
getProvisionedMjolnirConfig(managementRoomID)
|
||||
);
|
||||
if (isError(managedDraupnir)) {
|
||||
return managedDraupnir;
|
||||
}
|
||||
this.baseManager.startDraupnir(mxid);
|
||||
incrementGaugeValue(this.instanceCountGauge, "offline", localPart);
|
||||
decrementGaugeValue(this.instanceCountGauge, "disabled", localPart);
|
||||
incrementGaugeValue(this.instanceCountGauge, "online", localPart);
|
||||
return managedDraupnir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a draupnir for the corresponding mxid that is owned by a specific user.
|
||||
* @param draupnirID The mxid of the draupnir we are trying to get.
|
||||
* @param ownerID The owner of the draupnir. We ask for it explicitly to not leak access to another user's draupnir.
|
||||
* @returns The matching managed draupnir instance.
|
||||
*/
|
||||
public async getRunningDraupnir(draupnirClientID: StringUserID, ownerID: StringUserID): Promise<Draupnir | undefined> {
|
||||
const records = await this.dataStore.lookupByOwner(ownerID);
|
||||
if (records.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const associatedRecord = records.find(record => record.local_part === userLocalpart(draupnirClientID));
|
||||
if (associatedRecord === undefined || associatedRecord.owner !== ownerID) {
|
||||
return undefined;
|
||||
}
|
||||
return this.baseManager.findRunningDraupnir(draupnirClientID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all of the running Draupnir that are owned by this specific user.
|
||||
* @param ownerID An owner of multiple draupnir.
|
||||
* @returns Any draupnir that they own.
|
||||
*/
|
||||
public async getOwnedDraupnir(ownerID: StringUserID): Promise<StringUserID[]> {
|
||||
const records = await this.dataStore.lookupByOwner(ownerID);
|
||||
return records.map(record => this.draupnirMXID(record));
|
||||
}
|
||||
|
||||
/**
|
||||
* provision a new Draupnir for a matrix user.
|
||||
* @param requestingUserID The mxid of the user we are creating a Draupnir for.
|
||||
* @returns The matrix id of the new Draupnir and its management room.
|
||||
*/
|
||||
public async provisionNewDraupnir(requestingUserID: StringUserID): Promise<ActionResult<MjolnirRecord>> {
|
||||
const access = this.accessControl.getUserAccess(requestingUserID);
|
||||
if (access.outcome !== Access.Allowed) {
|
||||
return ActionError.Result(`${requestingUserID} tried to provision a draupnir when they do not have access ${access.outcome} ${access.rule?.reason ?? 'no reason specified'}`);
|
||||
}
|
||||
const provisionedMjolnirs = await this.dataStore.lookupByOwner(requestingUserID);
|
||||
if (provisionedMjolnirs.length === 0) {
|
||||
const mjolnirLocalPart = `draupnir_${randomUUID()}`;
|
||||
const mjIntent = await this.makeMatrixIntent(mjolnirLocalPart);
|
||||
|
||||
const managementRoomID = await mjIntent.matrixClient.createRoom({
|
||||
preset: 'private_chat',
|
||||
invite: [requestingUserID],
|
||||
name: `${requestingUserID}'s Draupnir`,
|
||||
power_level_content_override: {
|
||||
users: {
|
||||
[requestingUserID]: 100,
|
||||
// Give the mjolnir a higher PL so that can avoid issues with managing the management room.
|
||||
[await mjIntent.matrixClient.getUserId()]: 101
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!isStringRoomID(managementRoomID)) {
|
||||
throw new TypeError(`${managementRoomID} malformed managmentRoomID`);
|
||||
}
|
||||
const draupnir = await this.makeInstance(mjolnirLocalPart, requestingUserID, managementRoomID, mjIntent.matrixClient);
|
||||
if (isError(draupnir)) {
|
||||
return draupnir;
|
||||
}
|
||||
const policyListResult = await createFirstList(draupnir.ok, requestingUserID, "list");
|
||||
if (isError(policyListResult)) {
|
||||
return policyListResult;
|
||||
}
|
||||
const record = {
|
||||
local_part: mjolnirLocalPart,
|
||||
owner: requestingUserID,
|
||||
management_room: managementRoomID,
|
||||
} as MjolnirRecord;
|
||||
await this.dataStore.store(record);
|
||||
return Ok(record);
|
||||
} else {
|
||||
return ActionError.Result(`User: ${requestingUserID} has already provisioned ${provisionedMjolnirs.length} draupnirs.`);
|
||||
}
|
||||
}
|
||||
|
||||
public getUnstartedDraupnirs(): UnstartedDraupnir[] {
|
||||
return this.baseManager.getUnstartedDraupnirs();
|
||||
}
|
||||
|
||||
public findUnstartedMjolnir(clientUserID: StringUserID): UnstartedDraupnir | undefined {
|
||||
return this.baseManager.findUnstartedDraupnir(clientUserID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility that creates a matrix client for a virtual user on our homeserver with the specified loclapart.
|
||||
* @param localPart The localpart of the virtual user we need a client for.
|
||||
* @returns A bridge intent with the complete mxid of the virtual user and a MatrixClient.
|
||||
*/
|
||||
private async makeMatrixIntent(localPart: string): Promise<Intent> {
|
||||
const mjIntent = this.bridge.getIntentFromLocalpart(localPart);
|
||||
await mjIntent.ensureRegistered();
|
||||
return mjIntent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to start a mjolnir, and notify its management room of any failure to start.
|
||||
* Will be added to `this.unstartedMjolnirs` if we fail to start it AND it is not already running.
|
||||
* @param mjolnirRecord The record for the mjolnir that we want to start.
|
||||
*/
|
||||
public async startDraupnir(mjolnirRecord: MjolnirRecord): Promise<ActionResult<void>> {
|
||||
const clientUserID = this.draupnirMXID(mjolnirRecord);
|
||||
if (this.baseManager.isDraupnirListening(clientUserID)) {
|
||||
throw new TypeError(`${mjolnirRecord.local_part} is already running, we cannot start it.`);
|
||||
}
|
||||
const mjIntent = await this.makeMatrixIntent(mjolnirRecord.local_part);
|
||||
const access = this.accessControl.getUserAccess(mjolnirRecord.owner);
|
||||
if (access.outcome !== Access.Allowed) {
|
||||
// Don't await, we don't want to clobber initialization just because we can't tell someone they're no longer allowed.
|
||||
Task((async () => {
|
||||
mjIntent.matrixClient.sendNotice(mjolnirRecord.management_room, `Your draupnir has been disabled by the administrator: ${access.rule?.reason ?? "no reason supplied"}`);
|
||||
})());
|
||||
this.baseManager.reportUnstartedDraupnir(DraupnirFailType.Unauthorized, access.outcome, clientUserID);
|
||||
decrementGaugeValue(this.instanceCountGauge, "online", mjolnirRecord.local_part);
|
||||
incrementGaugeValue(this.instanceCountGauge, "disabled", mjolnirRecord.local_part);
|
||||
return ActionError.Result(`Tried to start a draupnir that has been disabled by the administrator: ${access.rule?.reason ?? 'no reason supplied'}`);
|
||||
} else {
|
||||
const startResult = await this.makeInstance(
|
||||
mjolnirRecord.local_part,
|
||||
mjolnirRecord.owner,
|
||||
mjolnirRecord.management_room,
|
||||
mjIntent.matrixClient,
|
||||
).catch((e) => {
|
||||
log.error(`Could not start mjolnir ${mjolnirRecord.local_part} for ${mjolnirRecord.owner}:`, e);
|
||||
this.baseManager.reportUnstartedDraupnir(DraupnirFailType.StartError, e, clientUserID);
|
||||
return ActionException.Result(`Could not start draupnir ${clientUserID} for owner ${mjolnirRecord.owner}`, {
|
||||
exception: e,
|
||||
exceptionKind: ActionExceptionKind.Unknown
|
||||
})
|
||||
});
|
||||
if (isError(startResult)) {
|
||||
// Don't await, we don't want to clobber initialization if this fails.
|
||||
Task((async () => {
|
||||
mjIntent.matrixClient.sendNotice(mjolnirRecord.management_room, `Your draupnir could not be started. Please alert the administrator`);
|
||||
})());
|
||||
decrementGaugeValue(this.instanceCountGauge, "online", mjolnirRecord.local_part);
|
||||
incrementGaugeValue(this.instanceCountGauge, "offline", mjolnirRecord.local_part);
|
||||
return startResult;
|
||||
}
|
||||
return Ok(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We need to check that an owner still has access to the appservice each time they send a command to the mjolnir or use the web api.
|
||||
// https://github.com/matrix-org/mjolnir/issues/410
|
||||
/**
|
||||
* Used at startup to create all the ManagedMjolnir instances and start them so that they will respond to users.
|
||||
*/
|
||||
public async startDraupnirs(mjolnirRecords: MjolnirRecord[]): Promise<void> {
|
||||
for (const mjolnirRecord of mjolnirRecords) {
|
||||
await this.startDraupnir(mjolnirRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createFirstList(draupnir: Draupnir, draupnirOwnerID: StringUserID, shortcode: string): Promise<ActionResult<void>> {
|
||||
const policyRoom = await draupnir.policyRoomManager.createPolicyRoom(
|
||||
shortcode,
|
||||
[draupnirOwnerID],
|
||||
{ name: `${draupnirOwnerID}'s policy room` }
|
||||
);
|
||||
if (isError(policyRoom)) {
|
||||
throw policyRoom.error;
|
||||
}
|
||||
const addRoomResult = await draupnir.protectedRoomsSet.protectedRoomsConfig.addRoom(policyRoom.ok);
|
||||
if (isError(addRoomResult)) {
|
||||
return addRoomResult;
|
||||
}
|
||||
return await draupnir.protectedRoomsSet.issuerManager.watchList(PropagationType.Direct, policyRoom.ok, {});
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
import { Request, WeakEvent, Bridge, Intent, Logger } from "matrix-appservice-bridge";
|
||||
import { getProvisionedMjolnirConfig } from "../config";
|
||||
import { MatrixClient, UserID } from "matrix-bot-sdk";
|
||||
import { DataStore, MjolnirRecord } from "./datastore";
|
||||
import { AccessControl } from "./AccessControl";
|
||||
import { randomUUID } from "crypto";
|
||||
import EventEmitter from "events";
|
||||
import { Gauge } from "prom-client";
|
||||
import { decrementGaugeValue, incrementGaugeValue } from "../utils";
|
||||
import { makeDraupnirBotModeFromConfig } from "../DraupnirBotMode";
|
||||
import { Access, MatrixRoomID, PropagationType, StringRoomID, StringUserID, isError, isStringRoomID } from "matrix-protection-suite";
|
||||
import { Draupnir } from "../Draupnir";
|
||||
import { MatrixEmitter } from "matrix-protection-suite-for-matrix-bot-sdk";
|
||||
|
||||
const log = new Logger('MjolnirManager');
|
||||
|
||||
// FIXME: AAAAAAAAaaaaaaaaaaaaa there's some inconsistency between "mjolnir id" "mjolnirRecord.localpart" and "user if of the mjolnir"
|
||||
// all over this file.
|
||||
|
||||
/**
|
||||
* The MjolnirManager is responsible for:
|
||||
* * Provisioning new mjolnir instances.
|
||||
* * Starting mjolnirs when the appservice is brought online.
|
||||
* * Informing mjolnirs about new events.
|
||||
*/
|
||||
export class MjolnirManager {
|
||||
private readonly mjolnirs: Map</*the user id of the mjolnir*/string, ManagedDraupnir> = new Map();
|
||||
private readonly unstartedMjolnirs: Map</** user id of the mjolnir */string, UnstartedMjolnir> = new Map();
|
||||
|
||||
private constructor(
|
||||
private readonly dataStore: DataStore,
|
||||
private readonly bridge: Bridge,
|
||||
private readonly accessControl: AccessControl,
|
||||
private readonly instanceCountGauge: Gauge<"status" | "uuid">
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the mjolnir manager from the datastore and the access control.
|
||||
* @param dataStore The data store interface that has the details for provisioned mjolnirs.
|
||||
* @param bridge The bridge abstraction that encapsulates details about the appservice.
|
||||
* @param accessControl Who has access to the bridge.
|
||||
* @returns A new mjolnir manager.
|
||||
*/
|
||||
public static async makeMjolnirManager(dataStore: DataStore, bridge: Bridge, accessControl: AccessControl, instanceCountGauge: Gauge<"status" | "uuid">): Promise<MjolnirManager> {
|
||||
const mjolnirManager = new MjolnirManager(dataStore, bridge, accessControl, instanceCountGauge);
|
||||
await mjolnirManager.startMjolnirs(await dataStore.list());
|
||||
return mjolnirManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new mjolnir for a user.
|
||||
* @param requestingUserID The user that is requesting this mjolnir and who will own it.
|
||||
* @param managementRoomId An existing matrix room to act as the management room.
|
||||
* @param client A client for the appservice virtual user that the new mjolnir should use.
|
||||
* @returns A new managed mjolnir.
|
||||
*/
|
||||
public async makeInstance(localPart: string, requestingUserID: StringUserID, managementRoomID: StringRoomID, client: MatrixClient): Promise<ManagedDraupnir> {
|
||||
const mxid = await client.getUserId();
|
||||
const intentListener = new MatrixIntentListener(mxid);
|
||||
const managedMjolnir = new ManagedDraupnir(
|
||||
requestingUserID,
|
||||
await makeDraupnirBotModeFromConfig(
|
||||
client,
|
||||
intentListener,
|
||||
getProvisionedMjolnirConfig(managementRoomID)
|
||||
),
|
||||
intentListener,
|
||||
);
|
||||
await managedMjolnir.start();
|
||||
this.mjolnirs.set(mxid, managedMjolnir);
|
||||
this.unstartedMjolnirs.delete(mxid);
|
||||
incrementGaugeValue(this.instanceCountGauge, "offline", localPart);
|
||||
decrementGaugeValue(this.instanceCountGauge, "disabled", localPart);
|
||||
incrementGaugeValue(this.instanceCountGauge, "online", localPart);
|
||||
return managedMjolnir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a mjolnir for the corresponding mxid that is owned by a specific user.
|
||||
* @param mjolnirID The mxid of the mjolnir we are trying to get.
|
||||
* @param ownerID The owner of the mjolnir. We ask for it explicitly to not leak access to another user's mjolnir.
|
||||
* @returns The matching managed mjolnir instance.
|
||||
*/
|
||||
public getMjolnir(mjolnirID: StringUserID, ownerID: StringUserID): ManagedDraupnir | undefined {
|
||||
const mjolnir = this.mjolnirs.get(mjolnirID);
|
||||
if (mjolnir) {
|
||||
if (mjolnir.ownerID !== ownerID) {
|
||||
throw new Error(`${mjolnirID} is owned by a different user to ${ownerID}`);
|
||||
} else {
|
||||
return mjolnir;
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all of the mjolnirs that are owned by this specific user.
|
||||
* @param ownerID An owner of multiple mjolnirs.
|
||||
* @returns Any mjolnirs that they own.
|
||||
*/
|
||||
public getOwnedMjolnirs(ownerID: StringUserID): ManagedDraupnir[] {
|
||||
// TODO we need to use the database for this but also provide the utility
|
||||
// for going from a MjolnirRecord to a ManagedMjolnir.
|
||||
// https://github.com/matrix-org/mjolnir/issues/409
|
||||
return [...this.mjolnirs.values()].filter(mjolnir => mjolnir.ownerID !== ownerID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener that should be setup and called by `MjolnirAppService` while listening to the bridge abstraction provided by matrix-appservice-bridge.
|
||||
*/
|
||||
public onEvent(request: Request<WeakEvent>) {
|
||||
// TODO We need a way to map a room id (that the event is from) to a set of managed mjolnirs that should be informed.
|
||||
// https://github.com/matrix-org/mjolnir/issues/412
|
||||
[...this.mjolnirs.values()].forEach((mj: ManagedDraupnir) => mj.onEvent(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* provision a new mjolnir for a matrix user.
|
||||
* @param requestingUserID The mxid of the user we are creating a mjolnir for.
|
||||
* @returns The matrix id of the new mjolnir and its management room.
|
||||
*/
|
||||
public async provisionNewMjolnir(requestingUserID: StringUserID): Promise<[StringUserID, StringRoomID]> {
|
||||
const access = this.accessControl.getUserAccess(requestingUserID);
|
||||
if (access.outcome !== Access.Allowed) {
|
||||
throw new Error(`${requestingUserID} tried to provision a mjolnir when they do not have access ${access.outcome} ${access.rule?.reason ?? 'no reason specified'}`);
|
||||
}
|
||||
const provisionedMjolnirs = await this.dataStore.lookupByOwner(requestingUserID);
|
||||
if (provisionedMjolnirs.length === 0) {
|
||||
const mjolnirLocalPart = `draupnir_${randomUUID()}`;
|
||||
const mjIntent = await this.makeMatrixIntent(mjolnirLocalPart);
|
||||
|
||||
const managementRoomID = await mjIntent.matrixClient.createRoom({
|
||||
preset: 'private_chat',
|
||||
invite: [requestingUserID],
|
||||
name: `${requestingUserID}'s Draupnir`,
|
||||
power_level_content_override: {
|
||||
users: {
|
||||
[requestingUserID]: 100,
|
||||
// Give the mjolnir a higher PL so that can avoid issues with managing the management room.
|
||||
[await mjIntent.matrixClient.getUserId()]: 101
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!isStringRoomID(managementRoomID)) {
|
||||
throw new TypeError(`${managementRoomID} malformed managmentRoomID`);
|
||||
}
|
||||
|
||||
const mjolnir = await this.makeInstance(mjolnirLocalPart, requestingUserID, managementRoomID, mjIntent.matrixClient);
|
||||
await mjolnir.createFirstList(requestingUserID, "list");
|
||||
|
||||
await this.dataStore.store({
|
||||
local_part: mjolnirLocalPart,
|
||||
owner: requestingUserID,
|
||||
management_room: managementRoomID,
|
||||
});
|
||||
|
||||
return [mjIntent.userId as StringUserID, managementRoomID as StringRoomID];
|
||||
} else {
|
||||
throw new Error(`User: ${requestingUserID} has already provisioned ${provisionedMjolnirs.length} Draupnirs.`);
|
||||
}
|
||||
}
|
||||
|
||||
public reportUnstartedMjolnir(code: UnstartedMjolnir.FailCode, cause: any, mjolnirRecord: MjolnirRecord, mxid: string): void {
|
||||
this.unstartedMjolnirs.set(mjolnirRecord.local_part, new UnstartedMjolnir(mjolnirRecord, new UserID(mxid), code, cause));
|
||||
}
|
||||
|
||||
public getUnstartedMjolnirs(): UnstartedMjolnir[] {
|
||||
return [...this.unstartedMjolnirs.values()];
|
||||
}
|
||||
|
||||
public findUnstartedMjolnir(localPart: string): UnstartedMjolnir | undefined {
|
||||
return [...this.unstartedMjolnirs.values()].find(unstarted => unstarted.mjolnirRecord.local_part === localPart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility that creates a matrix client for a virtual user on our homeserver with the specified loclapart.
|
||||
* @param localPart The localpart of the virtual user we need a client for.
|
||||
* @returns A bridge intent with the complete mxid of the virtual user and a MatrixClient.
|
||||
*/
|
||||
private async makeMatrixIntent(localPart: string): Promise<Intent> {
|
||||
const mjIntent = this.bridge.getIntentFromLocalpart(localPart);
|
||||
await mjIntent.ensureRegistered();
|
||||
return mjIntent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to start a mjolnir, and notify its management room of any failure to start.
|
||||
* Will be added to `this.unstartedMjolnirs` if we fail to start it AND it is not already running.
|
||||
* @param mjolnirRecord The record for the mjolnir that we want to start.
|
||||
*/
|
||||
public async startMjolnir(mjolnirRecord: MjolnirRecord): Promise<void> {
|
||||
// if a mjolnir is in `this.mjonirs` it is started, as if it is present, it is going to be given Matrix events.
|
||||
if (this.mjolnirs.has(mjolnirRecord.local_part)) {
|
||||
throw new TypeError(`${mjolnirRecord.local_part} is already running, we cannot start it.`);
|
||||
}
|
||||
const mjIntent = await this.makeMatrixIntent(mjolnirRecord.local_part);
|
||||
const access = this.accessControl.getUserAccess(mjolnirRecord.owner);
|
||||
if (access.outcome !== Access.Allowed) {
|
||||
// Don't await, we don't want to clobber initialization just because we can't tell someone they're no longer allowed.
|
||||
mjIntent.matrixClient.sendNotice(mjolnirRecord.management_room, `Your mjolnir has been disabled by the administrator: ${access.rule?.reason ?? "no reason supplied"}`);
|
||||
this.reportUnstartedMjolnir(UnstartedMjolnir.FailCode.Unauthorized, access.outcome, mjolnirRecord, mjIntent.userId);
|
||||
decrementGaugeValue(this.instanceCountGauge, "online", mjolnirRecord.local_part);
|
||||
incrementGaugeValue(this.instanceCountGauge, "disabled", mjolnirRecord.local_part);
|
||||
} else {
|
||||
await this.makeInstance(
|
||||
mjolnirRecord.local_part,
|
||||
mjolnirRecord.owner,
|
||||
mjolnirRecord.management_room,
|
||||
mjIntent.matrixClient,
|
||||
).catch((e: any) => {
|
||||
log.error(`Could not start mjolnir ${mjolnirRecord.local_part} for ${mjolnirRecord.owner}:`, e);
|
||||
// Don't await, we don't want to clobber initialization if this fails.
|
||||
mjIntent.matrixClient.sendNotice(mjolnirRecord.management_room, `Your mjolnir could not be started. Please alert the administrator`);
|
||||
this.reportUnstartedMjolnir(UnstartedMjolnir.FailCode.StartError, e, mjolnirRecord, mjIntent.userId);
|
||||
decrementGaugeValue(this.instanceCountGauge, "online", mjolnirRecord.local_part);
|
||||
incrementGaugeValue(this.instanceCountGauge, "offline", mjolnirRecord.local_part);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We need to check that an owner still has access to the appservice each time they send a command to the mjolnir or use the web api.
|
||||
// https://github.com/matrix-org/mjolnir/issues/410
|
||||
/**
|
||||
* Used at startup to create all the ManagedMjolnir instances and start them so that they will respond to users.
|
||||
*/
|
||||
public async startMjolnirs(mjolnirRecords: MjolnirRecord[]): Promise<void> {
|
||||
for (const mjolnirRecord of mjolnirRecords) {
|
||||
await this.startMjolnir(mjolnirRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This isn't acceptable really, we need a managerManager that
|
||||
// shares the same caches internally but uses different clients to do things.
|
||||
// (Within MPS).
|
||||
export class ManagedDraupnir {
|
||||
public constructor(
|
||||
public readonly ownerID: StringUserID,
|
||||
private readonly draupnir: Draupnir,
|
||||
private readonly matrixEmitter: MatrixIntentListener,
|
||||
) { }
|
||||
|
||||
public async onEvent(request: Request<WeakEvent>) {
|
||||
this.matrixEmitter.handleEvent(request.getData());
|
||||
}
|
||||
|
||||
public async joinRoom(roomId: string) {
|
||||
await this.draupnir.client.joinRoom(roomId);
|
||||
}
|
||||
public async addProtectedRoom(room: MatrixRoomID) {
|
||||
await this.draupnir.protectedRoomsSet.protectedRoomsConfig.addRoom(room);
|
||||
}
|
||||
|
||||
public async createFirstList(draupnirOwnerID: StringUserID, shortcode: string) {
|
||||
const policyRoom = await this.draupnir.managerManager.policyRoomManager.createPolicyRoom(
|
||||
shortcode,
|
||||
[draupnirOwnerID],
|
||||
{ name: `${draupnirOwnerID}'s policy room` }
|
||||
);
|
||||
if (isError(policyRoom)) {
|
||||
throw policyRoom.error;
|
||||
}
|
||||
await this.addProtectedRoom(policyRoom.ok);
|
||||
return await this.draupnir.protectedRoomsSet.issuerManager.watchList(PropagationType.Direct, policyRoom.ok, {});
|
||||
}
|
||||
|
||||
public get managementRoomID(): StringRoomID {
|
||||
return this.draupnir.managementRoomID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intended to be called by the MjolnirManager to make sure the mjolnir is ready to listen to events.
|
||||
* This managed mjolnir should not be informed of any events via `onEvent` until `start` is called.
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
await this.draupnir.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to listen for events intended for a single mjolnir that resides in the appservice.
|
||||
* This exists entirely because the Mjolnir class was previously designed only to receive events
|
||||
* from a syncing matrix-bot-sdk MatrixClient. Since appservices provide a transactional push
|
||||
* api for all users on the appservice, almost the opposite of sync, we needed to create an
|
||||
* interface for both. See `MatrixEmitter`.
|
||||
*/
|
||||
export class MatrixIntentListener extends EventEmitter implements MatrixEmitter {
|
||||
constructor(private readonly mjolnirId: string) {
|
||||
super()
|
||||
}
|
||||
|
||||
public handleEvent(mxEvent: WeakEvent) {
|
||||
// These are ordered to be the same as matrix-bot-sdk's MatrixClient
|
||||
// They shouldn't need to be, but they are just in case it matters.
|
||||
if (mxEvent['type'] === 'm.room.member' && mxEvent.state_key === this.mjolnirId) {
|
||||
if (mxEvent['content']['membership'] === 'leave') {
|
||||
this.emit('room.leave', mxEvent.room_id, mxEvent);
|
||||
}
|
||||
if (mxEvent['content']['membership'] === 'invite') {
|
||||
this.emit('room.invite', mxEvent.room_id, mxEvent);
|
||||
}
|
||||
if (mxEvent['content']['membership'] === 'join') {
|
||||
this.emit('room.join', mxEvent.room_id, mxEvent);
|
||||
}
|
||||
}
|
||||
if (mxEvent.type === 'm.room.message') {
|
||||
this.emit('room.message', mxEvent.room_id, mxEvent);
|
||||
}
|
||||
if (mxEvent.type === 'm.room.tombstone' && mxEvent.state_key === '') {
|
||||
this.emit('room.archived', mxEvent.room_id, mxEvent);
|
||||
}
|
||||
this.emit('room.event', mxEvent.room_id, mxEvent);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called by `Mjolnir`.
|
||||
*/
|
||||
public async start() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called by `Mjolnir`.
|
||||
*/
|
||||
public stop() {
|
||||
// Nothing to do.
|
||||
}
|
||||
}
|
||||
|
||||
export class UnstartedMjolnir {
|
||||
constructor(
|
||||
public readonly mjolnirRecord: MjolnirRecord,
|
||||
public readonly mxid: UserID,
|
||||
public readonly failCode: UnstartedMjolnir.FailCode,
|
||||
public readonly cause: any,
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export namespace UnstartedMjolnir {
|
||||
export enum FailCode {
|
||||
Unauthorized = "Unauthorized",
|
||||
StartError = "StartError",
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ const listUnstarted = defineInterfaceCommand<AppserviceBaseExecutor>({
|
||||
table: "appservice bot",
|
||||
parameters: parameters([]),
|
||||
command: async function () {
|
||||
return Ok(this.appservice.mjolnirManager.getUnstartedMjolnirs());
|
||||
return Ok(this.appservice.draupnirManager.getUnstartedMjolnirs());
|
||||
},
|
||||
summary: "List any Mjolnir that failed to start."
|
||||
});
|
||||
@@ -81,7 +81,7 @@ const restart = defineInterfaceCommand<AppserviceBaseExecutor>({
|
||||
}
|
||||
]),
|
||||
command: async function (this, _keywords, mjolnirId: UserID): Promise<ActionResult<true>> {
|
||||
const mjolnirManager = this.appservice.mjolnirManager;
|
||||
const mjolnirManager = this.appservice.draupnirManager;
|
||||
const mjolnir = mjolnirManager.findUnstartedMjolnir(mjolnirId.localpart);
|
||||
if (mjolnir?.mjolnirRecord === undefined) {
|
||||
return ActionError.Result(`We can't find the unstarted mjolnir ${mjolnirId}, is it running?`);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (C) 2022-2023 Gnuxie <Gnuxie@protonmail.com>
|
||||
* Copyright (C) 2022-2024 Gnuxie <Gnuxie@protonmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is modified and is NOT licensed under the Apache License.
|
||||
@@ -25,19 +25,19 @@ limitations under the License.
|
||||
* are NOT distributed, contributed, committed, or licensed under the Apache License.
|
||||
*/
|
||||
|
||||
import { ActionError, ActionResult, MatrixRoomID, StandardClientsInRoomMap, StringUserID, isError } from "matrix-protection-suite";
|
||||
import { ActionError, ActionResult, ClientsInRoomMap, MatrixRoomID, RoomEvent, StringRoomID, StringUserID, isError } from "matrix-protection-suite";
|
||||
import { IConfig } from "../config";
|
||||
import { DraupnirFactory } from "./DraupnirFactory";
|
||||
import { Draupnir } from "../Draupnir";
|
||||
|
||||
export abstract class StandardDraupnirManager {
|
||||
export class StandardDraupnirManager {
|
||||
private readonly readyDraupnirs = new Map<StringUserID, Draupnir>();
|
||||
private readonly listeningDraupnirs = new Map<StringUserID, Draupnir>();
|
||||
private readonly clientsInRooms = new StandardClientsInRoomMap();
|
||||
private readonly failedDraupnirs = new Map<StringUserID, UnstartedDraupnir>();
|
||||
|
||||
public constructor(
|
||||
protected readonly draupnirFactory: DraupnirFactory
|
||||
protected readonly draupnirFactory: DraupnirFactory,
|
||||
private readonly clientsInRooms: ClientsInRoomMap
|
||||
) {
|
||||
// nothing to do.
|
||||
}
|
||||
@@ -58,11 +58,11 @@ export abstract class StandardDraupnirManager {
|
||||
return ActionError.Result(`There is a draupnir for ${clientUserID} already running`);
|
||||
}
|
||||
if (isError(draupnir)) {
|
||||
this.failedDraupnirs.set(clientUserID, new UnstartedDraupnir(
|
||||
clientUserID,
|
||||
this.reportUnstartedDraupnir(
|
||||
DraupnirFailType.InitializationError,
|
||||
draupnir.error
|
||||
))
|
||||
draupnir.error,
|
||||
clientUserID
|
||||
);
|
||||
return draupnir;
|
||||
}
|
||||
this.readyDraupnirs.set(clientUserID, draupnir.ok);
|
||||
@@ -70,6 +70,34 @@ export abstract class StandardDraupnirManager {
|
||||
return draupnir;
|
||||
}
|
||||
|
||||
public isDraupnirReady(draupnirClientID: StringUserID): boolean {
|
||||
return this.readyDraupnirs.has(draupnirClientID);
|
||||
}
|
||||
|
||||
public isDraupnirListening(draupnirClientID: StringUserID): boolean {
|
||||
return this.listeningDraupnirs.has(draupnirClientID);
|
||||
}
|
||||
|
||||
public isDraupnirFailed(draupnirClientID: StringUserID): boolean {
|
||||
return this.failedDraupnirs.has(draupnirClientID);
|
||||
}
|
||||
|
||||
public reportUnstartedDraupnir(failType: DraupnirFailType, cause: unknown, draupnirClientID: StringUserID): void {
|
||||
this.failedDraupnirs.set(draupnirClientID, new UnstartedDraupnir(draupnirClientID, failType, cause));
|
||||
}
|
||||
|
||||
public getUnstartedDraupnirs(): UnstartedDraupnir[] {
|
||||
return [...this.failedDraupnirs.values()];
|
||||
}
|
||||
|
||||
public findUnstartedDraupnir(draupnirClientID: StringUserID): UnstartedDraupnir | undefined {
|
||||
return this.failedDraupnirs.get(draupnirClientID);
|
||||
}
|
||||
|
||||
public findRunningDraupnir(draupnirClientID: StringUserID): Draupnir | undefined {
|
||||
return this.listeningDraupnirs.get(draupnirClientID);
|
||||
}
|
||||
|
||||
public startDraupnir(
|
||||
clientUserID: StringUserID
|
||||
): void {
|
||||
@@ -94,6 +122,10 @@ export abstract class StandardDraupnirManager {
|
||||
this.readyDraupnirs.set(clientUserID, draupnir);
|
||||
}
|
||||
}
|
||||
|
||||
public handleTimelineEvent(roomID: StringRoomID, event: RoomEvent): void {
|
||||
this.clientsInRooms.handleTimelineEvent(roomID, event);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnstartedDraupnir {
|
||||
|
||||
Reference in New Issue
Block a user