From b8bf6c56925ede10b4e6aa707c5cfa414c2c75b7 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 12 Mar 2025 15:01:57 +0000 Subject: [PATCH] Add RoomTakedownCapability and SynapseAdmin implementation. --- src/capabilities/RoomTakedownCapability.tsx | 45 +++++++ .../RoomTakedownCapabilityRenderer.tsx | 124 ++++++++++++++++++ .../SynapseAdminRoomTakedown.ts | 64 +++++++++ src/capabilities/capabilityIndex.ts | 2 + src/protections/RoomTakedown/RoomTakedown.ts | 6 +- 5 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 src/capabilities/RoomTakedownCapability.tsx create mode 100644 src/capabilities/RoomTakedownCapabilityRenderer.tsx create mode 100644 src/capabilities/SynapseAdminRoomTakedown/SynapseAdminRoomTakedown.ts diff --git a/src/capabilities/RoomTakedownCapability.tsx b/src/capabilities/RoomTakedownCapability.tsx new file mode 100644 index 00000000..55cb3dac --- /dev/null +++ b/src/capabilities/RoomTakedownCapability.tsx @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2025 Gnuxie +// +// SPDX-License-Identifier: Apache-2.0 + +import { Result } from "@gnuxie/typescript-result"; +import { Type } from "@sinclair/typebox"; +import { + StringRoomID, + StringUserID, +} from "@the-draupnir-project/matrix-basic-types"; +import { + Capability, + CapabilityMethodSchema, + describeCapabilityInterface, +} from "matrix-protection-suite"; + +export type RoomTakedownDetails = Partial<{ + creator: StringUserID | undefined; + room_id: StringRoomID; + name: string | undefined; + topic: string | undefined; + avatar: string | undefined; +}>; + +export const RoomTakedownCapability = Type.Intersect([ + Type.Object({ + isRoomTakendown: CapabilityMethodSchema, + takedownRoom: CapabilityMethodSchema, + }), + Capability, +]); +// we'll probably want to include room details too so we can provide some +// rudinemtary information to takedown. +// I'd make the fields optional however because it may be impossible +// to get those details on conduwuit. +export type RoomTakedownCapability = { + isRoomTakendown(roomID: StringRoomID): Promise>; + takedownRoom(roomID: StringRoomID): Promise>; +} & Capability; + +describeCapabilityInterface({ + name: "RoomTakedownCapability", + description: "Capability that targets matrix rooms", + schema: RoomTakedownCapability, +}); diff --git a/src/capabilities/RoomTakedownCapabilityRenderer.tsx b/src/capabilities/RoomTakedownCapabilityRenderer.tsx new file mode 100644 index 00000000..11f53613 --- /dev/null +++ b/src/capabilities/RoomTakedownCapabilityRenderer.tsx @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2025 Gnuxie +// +// SPDX-License-Identifier: Apache-2.0 + +import { + describeCapabilityRenderer, + DescriptionMeta, +} from "matrix-protection-suite"; +import { + RoomTakedownCapability, + RoomTakedownDetails, +} from "./RoomTakedownCapability"; +import { RendererMessageCollector } from "./RendererMessageCollector"; +import { + MatrixRoomReference, + StringRoomID, +} from "@the-draupnir-project/matrix-basic-types"; +import { isError, Result } from "@gnuxie/typescript-result"; +import { + DeadDocumentJSX, + DocumentNode, +} from "@the-draupnir-project/interface-manager"; +import { renderFailedSingularConsequence } from "./CommonRenderers"; +import { renderRoomPill } from "../commands/interface-manager/MatrixHelpRenderer"; +import { Draupnir } from "../Draupnir"; + +function renderCodeOrDefault( + item: string | undefined, + defaultText: string +): DocumentNode { + return item === undefined ? ( + {defaultText} + ) : ( + {item} + ); +} + +function renderTakedown( + roomID: StringRoomID, + details: RoomTakedownDetails +): DocumentNode { + return ( +
+ + Successfully takendown the room{" "} + {renderRoomPill(MatrixRoomReference.fromRoomID(roomID))} + +
    +
  • name: {renderCodeOrDefault(details.name, "no name available")}
  • +
  • + creator:{" "} + {renderCodeOrDefault( + details.creator, + "creator information unavailable" + )} +
  • +
  • + topic: {renderCodeOrDefault(details.topic, "topic unavailable")} +
  • +
