From 313a5d7fc4d16ea706ee3259f417f27e6422a59c Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 22 Oct 2024 10:50:03 +0100 Subject: [PATCH 1/5] Add support for forwarding mentions of the moderator bot to the moderation room. --- config/default.yaml | 4 ++++ src/Mjolnir.ts | 37 +++++++++++++++++++++++++++++++++++-- src/config.ts | 2 ++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/config/default.yaml b/config/default.yaml index e8a81643..610bc717 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -65,6 +65,10 @@ recordIgnoredInvites: false # (see verboseLogging to adjust this a bit.) managementRoom: "#moderators:example.org" +# Forward any messages mentioning the bot user to the mangement room. Repeated mentions within +# a 10 minute period are ignored. +forwardMentionsToManagementRoom: false + # Whether Mjolnir should log a lot more messages in the room, # mainly involves "all-OK" messages, and debugging messages for when mjolnir checks bans in a room. verboseLogging: true diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index c1541c28..a951e958 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -14,7 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { extractRequestError, LogLevel, LogService, MembershipEvent } from "@vector-im/matrix-bot-sdk"; +import { + extractRequestError, + LogLevel, + LogService, + MembershipEvent, + Permalinks, + UserID, +} from "@vector-im/matrix-bot-sdk"; import { ALL_RULE_TYPES as ALL_BAN_LIST_RULE_TYPES } from "./models/ListRule"; import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler"; @@ -34,6 +41,7 @@ import { RoomMemberManager } from "./RoomMembers"; import ProtectedRoomsConfig from "./ProtectedRoomsConfig"; import { MatrixEmitter, MatrixSendClient } from "./MatrixEmitter"; import { OpenMetrics } from "./webapis/OpenMetrics"; +import { LRUCache } from "lru-cache"; export const STATE_NOT_STARTED = "not_started"; export const STATE_CHECKING_PERMISSIONS = "checking_permissions"; @@ -81,6 +89,11 @@ export class Mjolnir { public readonly policyListManager: PolicyListManager; + public readonly lastBotMentionForRoomId = new LRUCache({ + ttl: 1000 * 60 * 8, // 8 minutes + ttlAutopurge: true, + }); + /** * Adds a listener to the client that will automatically accept invitations. * @param {MatrixSendClient} client @@ -201,9 +214,29 @@ export class Mjolnir { matrixEmitter.on("room.message", async (roomId, event) => { const eventContent = event.content; - if (roomId !== this.managementRoomId) return; if (typeof eventContent !== "object") return; + if (this.config.forwardMentionsToManagementRoom && this.protectedRoomsTracker.isProtectedRoom(roomId)) { + if (eventContent?.["m.mentions"]?.user_ids?.includes(this.clientUserId)) { + LogService.info("Mjolnir", `Bot mentioned ${roomId} by ${event.sender}`); + // Bot mentioned in a public room. + if (this.lastBotMentionForRoomId.has(roomId)) { + // Mentioned too recently, ignore. + return; + } + this.lastBotMentionForRoomId.set(roomId, true); + const permalink = Permalinks.forEvent(roomId, event.event_id, [ + new UserID(this.clientUserId).domain, + ]); + await this.managementRoomOutput.logMessage( + LogLevel.INFO, + "Mjolnir", + `Bot mentioned ${roomId} by ${event.sender} in ${permalink}`, + roomId, + ); + } + } + const { msgtype, body: originalBody, sender, event_id } = eventContent; if (msgtype !== "m.text" || typeof originalBody !== "string") { return; diff --git a/src/config.ts b/src/config.ts index 38057a10..8b965ecf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -90,6 +90,7 @@ export interface IConfig { acceptInvitesFromSpace: string; recordIgnoredInvites: boolean; managementRoom: string; + forwardMentionsToManagementRoom: boolean; verboseLogging: boolean; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; syncOnStartup: boolean; @@ -209,6 +210,7 @@ const defaultConfig: IConfig = { autojoinOnlyIfManager: true, recordIgnoredInvites: false, managementRoom: "!noop:example.org", + forwardMentionsToManagementRoom: false, verboseLogging: false, logLevel: "INFO", syncOnStartup: true, From e772122827f3d1d8e46686a6fc1ca0f267af4267 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 22 Oct 2024 10:50:11 +0100 Subject: [PATCH 2/5] Add a test --- test/integration/forwardedMentionsTest.ts | 59 +++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 test/integration/forwardedMentionsTest.ts diff --git a/test/integration/forwardedMentionsTest.ts b/test/integration/forwardedMentionsTest.ts new file mode 100644 index 00000000..5dc84b37 --- /dev/null +++ b/test/integration/forwardedMentionsTest.ts @@ -0,0 +1,59 @@ +import { MatrixClient, UserID } from "@vector-im/matrix-bot-sdk"; +import { Mjolnir } from "../../src/Mjolnir"; +import { newTestUser, noticeListener } from "./clientHelper"; +import { strict as assert } from "assert"; + +describe("Test: config.forwardMentionsToManagementRoom behaves correctly.", function () { + let moderator: MatrixClient; + this.beforeEach(async function () { + moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); + await moderator.start(); + }); + + this.afterEach(async function () { + moderator.stop(); + }); + + it("correctly forwards a mention.", async function () { + const mjolnir: Mjolnir = this.mjolnir!; + const botUserId = await mjolnir.client.getUserId(); + mjolnir.config.forwardMentionsToManagementRoom = true; + + const mentioninguser = await newTestUser(this.config.homeserverUrl, { name: { contains: "mentioninguser" } }); + const mentioningUserId = await mentioninguser.getUserId(); + await moderator.joinRoom(mjolnir.managementRoomId); + const protectedRoom = await moderator.createRoom({ preset: "public_chat" }); + await mjolnir.client.joinRoom(protectedRoom); + await mentioninguser.joinRoom(protectedRoom); + await mjolnir.protectedRoomsTracker.addProtectedRoom(protectedRoom); + + await moderator.start(); + const noticeBody = new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error("Timed out waiting for notice")), 8000); + moderator.on( + "room.message", + noticeListener(this.mjolnir.managementRoomId, (event) => { + if (event.content.body.includes(`Bot mentioned`)) { + clearTimeout(timeout); + resolve(event.content.body); + } + }), + ); + }); + + const mentionEventId = await mentioninguser.sendMessage(protectedRoom, { + msgtype: "m.text", + body: "Moderator: Testing this", + ["m.mentions"]: { + user_ids: [botUserId], + }, + }); + const domain = new UserID(mentioningUserId).domain; + + assert.equal( + await noticeBody, + `Bot mentioned ${protectedRoom} by ${mentioningUserId} in https://matrix.to/#/${protectedRoom}/${mentionEventId}?via=${domain}`, + "Forwarded mention format mismatch", + ); + }); +}); From 3064e2779587972dc064090eda583c00470ee40d Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 22 Oct 2024 10:50:21 +0100 Subject: [PATCH 3/5] Cleanup a few imports from other tests. --- test/integration/protectedRoomsConfigTest.ts | 3 +-- test/integration/standardConsequenceTest.ts | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/test/integration/protectedRoomsConfigTest.ts b/test/integration/protectedRoomsConfigTest.ts index 82694205..578bdfe9 100644 --- a/test/integration/protectedRoomsConfigTest.ts +++ b/test/integration/protectedRoomsConfigTest.ts @@ -4,10 +4,9 @@ import { MatrixSendClient } from "../../src/MatrixEmitter"; import { Mjolnir } from "../../src/Mjolnir"; import PolicyList from "../../src/models/PolicyList"; import { newTestUser } from "./clientHelper"; -import { createBanList, getFirstReaction } from "./commands/commandUtils"; +import { createBanList } from "./commands/commandUtils"; async function createPolicyList(client: MatrixClient): Promise { - const serverName = new UserID(await client.getUserId()).domain; const policyListId = await client.createRoom({ preset: "public_chat" }); return new PolicyList(policyListId, Permalinks.forRoom(policyListId), client); } diff --git a/test/integration/standardConsequenceTest.ts b/test/integration/standardConsequenceTest.ts index eea8d636..3981ce82 100644 --- a/test/integration/standardConsequenceTest.ts +++ b/test/integration/standardConsequenceTest.ts @@ -1,9 +1,6 @@ -import { strict as assert } from "assert"; - import { Mjolnir } from "../../src/Mjolnir"; import { Protection } from "../../src/protections/IProtection"; -import { newTestUser, noticeListener } from "./clientHelper"; -import { matrixClient, mjolnir } from "./mjolnirSetupUtils"; +import { newTestUser } from "./clientHelper"; import { ConsequenceBan, ConsequenceRedact } from "../../src/protections/consequence"; describe("Test: standard consequences", function () { From 57a0b1f1969c79bc98631401f104aaf993083979 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 25 Oct 2024 13:44:34 +0100 Subject: [PATCH 4/5] Add licence + test for spam. --- src/Mjolnir.ts | 2 +- test/integration/forwardedMentionsTest.ts | 58 +++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index c9dfb35b..2c1b8bba 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -1,5 +1,5 @@ /* -Copyright 2019-2021 The Matrix.org Foundation C.I.C. +Copyright 2019-2024 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. diff --git a/test/integration/forwardedMentionsTest.ts b/test/integration/forwardedMentionsTest.ts index 5dc84b37..f0c393f5 100644 --- a/test/integration/forwardedMentionsTest.ts +++ b/test/integration/forwardedMentionsTest.ts @@ -1,7 +1,24 @@ +/* +Copyright 2024 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 { MatrixClient, UserID } from "@vector-im/matrix-bot-sdk"; import { Mjolnir } from "../../src/Mjolnir"; import { newTestUser, noticeListener } from "./clientHelper"; import { strict as assert } from "assert"; +import expect from "expect"; describe("Test: config.forwardMentionsToManagementRoom behaves correctly.", function () { let moderator: MatrixClient; @@ -56,4 +73,45 @@ describe("Test: config.forwardMentionsToManagementRoom behaves correctly.", func "Forwarded mention format mismatch", ); }); + + it("only forwards the first mention from a user.", async function () { + const mjolnir: Mjolnir = this.mjolnir!; + const botUserId = await mjolnir.client.getUserId(); + mjolnir.config.forwardMentionsToManagementRoom = true; + + const mentioninguser = await newTestUser(this.config.homeserverUrl, { name: { contains: "mentioninguser" } }); + await moderator.joinRoom(mjolnir.managementRoomId); + const protectedRoom = await moderator.createRoom({ preset: "public_chat" }); + await mjolnir.client.joinRoom(protectedRoom); + await mentioninguser.joinRoom(protectedRoom); + await mjolnir.protectedRoomsTracker.addProtectedRoom(protectedRoom); + + await moderator.start(); + let mentionCount = 0; + moderator.on( + "room.message", + noticeListener(this.mjolnir.managementRoomId, (event) => { + if (event.content.body.includes(`Bot mentioned`)) { + mentionCount++; + } + }), + ); + + function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + for (let index = 0; index < 5; index++) { + await mentioninguser.sendMessage(protectedRoom, { + msgtype: "m.text", + body: `Moderator: Testing this ${index}`, + ["m.mentions"]: { + user_ids: [botUserId], + }, + }); + await delay(2000); + } + + expect(mentionCount).toBe(1); + }); }); From 3ba42df0946364bccb4fed7f2733bf660802be3e Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 25 Oct 2024 16:18:39 +0100 Subject: [PATCH 5/5] Fix test --- test/integration/forwardedMentionsTest.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/forwardedMentionsTest.ts b/test/integration/forwardedMentionsTest.ts index f0c393f5..caa1eec1 100644 --- a/test/integration/forwardedMentionsTest.ts +++ b/test/integration/forwardedMentionsTest.ts @@ -87,12 +87,12 @@ describe("Test: config.forwardMentionsToManagementRoom behaves correctly.", func await mjolnir.protectedRoomsTracker.addProtectedRoom(protectedRoom); await moderator.start(); - let mentionCount = 0; + let mentions = new Set(); moderator.on( "room.message", noticeListener(this.mjolnir.managementRoomId, (event) => { if (event.content.body.includes(`Bot mentioned`)) { - mentionCount++; + mentions.add(event.event_id); } }), ); @@ -112,6 +112,6 @@ describe("Test: config.forwardMentionsToManagementRoom behaves correctly.", func await delay(2000); } - expect(mentionCount).toBe(1); + expect(mentions.size).toBe(1); }); });