Add RoomTakedownCapability and SynapseAdmin implementation.

This commit is contained in:
gnuxie committed 2025-03-21 16:19:08 +00:00
1 parent 7282374caf
commit b8bf6c5692
5 files changed
+236 -5

No files matched your search

@@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
//
// 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<Result<boolean>>;
takedownRoom(roomID: StringRoomID): Promise<Result<RoomTakedownDetails>>;
} & Capability;
describeCapabilityInterface({
name: "RoomTakedownCapability",
description: "Capability that targets matrix rooms",
schema: RoomTakedownCapability,
});
@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
//
// 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 ? (
<fragment>{defaultText}</fragment>
) : (
<code>{item}</code>
);
}
function renderTakedown(
roomID: StringRoomID,
details: RoomTakedownDetails
): DocumentNode {
return (
<details>
<summary>
Successfully takendown the room{" "}
{renderRoomPill(MatrixRoomReference.fromRoomID(roomID))}
</summary>
<ul>
<li>name: {renderCodeOrDefault(details.name, "no name available")}</li>
<li>
creator:{" "}
{renderCodeOrDefault(
details.creator,
"creator information unavailable"
)}
</li>
<li>
topic: {renderCodeOrDefault(details.topic, "topic unavailable")}
</li>
</ul>
</details>
);
}
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<Result<boolean>> {
// 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<Result<RoomTakedownDetails>> {
const capabilityResult = await this.capability.takedownRoom(roomID);
if (isError(capabilityResult)) {
this.messageCollector.addOneliner(
this.description,
this.capability,
renderFailedSingularConsequence(
this.description,
<span>
Failed to takedown room{" "}
{renderRoomPill(MatrixRoomReference.fromRoomID(roomID))}
</span>,
capabilityResult.error
)
);
return capabilityResult;
}
this.messageCollector.addMessage(
this.description,
this.capability,
renderTakedown(roomID, capabilityResult.ok)
);
return capabilityResult;
}
}
describeCapabilityRenderer<RoomTakedownCapability, Draupnir>({
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
);
},
});
@@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
//
// 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<Result<boolean>> {
const blockStatusResponse = await this.adminClient.getBlockStatus(roomID);
if (isError(blockStatusResponse)) {
return blockStatusResponse;
} else {
return Ok(blockStatusResponse.ok.block);
}
}
public async takedownRoom(
roomID: StringRoomID
): Promise<Result<RoomTakedownDetails>> {
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);
}
}
}
+2
View File
@@ -11,3 +11,5 @@
import "./StandardEventConsequencesRenderer";
import "./ServerACLConsequencesRenderer";
import "./StandardUserConsequencesRenderer";
import "./RoomTakedownCapability";
import "./RoomTakedownCapabilityRenderer";
+1 -5
View File
@@ -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<Result<void>>;
};
export type RoomTakedownCapability = {
isRoomTakendown(roomID: StringRoomID): Promise<Result<boolean>>;
takedownRoom(roomID: StringRoomID): Promise<Result<void>>;
};
/**
* This exists as the main handler for reacting to literal room policies
* (that have been reversed from hashed policies elsewhere) AND