Plumb in SynapseHTTPAntispam to RoomTakedownProtection room discovery.

This commit is contained in:
gnuxie
2025-03-21 16:19:08 +00:00
parent 7b212d75aa
commit 3f00346ae6
2 changed files with 143 additions and 8 deletions
@@ -0,0 +1,106 @@
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
//
// SPDX-License-Identifier: Apache-2.0
import {
ConstantPeriodItemBatch,
isError,
Logger,
SHA256RoomHashStore,
StandardBatcher,
} from "matrix-protection-suite";
import { CheckEventForSpamRequestBody } from "../../webapis/SynapseHTTPAntispam/CheckEventForSpamEndpoint";
import { SynapseHttpAntispam } from "../../webapis/SynapseHTTPAntispam/SynapseHttpAntispam";
import { StringRoomID } from "@the-draupnir-project/matrix-basic-types";
import { UserMayInviteRequestBody } from "../../webapis/SynapseHTTPAntispam/UserMayInviteEndpoint";
import { UserMayJoinRoomRequestBody } from "../../webapis/SynapseHTTPAntispam/UserMayJoinRoomEndpoint";
const log = new Logger("SynapseHTTPAntispamRoomDiscovery");
export interface RoomDiscovery {
unregisterListeners(): void;
}
export class SynapseHTTPAntispamRoomDiscovery implements RoomDiscovery {
private readonly discoveredRooms = new Set<StringRoomID>();
private readonly batcher = new StandardBatcher(
() =>
new ConstantPeriodItemBatch<StringRoomID, void>(
this.forwardDiscoveredBatch,
{ waitPeriodMS: 2500 }
)
);
constructor(
private readonly synapseHTTPAntispam: SynapseHttpAntispam,
private readonly hashStore: SHA256RoomHashStore
) {
synapseHTTPAntispam.checkEventForSpamHandles.registerNonBlockingHandle(
this.handleCheckEventForSpam
);
synapseHTTPAntispam.userMayInviteHandles.registerNonBlockingHandle(
this.handleUserMayInvite
);
synapseHTTPAntispam.userMayJoinRoomHandles.registerNonBlockingHandle(
this.handleUserMayJoin
);
}
private readonly forwardDiscoveredBatch = async function (
this: SynapseHTTPAntispamRoomDiscovery,
rawEntries: [StringRoomID][]
): Promise<void> {
const entries = rawEntries.flat();
const storeResult = await this.hashStore.storeUndiscoveredRooms(entries);
if (isError(storeResult)) {
log.error(
"Unxpected error while trying to store undiscovered rooms",
storeResult.error
);
return;
}
for (const roomID of entries) {
this.discoveredRooms.add(roomID);
}
}.bind(this);
private readonly handleCheckEventForSpam = function (
this: SynapseHTTPAntispamRoomDiscovery,
{ event }: CheckEventForSpamRequestBody
): void {
if (event.type === "m.room.member") {
if (!this.discoveredRooms.has(event.room_id)) {
this.batcher.add(event.room_id);
}
}
}.bind(this);
private readonly handleUserMayInvite = function (
this: SynapseHTTPAntispamRoomDiscovery,
{ room_id }: UserMayInviteRequestBody
): void {
if (!this.discoveredRooms.has(room_id)) {
this.batcher.add(room_id);
}
}.bind(this);
private readonly handleUserMayJoin = function (
this: SynapseHTTPAntispamRoomDiscovery,
{ room }: UserMayJoinRoomRequestBody
): void {
if (!this.discoveredRooms.has(room)) {
this.batcher.add(room);
}
}.bind(this);
unregisterListeners(): void {
this.synapseHTTPAntispam.checkEventForSpamHandles.unregisterHandle(
this.handleCheckEventForSpam
);
this.synapseHTTPAntispam.userMayInviteHandles.unregisterHandle(
this.handleUserMayInvite
);
this.synapseHTTPAntispam.userMayJoinRoomHandles.unregisterHandle(
this.handleUserMayJoin
);
}
}
@@ -6,6 +6,7 @@ import {
AbstractProtection,
ActionResult,
describeProtection,
Logger,
Ok,
PolicyListRevision,
PolicyRuleChange,
@@ -21,12 +22,13 @@ import { Draupnir } from "../../Draupnir";
import { StandardRoomTakedown } from "./RoomTakedown";
import { RoomAuditLog } from "./RoomAuditLog";
import { SynapseAdminRoomTakedownCapability } from "../../capabilities/SynapseAdminRoomTakedown/SynapseAdminRoomTakedown";
import { ResultError } from "@gnuxie/typescript-result";
import {
RoomDiscovery,
SynapseHTTPAntispamRoomDiscovery,
} from "./RoomDiscovery";
// FIXME: We still haven't figured out how to poll for new rooms via the
// Synapse admin API.
// FIXME: We need to add the stores to draupnir somehow.
// probably from the toplevel.
const log = new Logger("RoomTakedownProtection");
type RoomTakedownProtectionCapabilities = {
roomTakedownCapability: RoomTakedownCapability;
@@ -48,7 +50,8 @@ export class RoomTakedownProtection
capabilities: RoomTakedownProtectionCapabilities,
protectedRoomsSet: ProtectedRoomsSet,
hashStore: SHA256RoomHashStore,
auditLog: RoomAuditLog
auditLog: RoomAuditLog,
private readonly roomDiscovery: RoomDiscovery | undefined
) {
super(description, capabilities, protectedRoomsSet, {});
this.roomTakedown = new StandardRoomTakedown(
@@ -69,6 +72,10 @@ export class RoomTakedownProtection
): Promise<ActionResult<void>> {
return this.roomTakedown.handlePolicyChange(revision, changes);
}
handleProtectionDisable(): void {
this.roomDiscovery?.unregisterListeners();
}
}
describeProtection<RoomTakedownProtectionCapabilities, Draupnir>({
@@ -81,13 +88,35 @@ describeProtection<RoomTakedownProtectionCapabilities, Draupnir>({
roomTakedownCapability: SynapseAdminRoomTakedownCapability.name,
},
factory(description, protectedRoomsSet, draupnir, capabilitySet, _settings) {
if (
draupnir.stores.hashStore === undefined ||
draupnir.stores.roomAuditLog === undefined
) {
return ResultError.Result(
"This protection requires a hash store and audit log to be available to draupnir, and they are not in your configuration."
);
}
const roomDiscovery = (() => {
if (draupnir.synapseHTTPAntispam !== undefined) {
return new SynapseHTTPAntispamRoomDiscovery(
draupnir.synapseHTTPAntispam,
draupnir.stores.hashStore
);
} else {
log.warn(
"synapseHTTPAntispam is not configured for this draupnir, and will not be used for room discovery"
);
return undefined;
}
})();
return Ok(
new RoomTakedownProtection(
description,
capabilitySet,
protectedRoomsSet,
draupnir.hashStore,
draupnir.auditLog
draupnir.stores.hashStore,
draupnir.stores.roomAuditLog,
roomDiscovery
)
);
},