diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index c1541c28..122e2ba6 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -34,6 +34,7 @@ import { RoomMemberManager } from "./RoomMembers"; import ProtectedRoomsConfig from "./ProtectedRoomsConfig"; import { MatrixEmitter, MatrixSendClient } from "./MatrixEmitter"; import { OpenMetrics } from "./webapis/OpenMetrics"; +import { ModCache } from "./ModCache"; export const STATE_NOT_STARTED = "not_started"; export const STATE_CHECKING_PERMISSIONS = "checking_permissions"; @@ -81,6 +82,11 @@ export class Mjolnir { public readonly policyListManager: PolicyListManager; + /** + * Members of the moderator room and others who should not be banned, ACL'd etc. + */ + public moderators: ModCache; + /** * Adds a listener to the client that will automatically accept invitations. * @param {MatrixSendClient} client @@ -180,6 +186,9 @@ export class Mjolnir { "Mjolnir is starting up. Use !mjolnir to query status.", ); Mjolnir.addJoinOnInviteListener(mjolnir, client, config); + + mjolnir.moderators = new ModCache(mjolnir.client, mjolnir.matrixEmitter, mjolnir.managementRoomId); + return mjolnir; } @@ -285,6 +294,7 @@ export class Mjolnir { this.managementRoomOutput, this.protectionManager, config, + this.moderators, ); } @@ -391,6 +401,7 @@ export class Mjolnir { this.webapis.stop(); this.reportPoller?.stop(); this.openMetrics.stop(); + this.moderators.stop(); } /** diff --git a/src/ModCache.ts b/src/ModCache.ts new file mode 100644 index 00000000..d41f2ea9 --- /dev/null +++ b/src/ModCache.ts @@ -0,0 +1,114 @@ +/* +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 { MatrixEmitter, MatrixSendClient } from "./MatrixEmitter"; + +export class ModCache { + private modRoomMembers: string[] = []; + private ignoreList: string[] = []; + private client: MatrixSendClient; + private emitter: MatrixEmitter; + private managementRoomId: string; + private ttl: number = 1000 * 60 * 60; // 60 minutes + private lastInvalidation = 0; + private interval: any; + + constructor(client: MatrixSendClient, emitter: MatrixEmitter, managementRoomId: string) { + this.client = client; + this.emitter = emitter; + this.managementRoomId = managementRoomId; + this.lastInvalidation = Date.now(); + this.init(); + } + + /** + * Initially populate cache and set bot listening for membership events in moderation room + */ + async init() { + await this.populateCache(); + this.interval = setInterval( + () => { + if (Date.now() - this.lastInvalidation > this.ttl) { + this.populateCache(); + } + }, + 1000 * 60, // check invalidation status every minute + ); + this.emitter.on("room.event", async (roomId: string, event: any) => { + if (roomId === this.managementRoomId && event.type === "m.room.member") { + await this.populateCache(); + this.lastInvalidation = Date.now(); + } + }); + } + + /** + * Populate the cache by fetching moderation room membership events + */ + public async populateCache() { + const memberEvents = await this.client.getRoomMembers( + this.managementRoomId, + undefined, + ["join", "invite"], + ["ban", "leave"], + ); + this.modRoomMembers = []; + memberEvents.forEach((event) => { + if (!this.modRoomMembers.includes(event.stateKey)) { + this.modRoomMembers.push(event.stateKey); + } + const server = event.stateKey.split(":")[1]; + if (!this.modRoomMembers.includes(server)) { + this.modRoomMembers.push(server); + } + }); + } + + /** + * Check if a given entity is in cache + */ + public checkMembership(entity: string) { + return this.modRoomMembers.includes(entity) || this.ignoreList.includes(entity); + } + + /** + * Add a given entity to the list of users/servers who will not be banned but are not necessarily in moderator room + */ + public addToIgnore(entity: string) { + this.ignoreList.push(entity); + } + + /** + * Return a list of entities to ignore bans/ACLs for + */ + public listIgnored() { + return this.ignoreList; + } + + /** + * Return a list of both ignored entities and moderator room members + */ + public listAll() { + return this.ignoreList.concat(this.modRoomMembers); + } + + /** + * Clear the interval which refreshes cache + */ + public stop() { + clearInterval(this.interval); + } +} diff --git a/src/ProtectedRoomsSet.ts b/src/ProtectedRoomsSet.ts index c1e78b25..a88b414a 100644 --- a/src/ProtectedRoomsSet.ts +++ b/src/ProtectedRoomsSet.ts @@ -27,6 +27,7 @@ import { ProtectionManager } from "./protections/ProtectionManager"; import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue"; import { ProtectedRoomActivityTracker } from "./queues/ProtectedRoomActivityTracker"; import { htmlEscape } from "./utils"; +import { ModCache } from "./ModCache"; /** * This class aims to synchronize `m.ban` rules in a set of policy lists with @@ -113,6 +114,7 @@ export class ProtectedRoomsSet { private readonly managementRoomOutput: ManagementRoomOutput, private readonly protectionManager: ProtectionManager, private readonly config: IConfig, + private readonly moderators: ModCache, ) { for (const reason of this.config.automaticallyRedactForReasons) { this.automaticRedactionReasons.push(new MatrixGlob(reason.toLowerCase())); @@ -446,6 +448,15 @@ export class ProtectedRoomsSet { ); if (!this.config.noop) { + if (this.moderators.checkMembership(member.userId)) { + await this.managementRoomOutput.logMessage( + LogLevel.WARN, + "ApplyBan", + `Attempted + to ban ${member.userId} but this is a member of the management room, skipping.`, + ); + continue; + } await this.client.banUser(member.userId, roomId, memberAccess.rule!.reason); if (this.automaticRedactGlobs.find((g) => g.test(reason.toLowerCase()))) { this.redactUser(member.userId, roomId); diff --git a/src/appservice/AppService.ts b/src/appservice/AppService.ts index 4c8f8c86..275d7c5f 100644 --- a/src/appservice/AppService.ts +++ b/src/appservice/AppService.ts @@ -208,6 +208,7 @@ export class MjolnirAppService { await this.dataStore.close(); await this.api.close(); this.openMetrics.stop(); + this.mjolnirManager.closeAll(); } /** diff --git a/src/appservice/MjolnirManager.ts b/src/appservice/MjolnirManager.ts index fd457d23..e0d4fe36 100644 --- a/src/appservice/MjolnirManager.ts +++ b/src/appservice/MjolnirManager.ts @@ -163,6 +163,15 @@ export class MjolnirManager { } } + /** + * Stop all the managed mjolnirs + */ + public closeAll() { + for (const mjolnir of this.perMjolnirId.values()) { + mjolnir.stop(); + } + } + /** * Utility that creates a matrix client for a virtual user on our homeserver with the specified loclapart. * @param localPart The localpart of the virtual user we need a client for. @@ -246,6 +255,13 @@ export class ManagedMjolnir { await this.mjolnir.start(); } + /** + * Stop Mjolnir from syncing and processing commands. + */ + public stop() { + this.mjolnir.stop(); + } + public async getUserId(): Promise { return await this.mjolnir.client.getUserId(); } diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index faa2efe3..4c3ae17b 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -50,6 +50,7 @@ import { execSinceCommand } from "./SinceCommand"; import { execSetupProtectedRoom } from "./SetupDecentralizedReportingCommand"; import { execSuspendCommand } from "./SuspendCommand"; import { execUnsuspendCommand } from "./UnsuspendCommand"; +import { execIgnoreCommand, execListIgnoredCommand } from "./IgnoreCommand"; export const COMMAND_PREFIX = "!mjolnir"; @@ -141,6 +142,10 @@ export async function handleCommand(roomId: string, event: { content: { body: st return await execSuspendCommand(roomId, event, mjolnir, parts); } else if (parts[1] === "unsuspend" && parts.length > 2) { return await execUnsuspendCommand(roomId, event, mjolnir, parts); + } else if (parts[1] === "ignore") { + return await execIgnoreCommand(roomId, event, mjolnir, parts); + } else if (parts[1] === "ignored") { + return await execListIgnoredCommand(roomId, event, mjolnir, parts); } else { // Help menu const menu = @@ -184,8 +189,10 @@ export async function handleCommand(roomId: string, event: { content: { body: st "!mjolnir shutdown room [message] - Uses the bot's account to shut down a room, preventing access to the room on this server\n" + "!mjolnir powerlevel [room alias/ID] - Sets the power level of the user in the specified room (or all protected rooms)\n" + "!mjolnir make admin [user alias/ID] - Make the specified user or the bot itself admin of the room\n" + - "!mjolnir suspend - Suspend the specified user" + - "!mjolnir unsuspend - Unsuspend the specified user" + + "!mjolnir suspend - Suspend the specified user\n" + + "!mjolnir unsuspend - Unsuspend the specified user\n" + + "!mjolnir ignore - Add user to list of users/servers that cannot be banned/ACL'd. Note that this does not survive restart.\n" + + "!mjolnir ignored - List currently ignored entities.\n" + "!mjolnir help - This menu\n"; const html = `Mjolnir help:
${htmlEscape(menu)}
`; const text = `Mjolnir help:\n${menu}`; diff --git a/src/commands/IgnoreCommand.ts b/src/commands/IgnoreCommand.ts new file mode 100644 index 00000000..0788de82 --- /dev/null +++ b/src/commands/IgnoreCommand.ts @@ -0,0 +1,49 @@ +/* +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 { Mjolnir } from "../Mjolnir"; +import { LogLevel, RichReply } from "@vector-im/matrix-bot-sdk"; + +// !mjolnir ignore +export async function execIgnoreCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { + const target = parts[2]; + + await mjolnir.managementRoomOutput.logMessage( + LogLevel.INFO, + "IgnoreCommand", + `Adding ${target} to internal moderator list.`, + ); + mjolnir.moderators.addToIgnore(target); + await mjolnir.client.unstableApis.addReactionToEvent(roomId, event["event_id"], "✅"); +} + +// !mjolnir ignored +export async function execListIgnoredCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { + let html = "Ignored users:
    "; + let text = "Ignored users:\n"; + + for (const name of mjolnir.moderators.listIgnored()) { + html += `
  • ${name}
  • `; + text += `* ${name}\n`; + } + + html += "
"; + + const reply = RichReply.createFor(roomId, event, text, html); + reply["msgtype"] = "m.notice"; + await mjolnir.client.sendMessage(roomId, reply); + await mjolnir.client.unstableApis.addReactionToEvent(roomId, event["event_id"], "✅"); +} diff --git a/src/commands/SinceCommand.ts b/src/commands/SinceCommand.ts index 2b947773..5c25f0a8 100644 --- a/src/commands/SinceCommand.ts +++ b/src/commands/SinceCommand.ts @@ -266,6 +266,14 @@ async function execSinceCommandAux( case Action.Ban: { for (let join of recentJoins) { try { + if (mjolnir.moderators.checkMembership(join.userId)) { + await mjolnir.managementRoomOutput.logMessage( + LogLevel.WARN, + "SinceCommand", + `Attempting to ban user ${join.userId} but this is a member of the management room, skipping.`, + ); + continue; + } await mjolnir.client.banUser(join.userId, targetRoomId, reason); results.succeeded.push(join.userId); } catch (ex) { diff --git a/src/commands/UnbanBanCommand.ts b/src/commands/UnbanBanCommand.ts index d72ac3b4..66d2a86a 100644 --- a/src/commands/UnbanBanCommand.ts +++ b/src/commands/UnbanBanCommand.ts @@ -125,6 +125,19 @@ export async function execBanCommand(roomId: string, event: any, mjolnir: Mjolni const bits = await parseArguments(roomId, event, mjolnir, parts); if (!bits) return; // error already handled + const matcher = new MatrixGlob(bits.entity); + const moderators = mjolnir.moderators.listAll(); + moderators.forEach(async (name) => { + if (matcher.test(name)) { + await mjolnir.managementRoomOutput.logMessage( + LogLevel.ERROR, + "UnbanBanCommand", + `The ban command ${bits.entity} matches user in moderation room ${name}, aborting command.`, + ); + return; + } + }); + await bits.list!.banEntity(bits.ruleType!, bits.entity, bits.reason); await mjolnir.client.unstableApis.addReactionToEvent(roomId, event["event_id"], "✅"); } diff --git a/src/protections/BasicFlooding.ts b/src/protections/BasicFlooding.ts index d6ff7dad..d6ddc451 100644 --- a/src/protections/BasicFlooding.ts +++ b/src/protections/BasicFlooding.ts @@ -75,6 +75,14 @@ export class BasicFlooding extends Protection { roomId, ); if (!mjolnir.config.noop) { + if (mjolnir.moderators.checkMembership(event["sender"])) { + mjolnir.managementRoomOutput.logMessage( + LogLevel.WARN, + "BasicFlooding", + `Attempting to ban ${event["sender"]} but this is a member of the management room, aborting.`, + ); + return; + } await mjolnir.client.banUser(event["sender"], roomId, "spam"); } else { await mjolnir.managementRoomOutput.logMessage( diff --git a/src/protections/FirstMessageIsImage.ts b/src/protections/FirstMessageIsImage.ts index 026fa5de..51574f96 100644 --- a/src/protections/FirstMessageIsImage.ts +++ b/src/protections/FirstMessageIsImage.ts @@ -64,6 +64,14 @@ export class FirstMessageIsImage extends Protection { `Banning ${event["sender"]} for posting an image as the first thing after joining in ${roomId}.`, ); if (!mjolnir.config.noop) { + if (mjolnir.moderators.checkMembership(event["sender"])) { + await mjolnir.managementRoomOutput.logMessage( + LogLevel.WARN, + "FirstMessageIsImage", + `Attempting to ban ${event["sender"]} but they are member of moderation room, aborting.`, + ); + return; + } await mjolnir.client.banUser(event["sender"], roomId, "spam"); } else { await mjolnir.managementRoomOutput.logMessage( diff --git a/src/protections/ProtectionManager.ts b/src/protections/ProtectionManager.ts index fda1bdbd..485f05e1 100644 --- a/src/protections/ProtectionManager.ts +++ b/src/protections/ProtectionManager.ts @@ -286,6 +286,14 @@ export class ProtectionManager { if (consequence.name === "alert") { /* take no additional action, just print the below message to management room */ } else if (consequence.name === "ban") { + if (this.mjolnir.moderators.checkMembership(sender)) { + await this.mjolnir.managementRoomOutput.logMessage( + LogLevel.WARN, + "ProtectionManager", + `Attempting to ban ${sender} but this is a member of management room, skipping.`, + ); + continue; + } await this.mjolnir.client.banUser(sender, roomId, "abuse detected"); } else if (consequence.name === "redact") { await this.mjolnir.client.redactEvent(roomId, eventId, "abuse detected"); diff --git a/src/protections/TrustedReporters.ts b/src/protections/TrustedReporters.ts index 6277e86b..4ebc8e67 100644 --- a/src/protections/TrustedReporters.ts +++ b/src/protections/TrustedReporters.ts @@ -17,6 +17,7 @@ limitations under the License. import { Protection } from "./IProtection"; import { MXIDListProtectionSetting, NumberProtectionSetting } from "./ProtectionSettings"; import { Mjolnir } from "../Mjolnir"; +import { LogLevel } from "@vector-im/matrix-bot-sdk"; const MAX_REPORTED_EVENT_BACKLOG = 20; @@ -82,6 +83,15 @@ export class TrustedReporters extends Protection { await mjolnir.client.redactEvent(roomId, event.id, "abuse detected"); } if (reporters.size === this.settings.banThreshold.value) { + if (mjolnir.moderators.checkMembership(event.userId)) { + await mjolnir.managementRoomOutput.logMessage( + LogLevel.WARN, + "TrustedReporters", + `Attempting to ban + ${event.userId} but this is a member of the management room, aborting.`, + ); + return; + } met.push("ban"); await mjolnir.client.banUser(event.userId, roomId, "abuse detected"); } diff --git a/src/report/ReportManager.ts b/src/report/ReportManager.ts index e9d6d51e..a93dc133 100644 --- a/src/report/ReportManager.ts +++ b/src/report/ReportManager.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { PowerLevelAction } from "@vector-im/matrix-bot-sdk/lib/models/PowerLevelAction"; -import { LogService, UserID } from "@vector-im/matrix-bot-sdk"; +import { LogLevel, LogService, UserID } from "@vector-im/matrix-bot-sdk"; import { htmlToText } from "html-to-text"; import { htmlEscape } from "../utils"; import { JSDOM } from "jsdom"; @@ -770,6 +770,14 @@ class BanAccused implements IUIAction { return `Ban ${htmlEscape(report.accused_id)} from room ${htmlEscape(report.room_alias_or_id)}`; } public async execute(manager: ReportManager, report: IReport): Promise { + if (manager.mjolnir.moderators.checkMembership(report.accused_id)) { + await manager.mjolnir.managementRoomOutput.logMessage( + LogLevel.WARN, + "ReportManager", + `Attempting to ban ${report.accused_id} but this is a member of management room, aborting.`, + ); + return; + } await manager.mjolnir.client.banUser(report.accused_id, report.room_id); return; } diff --git a/test/integration/dontBanSelfTest.ts b/test/integration/dontBanSelfTest.ts new file mode 100644 index 00000000..d7d5d750 --- /dev/null +++ b/test/integration/dontBanSelfTest.ts @@ -0,0 +1,148 @@ +/* +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 } from "@vector-im/matrix-bot-sdk"; +import { newTestUser } from "./clientHelper"; +import { strict as assert } from "assert"; +import { getFirstReaction } from "./commands/commandUtils"; + +describe("Test: Bot doesn't ban moderation room members or ignored entities.", function () { + let client: MatrixClient; + let room: string; + let badClient: MatrixClient; + this.beforeEach(async function () { + client = await newTestUser(this.config.homeserverUrl, { name: { contains: "mod-room-test" } }); + await client.start(); + const mjolnirId = await this.mjolnir.client.getUserId(); + badClient = await newTestUser(this.config.homeserverUrl, { name: { contains: "mod-room-test-to-be-banned" } }); + const badId = await badClient.getUserId(); + room = await client.createRoom({ invite: [mjolnirId, badId] }); + await badClient.joinRoom(room); + await client.joinRoom(this.config.managementRoom); + await client.setUserPowerLevel(mjolnirId, room, 100); + }); + this.afterEach(async function () { + await client.stop(); + }); + + it("Properly tracks newly joined members in the moderation room", async function () { + this.timeout(20000); + + function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + await delay(4000); + const currentMods = this.mjolnir.moderators.listAll(); + let expectedCurrentMods = [await client.getUserId(), await this.mjolnir.client.getUserId()]; + expectedCurrentMods.forEach((mod) => { + if (!currentMods.includes(mod)) { + assert.fail("Expected mod not found."); + } + }); + const newMod = await newTestUser(this.config.homeserverUrl, { name: { contains: "mod-room-test" } }); + await newMod.joinRoom(this.config.managementRoom); + await delay(1000); + let updatedMods = this.mjolnir.moderators.listAll(); + if (!updatedMods.includes(await newMod.getUserId())) { + assert.fail("New moderator not found."); + } + }); + + it("Ignore command adds entities to ignore list.", async function () { + this.timeout(20000); + + function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + const helpfulBot = await newTestUser(this.config.homeserverUrl, { name: { contains: "mod-room-test-bot" } }); + const botId = await helpfulBot.getUserId(); + + await getFirstReaction(client, this.mjolnir.managementRoomId, "✅", async () => { + return await client.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text", + body: `!mjolnir ignore ${botId}`, + }); + }); + await delay(1000); + const mods = this.mjolnir.moderators.listAll(); + if (!mods.includes(botId)) { + assert.fail("Bot not added to moderator list."); + } + }); + + it("Moderators and ignored entities are not banned by automatic procedures.", async function () { + this.timeout(20000); + function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + await client.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text", + body: `!mjolnir rooms add ${room}`, + }); + await getFirstReaction(client, this.mjolnir.managementRoomId, "✅", async () => { + return await client.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text", + body: `!mjolnir enable BasicFloodingProtection`, + }); + }); + + for (let i = 0; i < 12; i++) { + await badClient.sendMessage(room, { msgtype: "m.text", body: "ban me" }); + } + + await delay(3000); + const badId = await badClient.getUserId(); + const badMemberEvent = await this.mjolnir.client.getRoomStateEvent(room, "m.room.member", badId); + if (badMemberEvent["membership"] !== "ban") { + assert.fail("Basic flooding protection is not working, this user should have been banned."); + } + + for (let i = 0; i < 12; i++) { + await this.mjolnir.client.sendMessage(room, { msgtype: "m.text", body: "doing mod things" }); + } + const mjolnirId = await this.mjolnir.client.getUserId(); + const mjolnirMemberEvent = await this.mjolnir.client.getRoomStateEvent(room, "m.room.member", mjolnirId); + + if (mjolnirMemberEvent["membership"] === "ban") { + assert.fail("Bot has banned itself."); + } + + const helpfulBot = await newTestUser(this.config.homeserverUrl, { name: { contains: "mod-room-test-bot" } }); + const botId = await helpfulBot.getUserId(); + + await this.mjolnir.client.inviteUser(botId, room); + await helpfulBot.joinRoom(room); + + await getFirstReaction(client, this.mjolnir.managementRoomId, "✅", async () => { + return await client.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text", + body: `!mjolnir ignore ${botId}`, + }); + }); + + for (let i = 0; i < 12; i++) { + await helpfulBot.sendMessage(room, { msgtype: "m.text", body: "doing helpful bot things" }); + } + const botMemberEvent = await this.mjolnir.client.getRoomStateEvent(room, "m.room.member", botId); + + if (botMemberEvent["membership"] === "ban") { + assert.fail("Bot has banned a member of ignore list."); + } + }); +}); diff --git a/test/integration/standardConsequenceTest.ts b/test/integration/standardConsequenceTest.ts index eea8d636..b83b3be8 100644 --- a/test/integration/standardConsequenceTest.ts +++ b/test/integration/standardConsequenceTest.ts @@ -65,7 +65,7 @@ describe("Test: standard consequences", function () { this.timeout(20000); let protectedRoomId = await this.mjolnir.client.createRoom({ invite: [await badUser.getUserId()] }); - await badUser.joinRoom(this.mjolnir.managementRoomId); + await goodUser.joinRoom(this.mjolnir.managementRoomId); await badUser.joinRoom(protectedRoomId); await this.mjolnir.addProtectedRoom(protectedRoomId); @@ -96,7 +96,7 @@ describe("Test: standard consequences", function () { ban = event; } }); - badUser.on("room.event", (roomId, event) => { + goodUser.on("room.event", (roomId, event) => { if ( roomId === this.mjolnir.managementRoomId && event?.type === "m.room.message" && diff --git a/yarn.lock b/yarn.lock index 4fef91cf..29113bca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3218,11 +3218,6 @@ prelude-ls@~1.1.2: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== -prettier@3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" - integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== - pretty-format@^27.2.4: version "27.2.4" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.2.4.tgz"