From 33e649c508120fbf72773ef747f59ff9b9122610 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Thu, 13 Mar 2025 15:43:50 +0000 Subject: [PATCH 1/4] Add library code to support synapse-http-antispam. We now need to add config, plumbing, and tests. --- .../CheckEventForSpamEndpoint.ts | 86 +++++++++++++++++ .../SpamCheckEndpointPluginManager.ts | 93 +++++++++++++++++++ .../SynapseHttpAntispam.ts | 88 ++++++++++++++++++ .../UserMayInviteEndpoint.ts | 89 ++++++++++++++++++ .../UserMayJoinRoomEndpoint.ts | 85 +++++++++++++++++ 5 files changed, 441 insertions(+) create mode 100644 src/webapis/SynapseHTTPAntispam/CheckEventForSpamEndpoint.ts create mode 100644 src/webapis/SynapseHTTPAntispam/SpamCheckEndpointPluginManager.ts create mode 100644 src/webapis/SynapseHTTPAntispam/SynapseHttpAntispam.ts create mode 100644 src/webapis/SynapseHTTPAntispam/UserMayInviteEndpoint.ts create mode 100644 src/webapis/SynapseHTTPAntispam/UserMayJoinRoomEndpoint.ts diff --git a/src/webapis/SynapseHTTPAntispam/CheckEventForSpamEndpoint.ts b/src/webapis/SynapseHTTPAntispam/CheckEventForSpamEndpoint.ts new file mode 100644 index 00000000..9ab42682 --- /dev/null +++ b/src/webapis/SynapseHTTPAntispam/CheckEventForSpamEndpoint.ts @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2025 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { Type } from "@sinclair/typebox"; +import { + EDStatic, + isError, + Logger, + RoomEvent, + Task, + Value, +} from "matrix-protection-suite"; +import { SpamCheckEndpointPluginManager } from "./SpamCheckEndpointPluginManager"; +import { Request, Response } from "express"; + +const log = new Logger("CheckEventForSpamEndpoint"); + +export type CheckEventForSpamListenerArguments = Parameters< + (details: CheckEventForSpamRequestBody) => void +>; + +type CheckEventForSpamRequestBody = EDStatic< + typeof CheckEventForSpamRequestBody +>; +const CheckEventForSpamRequestBody = Type.Object({ + event: RoomEvent(Type.Unknown()), +}); + +export class CheckEventForSpamEndpoint { + public constructor( + private readonly pluginManager: SpamCheckEndpointPluginManager + ) { + // nothing to do. + } + + private async handleCheckEventForSpamAsync( + request: Request, + response: Response, + isResponded: boolean + ): Promise { + const decodedBody = Value.Decode( + CheckEventForSpamRequestBody, + request.body + ); + if (isError(decodedBody)) { + log.error("Error decoding request body:", decodedBody.error); + if (!isResponded && this.pluginManager.isBlocking()) { + response + .status(400) + .send({ errcode: "M_INVALID_PARAM", error: "Error handling event" }); + } + return; + } + if (!isResponded && this.pluginManager.isBlocking()) { + const blockingResult = await this.pluginManager.callBlockingHandles( + decodedBody.ok + ); + if (blockingResult === "NOT_SPAM") { + response.status(200); + response.send({}); + } else { + response.status(400); + response.send(blockingResult); + } + } else if (!isResponded) { + response.status(200); + response.send({}); + } + this.pluginManager.callNonBlockingHandlesInTask(decodedBody.ok); + } + + public handleCheckEventForSpam(request: Request, response: Response): void { + if (!this.pluginManager.isBlocking()) { + response.status(200); + response.send({}); + } + void Task( + this.handleCheckEventForSpamAsync( + request, + response, + !this.pluginManager.isBlocking() + ) + ); + } +} diff --git a/src/webapis/SynapseHTTPAntispam/SpamCheckEndpointPluginManager.ts b/src/webapis/SynapseHTTPAntispam/SpamCheckEndpointPluginManager.ts new file mode 100644 index 00000000..97efd322 --- /dev/null +++ b/src/webapis/SynapseHTTPAntispam/SpamCheckEndpointPluginManager.ts @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2025 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { Logger, Task } from "matrix-protection-suite"; + +type BlockingResponse = + | "NOT_SPAM" + | { + errcode: string; + error: string; + }; + +const log = new Logger("SpamCheckEndpointPluginManager"); + +export type BlockingCallback = ( + ...args: CBArguments +) => Promise; +export type NonBlockingCallback = ( + ...args: CBArguments +) => void; + +export class SpamCheckEndpointPluginManager { + private readonly blockingHandles = new Set>(); + private readonly nonBlockingHandles = new Set< + NonBlockingCallback + >(); + + public registerBlockingHandle(handle: BlockingCallback): void { + this.blockingHandles.add(handle); + } + + public registerNonBlockingHandle( + handle: NonBlockingCallback + ): void { + this.nonBlockingHandles.add(handle); + } + + public unregisterHandle( + handle: BlockingCallback | NonBlockingCallback + ): void { + this.blockingHandles.delete(handle as BlockingCallback); + this.nonBlockingHandles.delete(handle as NonBlockingCallback); + } + + public unregisterListeners(): void { + this.blockingHandles.clear(); + this.nonBlockingHandles.clear(); + } + + public isBlocking(): boolean { + return this.blockingHandles.size > 0; + } + + public async callBlockingHandles( + ...args: CBArguments + ): ReturnType> { + const results = await Promise.allSettled( + [...this.blockingHandles.values()].map((handle) => handle(...args)) + ); + for (const result of results) { + if (result.status === "rejected") { + log.error( + "Error processing a blocking spam check callback:", + result.reason + ); + } else { + if (result.value !== "NOT_SPAM") { + return result.value; + } + } + } + return "NOT_SPAM"; + } + + public callNonBlockingHandles(...args: CBArguments): void { + for (const handle of this.nonBlockingHandles) { + try { + handle(...args); + } catch (e) { + log.error("Error processing a non blocking spam check callback:", e); + } + } + } + + public callNonBlockingHandlesInTask(...args: CBArguments): void { + void Task( + (async () => { + this.callNonBlockingHandles(...args); + })() + ); + } +} diff --git a/src/webapis/SynapseHTTPAntispam/SynapseHttpAntispam.ts b/src/webapis/SynapseHTTPAntispam/SynapseHttpAntispam.ts new file mode 100644 index 00000000..c9e79b39 --- /dev/null +++ b/src/webapis/SynapseHTTPAntispam/SynapseHttpAntispam.ts @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2025 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { Express, Request, Response } from "express"; +import { SpamCheckEndpointPluginManager } from "./SpamCheckEndpointPluginManager"; +import { + UserMayInviteEndpoint, + UserMayInviteListenerArguments, +} from "./UserMayInviteEndpoint"; +import { + UserMayJoinRoomEndpoint, + UserMayJoinRoomListenerArguments, +} from "./UserMayJoinRoomEndpoint"; +import { + CheckEventForSpamEndpoint, + CheckEventForSpamListenerArguments, +} from "./CheckEventForSpamEndpoint"; + +const SPAM_CHECK_PREFIX = "/api/1/spam_check"; +const AUTHORIZATION = new RegExp("Bearer (.*)"); + +function makeAuthenticatedEndpointHandler( + secret: string, + cb: (request: Request, response: Response) => void +): (request: Request, response: Response) => void { + return function (request, response) { + const authorization = request.get("Authorization"); + if (!authorization) { + response.status(401).send("Missing access token"); + return; + } + const [, accessToken] = AUTHORIZATION.exec(authorization) ?? []; + if (accessToken !== secret) { + response.status(401).send("Missing access token"); + return; + } + cb(request, response); + }; +} + +export class SynapseHttpAntispam { + public readonly userMayInviteHandles = + new SpamCheckEndpointPluginManager(); + private readonly userMayInviteEndpoint = new UserMayInviteEndpoint( + this.userMayInviteHandles + ); + public readonly userMayJoinRoomhandles = + new SpamCheckEndpointPluginManager(); + private readonly userMayJoinRoomEndpoint = new UserMayJoinRoomEndpoint( + this.userMayJoinRoomhandles + ); + public readonly checkEventForSpamHandles = + new SpamCheckEndpointPluginManager(); + private readonly checkEventForSpamEndpoint = new CheckEventForSpamEndpoint( + this.checkEventForSpamHandles + ); + public constructor( + private readonly webController: Express, + private readonly secret: string + ) { + // nothing to do + } + + public register(): void { + this.webController.post( + `${SPAM_CHECK_PREFIX}/user_may_invite`, + makeAuthenticatedEndpointHandler(this.secret, (request, response) => { + this.userMayInviteEndpoint.handleUserMayInvite(request, response); + }) + ); + this.webController.post( + `${SPAM_CHECK_PREFIX}/user_may_join_room`, + makeAuthenticatedEndpointHandler(this.secret, (request, response) => { + this.userMayJoinRoomEndpoint.handleUserMayJoinRoom(request, response); + }) + ); + this.webController.post( + `${SPAM_CHECK_PREFIX}/check_event_for_spam`, + makeAuthenticatedEndpointHandler(this.secret, (request, response) => { + this.checkEventForSpamEndpoint.handleCheckEventForSpam( + request, + response + ); + }) + ); + } +} diff --git a/src/webapis/SynapseHTTPAntispam/UserMayInviteEndpoint.ts b/src/webapis/SynapseHTTPAntispam/UserMayInviteEndpoint.ts new file mode 100644 index 00000000..97016d64 --- /dev/null +++ b/src/webapis/SynapseHTTPAntispam/UserMayInviteEndpoint.ts @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2025 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { SpamCheckEndpointPluginManager } from "./SpamCheckEndpointPluginManager"; +import { Request, Response } from "express"; +import { + EDStatic, + isError, + Logger, + StringRoomIDSchema, + StringUserIDSchema, + Task, + Value, +} from "matrix-protection-suite"; +import { Type } from "@sinclair/typebox"; + +// for check_event_for_spam we will leave the event as unparsed + +const log = new Logger("UserMayInviteEndpoint"); + +export type UserMayInviteListenerArguments = Parameters< + (details: UserMayInviteRequestBody) => void +>; + +type UserMayInviteRequestBody = EDStatic; +const UserMayInviteRequestBody = Type.Object({ + inviter: StringUserIDSchema, + invitee: StringUserIDSchema, + room_id: StringRoomIDSchema, +}); + +export type UserMayInvitePluginManager = + SpamCheckEndpointPluginManager; +export class UserMayInviteEndpoint { + public constructor( + private readonly pluginManager: SpamCheckEndpointPluginManager + ) { + // nothing to do. + } + + private async handleUserMayInviteAsync( + request: Request, + response: Response, + isResponded: boolean + ): Promise { + const decodedBody = Value.Decode(UserMayInviteRequestBody, request.body); + if (isError(decodedBody)) { + log.error("Error decoding request body:", decodedBody.error); + if (!isResponded && this.pluginManager.isBlocking()) { + response.status(400).send({ + errcode: "M_INVALID_PARAM", + error: "Error handling inviter, invitee, and room_id", + }); + } + return; + } + if (!isResponded && this.pluginManager.isBlocking()) { + const blockingResult = await this.pluginManager.callBlockingHandles( + decodedBody.ok + ); + if (blockingResult === "NOT_SPAM") { + response.status(200); + response.send({}); + } else { + response.status(400); + response.send(blockingResult); + } + } else if (!isResponded) { + response.status(200); + response.send({}); + } + this.pluginManager.callNonBlockingHandlesInTask(decodedBody.ok); + } + + public handleUserMayInvite(request: Request, response: Response): void { + if (!this.pluginManager.isBlocking()) { + response.status(200); + response.send({}); + } + void Task( + this.handleUserMayInviteAsync( + request, + response, + !this.pluginManager.isBlocking() + ) + ); + } +} diff --git a/src/webapis/SynapseHTTPAntispam/UserMayJoinRoomEndpoint.ts b/src/webapis/SynapseHTTPAntispam/UserMayJoinRoomEndpoint.ts new file mode 100644 index 00000000..be2cf007 --- /dev/null +++ b/src/webapis/SynapseHTTPAntispam/UserMayJoinRoomEndpoint.ts @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2025 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { Type } from "@sinclair/typebox"; +import { + EDStatic, + isError, + Logger, + StringRoomIDSchema, + StringUserIDSchema, + Task, + Value, +} from "matrix-protection-suite"; +import { SpamCheckEndpointPluginManager } from "./SpamCheckEndpointPluginManager"; +import { Request, Response } from "express"; + +const log = new Logger("UserMayJoinRoomEndpoint"); + +export type UserMayJoinRoomListenerArguments = Parameters< + (details: UserMayJoinRoomRequestBody) => void +>; + +type UserMayJoinRoomRequestBody = EDStatic; +const UserMayJoinRoomRequestBody = Type.Object({ + user: StringUserIDSchema, + room: StringRoomIDSchema, + is_invited: Type.Boolean(), +}); + +export class UserMayJoinRoomEndpoint { + public constructor( + private readonly pluginManager: SpamCheckEndpointPluginManager + ) { + // nothing to do. + } + + private async handleUserMayJoinRoomAsync( + request: Request, + response: Response, + isResponded: boolean + ): Promise { + const decodedBody = Value.Decode(UserMayJoinRoomRequestBody, request.body); + if (isError(decodedBody)) { + log.error("Error decoding request body:", decodedBody.error); + if (!isResponded && this.pluginManager.isBlocking()) { + response.status(400).send({ + errcode: "M_INVALID_PARAM", + error: "Error handling user, room, and is_invited", + }); + } + return; + } + if (!isResponded && this.pluginManager.isBlocking()) { + const blockingResult = await this.pluginManager.callBlockingHandles( + decodedBody.ok + ); + if (blockingResult === "NOT_SPAM") { + response.status(200); + response.send({}); + } else { + response.status(400); + response.send(blockingResult); + } + } else if (!isResponded) { + response.status(200); + response.send({}); + } + this.pluginManager.callNonBlockingHandlesInTask(decodedBody.ok); + } + + public handleUserMayJoinRoom(request: Request, response: Response): void { + if (!this.pluginManager.isBlocking()) { + response.status(200); + response.send({}); + } + void Task( + this.handleUserMayJoinRoomAsync( + request, + response, + !this.pluginManager.isBlocking() + ) + ); + } +} From cb6af646d85e322324b368a7e2057fe3113b82e3 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Fri, 14 Mar 2025 12:24:44 +0000 Subject: [PATCH 2/4] Replace antispam with http-antispam in mx-tester. --- mx-tester.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/mx-tester.yml b/mx-tester.yml index 7ef1578a..5baf265a 100644 --- a/mx-tester.yml +++ b/mx-tester.yml @@ -37,12 +37,23 @@ down: - docker stop mjolnir-test-reverse-proxy || true modules: - - name: mjolnir + - name: HTTPAntispam build: - - cp -r synapse_antispam $MX_TEST_MODULE_DIR/ + - git clone https://github.com/maunium/synapse-http-antispam.git + $MX_TEST_MODULE_DIR/ config: - module: mjolnir.Module - config: {} + module: synapse_http_antispam.HTTPAntispam + config: + base_url: http://host.docker.internal:8082/api/1/spam_check + authorization: DEFAULT + enabled_callbacks: + - user_may_invite + - user_may_join_room + - check_event_for_spam + fail_open: + user_may_invite: true + user_may_join_room: true + check_event_for_spam: true homeserver: # Basic configuration. From 9a9547feb59d71ab5f0c2852e355824bf66bfad0 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Fri, 14 Mar 2025 12:37:54 +0000 Subject: [PATCH 3/4] Add configuration for synapse-http-antispam. --- config/default.yaml | 12 +++++ config/harness.yaml | 3 ++ src/Draupnir.ts | 10 ++-- src/DraupnirBotMode.ts | 8 ++++ src/config.ts | 19 ++++++++ src/draupnirfactory/DraupnirFactory.ts | 7 ++- src/webapis/WebAPIs.ts | 9 ++++ test/integration/httpAntispamTest.ts | 64 ++++++++++++++++++++++++++ 8 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 test/integration/httpAntispamTest.ts diff --git a/config/default.yaml b/config/default.yaml index f31f1ee0..d663efe4 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -273,6 +273,18 @@ web: abuseReporting: # Whether to enable this feature. enabled: false + # Whether to setup a endpoints for synapse-http-antispam + # https://github.com/maunium/synapse-http-antispam + # this is required for some features of Draupnir, + # such as support for room takedown policies. + # + # Please FOLLOW the instructions here: + # https://the-draupnir-project.github.io/draupnir-documentation/bot/synapse-http-antispam + synapseHTTPAntispam: + enabled: false + # This is a secret that you must place into your synapse module config + # https://github.com/maunium/synapse-http-antispam?tab=readme-ov-file#configuration + authorization: REPLACE_ME # Whether or not to actively poll synapse for abuse reports, to be used # instead of intercepting client calls to synapse's abuse endpoint, when that diff --git a/config/harness.yaml b/config/harness.yaml index beb2fc32..9ca35bdb 100644 --- a/config/harness.yaml +++ b/config/harness.yaml @@ -186,3 +186,6 @@ web: abuseReporting: # Whether to enable this feature. enabled: true + synapseHTTPAntispam: + enabled: true + authorization: DEFAULT diff --git a/src/Draupnir.ts b/src/Draupnir.ts index 430e9f24..92b96fa3 100644 --- a/src/Draupnir.ts +++ b/src/Draupnir.ts @@ -81,6 +81,7 @@ import { COMMAND_CONFIRMATION_LISTENER, makeConfirmationPromptListener, } from "./commands/interface-manager/MatrixPromptForConfirmation"; +import { SynapseHttpAntispam } from "./webapis/SynapseHTTPAntispam/SynapseHttpAntispam"; const log = new Logger("Draupnir"); // webAPIS should not be included on the Draupnir class. @@ -144,7 +145,8 @@ export class Draupnir implements Client, MatrixAdaptorContext { public readonly acceptInvitesFromRoom: MatrixRoomID, public readonly acceptInvitesFromRoomIssuer: RoomMembershipRevisionIssuer, public readonly safeModeToggle: SafeModeToggle, - public readonly synapseAdminClient?: SynapseAdminClient + public readonly synapseAdminClient: SynapseAdminClient | undefined, + public readonly synapseHTTPAntispam: SynapseHttpAntispam | undefined ) { this.managementRoomOutput = new ManagementRoomOutput( this.managementRoomDetail, @@ -209,7 +211,8 @@ export class Draupnir implements Client, MatrixAdaptorContext { roomMembershipManager: RoomMembershipManager, config: IConfig, loggableConfigTracker: LoggableConfigTracker, - safeModeToggle: SafeModeToggle + safeModeToggle: SafeModeToggle, + synapseHTTPAntispam: SynapseHttpAntispam | undefined ): Promise> { const acceptInvitesFromRoom = await (async () => { if (config.autojoinOnlyIfManager) { @@ -267,7 +270,8 @@ export class Draupnir implements Client, MatrixAdaptorContext { acceptInvitesFromRoom.ok, acceptInvitesFromRoomIssuer.ok, safeModeToggle, - new SynapseAdminClient(client, clientUserID) + new SynapseAdminClient(client, clientUserID), + synapseHTTPAntispam ); const loadResult = await protectedRoomsSet.protections.loadProtections( protectedRoomsSet, diff --git a/src/DraupnirBotMode.ts b/src/DraupnirBotMode.ts index 953425a8..a08c8583 100644 --- a/src/DraupnirBotMode.ts +++ b/src/DraupnirBotMode.ts @@ -48,6 +48,7 @@ import { SafeModeDraupnir } from "./safemode/DraupnirSafeMode"; import { ResultError } from "@gnuxie/typescript-result"; import { SafeModeCause, SafeModeReason } from "./safemode/SafeModeCause"; import { SafeModeBootOption } from "./safemode/BootOption"; +import { SynapseHttpAntispam } from "./webapis/SynapseHTTPAntispam/SynapseHttpAntispam"; const log = new Logger("DraupnirBotMode"); @@ -73,6 +74,9 @@ interface BotModeTogle extends SafeModeToggle { error: ResultError, options?: SafeModeToggleOptions ): Promise>; + // The SynapseHTTPAntispam listeners, if available. + // Which they won't be for some bot mode and all application service users. + readonly synapseHTTPAntispam: SynapseHttpAntispam | undefined; } export class DraupnirBotModeToggle implements BotModeTogle { @@ -80,6 +84,10 @@ export class DraupnirBotModeToggle implements BotModeTogle { private safeModeDraupnir: SafeModeDraupnir | null = null; private webAPIs: WebAPIs | null = null; + public get synapseHTTPAntispam() { + return this.webAPIs?.synapseHTTPAntispam ?? undefined; + } + private constructor( private readonly clientUserID: StringUserID, private readonly managementRoom: MatrixRoomID, diff --git a/src/config.ts b/src/config.ts index 9d4f5bde..cc26e9f7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -40,6 +40,17 @@ export function getNonDefaultConfigProperties( ) { nonDefault.pantalaimon.password = "REDACTED"; } + if ( + "web" in nonDefault && + typeof nonDefault["web"] === "object" && + nonDefault["web"] !== null && + "synapseHTTPAntispam" in nonDefault["web"] && + typeof nonDefault["web"]["synapseHTTPAntispam"] === "object" + ) { + if (nonDefault["web"]["synapseHTTPAntispam"] !== null) { + nonDefault["web"]["synapseHTTPAntispam"].authorization = "REDACTED"; + } + } return nonDefault; } @@ -147,6 +158,10 @@ export interface IConfig { abuseReporting: { enabled: boolean; }; + synapseHTTPAntispam: { + enabled: boolean; + authorization: string; + }; }; // Store room state using sqlite to improve startup time when Synapse responds // slowly to requests for `/state`. @@ -242,6 +257,10 @@ const defaultConfig: IConfig = { abuseReporting: { enabled: false, }, + synapseHTTPAntispam: { + enabled: false, + authorization: "DEFAULT", + }, }, roomStateBackingStore: { enabled: true, diff --git a/src/draupnirfactory/DraupnirFactory.ts b/src/draupnirfactory/DraupnirFactory.ts index 64429121..85bb1af9 100644 --- a/src/draupnirfactory/DraupnirFactory.ts +++ b/src/draupnirfactory/DraupnirFactory.ts @@ -29,6 +29,7 @@ import { SafeModeDraupnir } from "../safemode/DraupnirSafeMode"; import { SafeModeCause } from "../safemode/SafeModeCause"; import { SafeModeToggle } from "../safemode/SafeModeToggle"; import { StandardManagementRoomDetail } from "../managementroom/ManagementRoomDetail"; +import { DraupnirBotModeToggle } from "../DraupnirBotMode"; const log = new Logger("DraupnirFactory"); @@ -141,7 +142,11 @@ export class DraupnirFactory { roomMembershipManager, config, configLogTracker, - toggle + toggle, + // synapseHTTPAntispam is only available in bot mode. + toggle instanceof DraupnirBotModeToggle + ? toggle.synapseHTTPAntispam + : undefined ); } diff --git a/src/webapis/WebAPIs.ts b/src/webapis/WebAPIs.ts index ad770b6b..4569bad3 100644 --- a/src/webapis/WebAPIs.ts +++ b/src/webapis/WebAPIs.ts @@ -20,6 +20,7 @@ import { isStringEventID, } from "@the-draupnir-project/matrix-basic-types"; import { Logger, Task } from "matrix-protection-suite"; +import { SynapseHttpAntispam } from "./SynapseHTTPAntispam/SynapseHttpAntispam"; const log = new Logger("WebAPIs"); @@ -33,6 +34,10 @@ const AUTHORIZATION = new RegExp("Bearer (.*)"); export class WebAPIs { private webController: express.Express = express(); private httpServer?: Server | undefined; + public readonly synapseHTTPAntispam = new SynapseHttpAntispam( + this.webController, + this.config.web.synapseHTTPAntispam.authorization + ); constructor( private reportManager: StandardReportManager, @@ -58,6 +63,10 @@ export class WebAPIs { } ); }); + // enable synapse http antispam + if (this.config.web.synapseHTTPAntispam.enabled) { + this.synapseHTTPAntispam.register(); + } // configure /report API. if (this.config.web.abuseReporting.enabled) { log.info(`configuring ${API_PREFIX}/report/:room_id/:event_id...`); diff --git a/test/integration/httpAntispamTest.ts b/test/integration/httpAntispamTest.ts new file mode 100644 index 00000000..540799f0 --- /dev/null +++ b/test/integration/httpAntispamTest.ts @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2025 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import expect from "expect"; +import { DraupnirTestContext } from "./mjolnirSetupUtils"; +import { ActionException, isOk } from "matrix-protection-suite"; +import { MatrixError } from "matrix-bot-sdk"; + +describe("Test for http antispam callbacks", function () { + it("We can process check_event_for_spam", async function ( + this: DraupnirTestContext + ) { + const draupnir = this.draupnir; + if (draupnir === undefined) { + throw new TypeError(`setup code is wrong`); + } + const synapseHTTPAntispam = this.toggle?.synapseHTTPAntispam; + if (synapseHTTPAntispam === undefined) { + throw new TypeError("Setup code is wrong"); + } + const promise = new Promise((resolve) => { + synapseHTTPAntispam.checkEventForSpamHandles.registerNonBlockingHandle( + (details) => { + if (details.event.sender === draupnir.clientUserID) { + resolve(undefined); + } + } + ); + }); + ( + await draupnir.clientPlatform + .toRoomMessageSender() + .sendMessage(draupnir.managementRoomID, { + body: "hello", + msgtype: "m.text", + }) + ).expect("should be able to send the message just fine"); + await promise; + // now try blocking + synapseHTTPAntispam.checkEventForSpamHandles.registerBlockingHandle(() => { + return Promise.resolve({ errcode: "M_FORBIDDEN", error: "no." }); + }); + const sendResult = await draupnir.clientPlatform + .toRoomMessageSender() + .sendMessage(draupnir.managementRoomID, { + body: "hello", + msgtype: "m.text", + }); + if (isOk(sendResult)) { + throw new TypeError("We expect the result to be blocked"); + } + if (!(sendResult.error instanceof ActionException)) { + throw new TypeError( + "We're trying to destructure this to get the MatrixError" + ); + } + // I'm pretty sure there are different versions of this being used in the code base + // so instanceof fails :/ sucks balls mare + const matrixError = sendResult.error.exception as MatrixError; + expect(matrixError.error).toBe("no."); + expect(matrixError.errcode).toBe("M_FORBIDDEN"); + } as unknown as Mocha.AsyncFunc); +}); From 4e9d2a010aadb59d59bae216b7d43da2a9fa4a48 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Fri, 14 Mar 2025 13:59:25 +0000 Subject: [PATCH 4/4] We have got issues with utils.ts... --- test/integration/httpAntispamTest.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/test/integration/httpAntispamTest.ts b/test/integration/httpAntispamTest.ts index 540799f0..011c12fc 100644 --- a/test/integration/httpAntispamTest.ts +++ b/test/integration/httpAntispamTest.ts @@ -4,7 +4,11 @@ import expect from "expect"; import { DraupnirTestContext } from "./mjolnirSetupUtils"; -import { ActionException, isOk } from "matrix-protection-suite"; +import { + ActionException, + isOk, + MatrixException, +} from "matrix-protection-suite"; import { MatrixError } from "matrix-bot-sdk"; describe("Test for http antispam callbacks", function () { @@ -57,8 +61,15 @@ describe("Test for http antispam callbacks", function () { } // I'm pretty sure there are different versions of this being used in the code base // so instanceof fails :/ sucks balls mare - const matrixError = sendResult.error.exception as MatrixError; - expect(matrixError.error).toBe("no."); - expect(matrixError.errcode).toBe("M_FORBIDDEN"); + // https://github.com/the-draupnir-project/Draupnir/issues/760 + // https://github.com/the-draupnir-project/Draupnir/issues/759 + if (sendResult.error instanceof MatrixException) { + expect(sendResult.error.matrixErrorMessage).toBe("no."); + expect(sendResult.error.matrixErrorCode).toBe("M_FORBIDDEN"); + } else { + const matrixError = sendResult.error.exception as MatrixError; + expect(matrixError.error).toBe("no."); + expect(matrixError.errcode).toBe("M_FORBIDDEN"); + } } as unknown as Mocha.AsyncFunc); });