+
+ ); +} + +class StandardRoomTakedownCapabilityRenderer implements RoomTakedownCapability { + public readonly requiredEventPermissions = + this.capability.requiredEventPermissions; + public readonly requiredPermissions = this.capability.requiredPermissions; + public readonly requiredStatePermissions = + this.capability.requiredStatePermissions; + constructor( + private readonly description: DescriptionMeta, + private readonly messageCollector: RendererMessageCollector, + private readonly capability: RoomTakedownCapability + ) { + // nothing to do. + } + + public async isRoomTakendown(roomID: StringRoomID): Promise> { + // pass this one through without rendering because it doesn't really have a consequence + return await this.capability.isRoomTakendown(roomID); + } + + public async takedownRoom( + roomID: StringRoomID + ): Promise> { + const capabilityResult = await this.capability.takedownRoom(roomID); + if (isError(capabilityResult)) { + this.messageCollector.addOneliner( + this.description, + this.capability, + renderFailedSingularConsequence( + this.description, + + Failed to takedown room{" "} + {renderRoomPill(MatrixRoomReference.fromRoomID(roomID))} + , + capabilityResult.error + ) + ); + return capabilityResult; + } + this.messageCollector.addMessage( + this.description, + this.capability, + renderTakedown(roomID, capabilityResult.ok) + ); + return capabilityResult; + } +} + +describeCapabilityRenderer({ + name: "StandardRoomTakedownCapabilityRenderer", + description: "Renders the standard room takedown capability result", + interface: "RoomTakedownCapability", + isDefaultForInterface: true, + factory(description, draupnir, capability) { + return new StandardRoomTakedownCapabilityRenderer( + description, + draupnir.capabilityMessageRenderer, + capability + ); + }, +}); diff --git a/src/capabilities/SynapseAdminRoomTakedown/SynapseAdminRoomTakedown.ts b/src/capabilities/SynapseAdminRoomTakedown/SynapseAdminRoomTakedown.ts new file mode 100644 index 00000000..88bb4490 --- /dev/null +++ b/src/capabilities/SynapseAdminRoomTakedown/SynapseAdminRoomTakedown.ts @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2025 Gnuxie +// +// SPDX-License-Identifier: Apache-2.0 + +import { StringRoomID } from "@the-draupnir-project/matrix-basic-types"; +import { Logger } from "matrix-protection-suite"; +import { + RoomTakedownCapability, + RoomTakedownDetails, +} from "../RoomTakedownCapability"; +import { SynapseAdminClient } from "matrix-protection-suite-for-matrix-bot-sdk"; +import { isError, Ok, Result } from "@gnuxie/typescript-result"; + +const log = new Logger("SynapseAdminRoomTakedownCapability"); + +export class SynapseAdminRoomTakedownCapability + implements RoomTakedownCapability +{ + public readonly requiredPermissions = []; + public readonly requiredStatePermissions = []; + public readonly requiredEventPermissions = []; + public constructor(private readonly adminClient: SynapseAdminClient) { + // nothing to do mare. + } + public async isRoomTakendown(roomID: StringRoomID): Promise> { + const blockStatusResponse = await this.adminClient.getBlockStatus(roomID); + if (isError(blockStatusResponse)) { + return blockStatusResponse; + } else { + return Ok(blockStatusResponse.ok.block); + } + } + + public async takedownRoom( + roomID: StringRoomID + ): Promise> { + const detailsResponse = await this.adminClient.getRoomDetails(roomID); + let details: RoomTakedownDetails; + if (isError(detailsResponse)) { + log.warn( + "Unable to fetch details for a room being requested to shutdown", + detailsResponse.error + ); + details = { room_id: roomID }; + } else { + details = { + name: detailsResponse.ok?.name ?? undefined, + creator: detailsResponse.ok?.creator, + avatar: detailsResponse.ok?.avatar ?? undefined, + topic: detailsResponse.ok?.topic ?? undefined, + room_id: roomID, + }; + } + const takedownResult = await this.adminClient.deleteRoom(roomID, { + block: true, + purge: true, + }); + if (isError(takedownResult)) { + return takedownResult; + } else { + return Ok(details); + } + } +} diff --git a/src/capabilities/capabilityIndex.ts b/src/capabilities/capabilityIndex.ts index 8fe5f2eb..a11c57b6 100644 --- a/src/capabilities/capabilityIndex.ts +++ b/src/capabilities/capabilityIndex.ts @@ -11,3 +11,5 @@ import "./StandardEventConsequencesRenderer"; import "./ServerACLConsequencesRenderer"; import "./StandardUserConsequencesRenderer"; +import "./RoomTakedownCapability"; +import "./RoomTakedownCapabilityRenderer"; diff --git a/src/protections/RoomTakedown/RoomTakedown.ts b/src/protections/RoomTakedown/RoomTakedown.ts index d6d9504f..d63edabd 100644 --- a/src/protections/RoomTakedown/RoomTakedown.ts +++ b/src/protections/RoomTakedown/RoomTakedown.ts @@ -16,6 +16,7 @@ import { } from "matrix-protection-suite"; import { RoomAuditLog } from "./RoomAuditLog"; import { isError, Ok, Result, ResultError } from "@gnuxie/typescript-result"; +import { RoomTakedownCapability } from "../../capabilities/RoomTakedownCapability"; const log = new Logger("RoomTakedown"); @@ -28,11 +29,6 @@ type RoomTakedown = { checkAllRooms(revision: PolicyListRevision): Promise>; }; -export type RoomTakedownCapability = { - isRoomTakendown(roomID: StringRoomID): Promise>; - takedownRoom(roomID: StringRoomID): Promise>; -}; - /** * This exists as the main handler for reacting to literal room policies * (that have been reversed from hashed policies elsewhere) AND