mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-05-15 03:45:22 +00:00
Merge pull request #758 from the-draupnir-project/gnuxie/synapse-http-antispam
synapse-http-antispam support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -186,3 +186,6 @@ web:
|
||||
abuseReporting:
|
||||
# Whether to enable this feature.
|
||||
enabled: true
|
||||
synapseHTTPAntispam:
|
||||
enabled: true
|
||||
authorization: DEFAULT
|
||||
|
||||
+15
-4
@@ -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.
|
||||
|
||||
+7
-3
@@ -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<ActionResult<Draupnir>> {
|
||||
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,
|
||||
|
||||
@@ -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<Result<SafeModeDraupnir>>;
|
||||
// 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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// 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<CheckEventForSpamListenerArguments>
|
||||
) {
|
||||
// nothing to do.
|
||||
}
|
||||
|
||||
private async handleCheckEventForSpamAsync(
|
||||
request: Request,
|
||||
response: Response,
|
||||
isResponded: boolean
|
||||
): Promise<void> {
|
||||
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()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// 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<CBArguments extends unknown[]> = (
|
||||
...args: CBArguments
|
||||
) => Promise<BlockingResponse>;
|
||||
export type NonBlockingCallback<CBArguments extends unknown[]> = (
|
||||
...args: CBArguments
|
||||
) => void;
|
||||
|
||||
export class SpamCheckEndpointPluginManager<CBArguments extends unknown[]> {
|
||||
private readonly blockingHandles = new Set<BlockingCallback<CBArguments>>();
|
||||
private readonly nonBlockingHandles = new Set<
|
||||
NonBlockingCallback<CBArguments>
|
||||
>();
|
||||
|
||||
public registerBlockingHandle(handle: BlockingCallback<CBArguments>): void {
|
||||
this.blockingHandles.add(handle);
|
||||
}
|
||||
|
||||
public registerNonBlockingHandle(
|
||||
handle: NonBlockingCallback<CBArguments>
|
||||
): void {
|
||||
this.nonBlockingHandles.add(handle);
|
||||
}
|
||||
|
||||
public unregisterHandle(
|
||||
handle: BlockingCallback<CBArguments> | NonBlockingCallback<CBArguments>
|
||||
): void {
|
||||
this.blockingHandles.delete(handle as BlockingCallback<CBArguments>);
|
||||
this.nonBlockingHandles.delete(handle as NonBlockingCallback<CBArguments>);
|
||||
}
|
||||
|
||||
public unregisterListeners(): void {
|
||||
this.blockingHandles.clear();
|
||||
this.nonBlockingHandles.clear();
|
||||
}
|
||||
|
||||
public isBlocking(): boolean {
|
||||
return this.blockingHandles.size > 0;
|
||||
}
|
||||
|
||||
public async callBlockingHandles(
|
||||
...args: CBArguments
|
||||
): ReturnType<BlockingCallback<CBArguments>> {
|
||||
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);
|
||||
})()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// 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<UserMayInviteListenerArguments>();
|
||||
private readonly userMayInviteEndpoint = new UserMayInviteEndpoint(
|
||||
this.userMayInviteHandles
|
||||
);
|
||||
public readonly userMayJoinRoomhandles =
|
||||
new SpamCheckEndpointPluginManager<UserMayJoinRoomListenerArguments>();
|
||||
private readonly userMayJoinRoomEndpoint = new UserMayJoinRoomEndpoint(
|
||||
this.userMayJoinRoomhandles
|
||||
);
|
||||
public readonly checkEventForSpamHandles =
|
||||
new SpamCheckEndpointPluginManager<CheckEventForSpamListenerArguments>();
|
||||
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
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// 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<typeof UserMayInviteRequestBody>;
|
||||
const UserMayInviteRequestBody = Type.Object({
|
||||
inviter: StringUserIDSchema,
|
||||
invitee: StringUserIDSchema,
|
||||
room_id: StringRoomIDSchema,
|
||||
});
|
||||
|
||||
export type UserMayInvitePluginManager =
|
||||
SpamCheckEndpointPluginManager<UserMayInviteListenerArguments>;
|
||||
export class UserMayInviteEndpoint {
|
||||
public constructor(
|
||||
private readonly pluginManager: SpamCheckEndpointPluginManager<UserMayInviteListenerArguments>
|
||||
) {
|
||||
// nothing to do.
|
||||
}
|
||||
|
||||
private async handleUserMayInviteAsync(
|
||||
request: Request,
|
||||
response: Response,
|
||||
isResponded: boolean
|
||||
): Promise<void> {
|
||||
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()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// 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<typeof UserMayJoinRoomRequestBody>;
|
||||
const UserMayJoinRoomRequestBody = Type.Object({
|
||||
user: StringUserIDSchema,
|
||||
room: StringRoomIDSchema,
|
||||
is_invited: Type.Boolean(),
|
||||
});
|
||||
|
||||
export class UserMayJoinRoomEndpoint {
|
||||
public constructor(
|
||||
private readonly pluginManager: SpamCheckEndpointPluginManager<UserMayJoinRoomListenerArguments>
|
||||
) {
|
||||
// nothing to do.
|
||||
}
|
||||
|
||||
private async handleUserMayJoinRoomAsync(
|
||||
request: Request,
|
||||
response: Response,
|
||||
isResponded: boolean
|
||||
): Promise<void> {
|
||||
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()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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...`);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
import expect from "expect";
|
||||
import { DraupnirTestContext } from "./mjolnirSetupUtils";
|
||||
import {
|
||||
ActionException,
|
||||
isOk,
|
||||
MatrixException,
|
||||
} 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
|
||||
// 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);
|
||||
});
|
||||
Reference in New Issue
Block a user