diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index 64f04dd4..ccf84383 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -228,6 +228,7 @@ export async function handleCommand(roomId: string, event: { content: { body: st "!mjolnir msc4284_set unset - Unset/disable the policy server in a specific room. Same constraints as the room ID/alias set command.\n" + "!mjolnir msc4284_set * - Set the policy server for all Mjolnir-protected rooms.\n" + "!mjolnir msc4284_set * unset - Unset/disable the policy server for all Mjolnir-protected rooms\n" + + "!mjolnir synapse_ext policy_server_spammy - Enable/disable receiving events flagged as spammy by policy servers. Requires compatible server.\n" + ""; // empty segment to reduce diff noise const botMenu = diff --git a/src/commands/SynapseExtensionsCommand.ts b/src/commands/SynapseExtensionsCommand.ts new file mode 100644 index 00000000..bebf9f72 --- /dev/null +++ b/src/commands/SynapseExtensionsCommand.ts @@ -0,0 +1,45 @@ +/* +Copyright 2025 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Mjolnir } from "../Mjolnir"; + +type SynapseAdminConfig = { + return_soft_failed_events?: boolean; + return_policy_server_spammy_events?: boolean; +}; + +const SYNAPSE_ADMIN_ACCOUNT_DATA_TYPE = "io.element.synapse.admin_client_config"; + +// !mjolnir synapse_ext <'policy_server_spammy'> +export async function execSynapseExtensionsCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { + const extension = parts[2]; + + if (extension === "policy_server_spammy") { + let currentConfig: SynapseAdminConfig = {}; + try { + currentConfig = await mjolnir.client.getAccountData(SYNAPSE_ADMIN_ACCOUNT_DATA_TYPE); + } catch (e) { + // assume unset + } + await mjolnir.client.setAccountData(SYNAPSE_ADMIN_ACCOUNT_DATA_TYPE, { + return_soft_failed_events: currentConfig.return_soft_failed_events ?? false, + return_policy_server_spammy_events: parts[3] === "true", + }); + await mjolnir.client.unstableApis.addReactionToEvent(roomId, event["event_id"], "✅"); + } else { + await mjolnir.client.replyNotice(roomId, event, "Unknown extension, please check the help for more information."); + } +} diff --git a/src/protections/ProtectionManager.ts b/src/protections/ProtectionManager.ts index 942a417a..fcac09ff 100644 --- a/src/protections/ProtectionManager.ts +++ b/src/protections/ProtectionManager.ts @@ -35,6 +35,7 @@ import { NsfwProtection } from "./NsfwProtection"; import { MentionSpam } from "./MentionSpam"; import { MessageIsVideo } from "./MessageIsVideo"; import { FirstMessageIsLink } from "./FirstMessageIsLink"; +import {RedactPolicyServerFlaggedEvents} from "./RedactPolicyServerFlaggedEvents"; const PROTECTIONS: Protection[] = [ new FirstMessageIsImage(), @@ -50,6 +51,7 @@ const PROTECTIONS: Protection[] = [ new MentionSpam(), new MessageIsVideo(), new FirstMessageIsLink(), + new RedactPolicyServerFlaggedEvents(), ]; const ENABLED_PROTECTIONS_EVENT_TYPE = "org.matrix.mjolnir.enabled_protections"; diff --git a/src/protections/RedactPolicyServerFlaggedEvents.ts b/src/protections/RedactPolicyServerFlaggedEvents.ts new file mode 100644 index 00000000..1a632b06 --- /dev/null +++ b/src/protections/RedactPolicyServerFlaggedEvents.ts @@ -0,0 +1,37 @@ +/* +Copyright 2025 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Protection } from "./IProtection"; +import { Mjolnir } from "../Mjolnir"; + +export class RedactPolicyServerFlaggedEvents extends Protection { + settings = {}; + + public get name(): string { + return "RedactPolicyServerFlaggedEvents"; + } + + public get description(): string { + return "Redacts events that are flagged by the policy server as probable spam. Requires enabling the policy_server_spammy Synapse extension (see help command for info)."; + } + + public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise { + if (event["unsigned"]?.["io.element.synapse.policy_server_spammy"] === true) { + console.log(`Redacting ${event["event_id"]} in ${roomId} due to policy server flagging it as spam.`); + await mjolnir.client.redactEvent(roomId, event["event_id"], "Probable spam"); + } + } +}