From 3ce930d4a69f54f81d269cf514ddc5df4c216adc Mon Sep 17 00:00:00 2001 From: "H. Shay" Date: Wed, 23 Oct 2024 10:18:49 -0700 Subject: [PATCH 1/7] don't allow bot to demote itself in protected room --- src/commands/SetPowerLevelCommand.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/commands/SetPowerLevelCommand.ts b/src/commands/SetPowerLevelCommand.ts index 7a76552f..d42a766d 100644 --- a/src/commands/SetPowerLevelCommand.ts +++ b/src/commands/SetPowerLevelCommand.ts @@ -23,12 +23,27 @@ export async function execSetPowerLevelCommand(roomId: string, event: any, mjoln const level = Math.round(Number(parts[3])); const inRoom = parts[4]; + const mjolnirId = await mjolnir.client.getUserId(); + let targetRooms = inRoom ? [await mjolnir.client.resolveRoom(inRoom)] : mjolnir.protectedRoomsTracker.getProtectedRooms(); for (const targetRoomId of targetRooms) { try { + if (target === mjolnirId) { + // don't let the bot demote itself + const currentLevels = await mjolnir.client.getRoomStateEvent(targetRoomId, "m.room.power_levels", ""); + const botLevel = currentLevels["users"][mjolnirId]; + if (level < botLevel) { + await mjolnir.managementRoomOutput.logMessage( + LogLevel.INFO, + "PowerLevelCommand", + `You are attempting to lower the bot's power level: current level ${botLevel}, requested level ${level}, aborting.`, + ); + return; + } + } await mjolnir.client.setUserPowerLevel(target, targetRoomId, level); } catch (e) { const message = e.message || (e.body ? e.body.error : ""); From af607301688358b62c1b02641e2b517caebc958b Mon Sep 17 00:00:00 2001 From: "H. Shay" Date: Wed, 23 Oct 2024 10:18:54 -0700 Subject: [PATCH 2/7] test --- .../commands/powerLevelCommandTest.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 test/integration/commands/powerLevelCommandTest.ts diff --git a/test/integration/commands/powerLevelCommandTest.ts b/test/integration/commands/powerLevelCommandTest.ts new file mode 100644 index 00000000..a63ab4c6 --- /dev/null +++ b/test/integration/commands/powerLevelCommandTest.ts @@ -0,0 +1,59 @@ +/* +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 { strict as assert } from "assert"; +import { newTestUser } from "../clientHelper"; + +describe("Test: power levels", function () { + it("Does not allow the bot to demote itself in a protected room.", async function () { + this.timeout(60000); + const mod = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); + await mod.joinRoom(this.config.managementRoom); + const targetRoom = await mod.createRoom({ preset: "public_chat" }); + await this.mjolnir.client.joinRoom(targetRoom); + const botId = await this.mjolnir.client.getUserId(); + await mod.setUserPowerLevel(botId, targetRoom, 100); + + await mod.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text.", + body: `!mjolnir rooms add ${targetRoom}`, + }); + + await mod.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text", + body: `!mjolnir powerlevel ${botId} 50 ${targetRoom}`, + }); + + mod.start(); + let reply = new Promise((resolve, reject) => { + mod.on("room.message", (roomId: string, event: any) => { + if ( + roomId === this.mjolnir.managementRoomId && + event.content?.body.includes("You are attempting to lower the bot's power level") + ) { + resolve(event); + } + }); + }); + await reply; + + const currentLevels = await mod.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); + const botLevel = currentLevels["users"][botId]; + assert.equal(botLevel, 100); + + mod.stop(); + }); +}); From 72b2025b701dca08a92081536bb97c02069d03eb Mon Sep 17 00:00:00 2001 From: "H. Shay" Date: Wed, 23 Oct 2024 10:35:14 -0700 Subject: [PATCH 3/7] also apply this behavior to members of moderation room --- src/commands/SetPowerLevelCommand.ts | 8 ++--- .../commands/powerLevelCommandTest.ts | 33 +++++++++++++++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/commands/SetPowerLevelCommand.ts b/src/commands/SetPowerLevelCommand.ts index d42a766d..ce3a8663 100644 --- a/src/commands/SetPowerLevelCommand.ts +++ b/src/commands/SetPowerLevelCommand.ts @@ -31,15 +31,15 @@ export async function execSetPowerLevelCommand(roomId: string, event: any, mjoln for (const targetRoomId of targetRooms) { try { - if (target === mjolnirId) { + if (target === mjolnirId || mjolnir.moderators.checkMembership(target)) { // don't let the bot demote itself const currentLevels = await mjolnir.client.getRoomStateEvent(targetRoomId, "m.room.power_levels", ""); - const botLevel = currentLevels["users"][mjolnirId]; - if (level < botLevel) { + const targetLevel = currentLevels["users"][mjolnirId]; + if (level < targetLevel) { await mjolnir.managementRoomOutput.logMessage( LogLevel.INFO, "PowerLevelCommand", - `You are attempting to lower the bot's power level: current level ${botLevel}, requested level ${level}, aborting.`, + `You are attempting to lower the bot/a moderator's power level: current level ${targetLevel}, requested level ${level}, aborting.`, ); return; } diff --git a/test/integration/commands/powerLevelCommandTest.ts b/test/integration/commands/powerLevelCommandTest.ts index a63ab4c6..95f4e0f4 100644 --- a/test/integration/commands/powerLevelCommandTest.ts +++ b/test/integration/commands/powerLevelCommandTest.ts @@ -18,14 +18,20 @@ import { strict as assert } from "assert"; import { newTestUser } from "../clientHelper"; describe("Test: power levels", function () { - it("Does not allow the bot to demote itself in a protected room.", async function () { + it("Does not allow the bot to demote itself or members of management room in a protected room.", async function () { this.timeout(60000); const mod = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); + const mod2 = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator2" } }); await mod.joinRoom(this.config.managementRoom); + await mod2.joinRoom(this.config.managementRoom); + const targetRoom = await mod.createRoom({ preset: "public_chat" }); await this.mjolnir.client.joinRoom(targetRoom); + await mod2.joinRoom(targetRoom); const botId = await this.mjolnir.client.getUserId(); await mod.setUserPowerLevel(botId, targetRoom, 100); + const mod2Id = await mod2.getUserId(); + await mod.setUserPowerLevel(mod2Id, targetRoom, 100); await mod.sendMessage(this.mjolnir.managementRoomId, { msgtype: "m.text.", @@ -42,7 +48,7 @@ describe("Test: power levels", function () { mod.on("room.message", (roomId: string, event: any) => { if ( roomId === this.mjolnir.managementRoomId && - event.content?.body.includes("You are attempting to lower the bot's power level") + event.content?.body.includes("You are attempting to lower the bot/a moderator's power level") ) { resolve(event); } @@ -50,10 +56,31 @@ describe("Test: power levels", function () { }); await reply; - const currentLevels = await mod.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); + let currentLevels = await mod.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); const botLevel = currentLevels["users"][botId]; assert.equal(botLevel, 100); + await mod.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text", + body: `!mjolnir powerlevel ${mod2Id} 50 ${targetRoom}`, + }); + + let reply2 = new Promise((resolve, reject) => { + mod.on("room.message", (roomId: string, event: any) => { + if ( + roomId === this.mjolnir.managementRoomId && + event.content?.body.includes("You are attempting to lower the bot/a moderator's power level") + ) { + resolve(event); + } + }); + }); + await reply2; + + currentLevels = await mod.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); + const mod2Level = currentLevels["users"][mod2Id]; + assert.equal(mod2Level, 100); + mod.stop(); }); }); From 9b4c80664bffad7bc138d5ac7dde4163f70c88f7 Mon Sep 17 00:00:00 2001 From: "H. Shay" Date: Mon, 28 Oct 2024 11:21:08 -0700 Subject: [PATCH 4/7] allow check to be overriden with --force argument --- src/commands/CommandHandler.ts | 2 +- src/commands/SetPowerLevelCommand.ts | 32 ++++++++----- .../commands/powerLevelCommandTest.ts | 45 +++++++++++++++++++ 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index 4c3ae17b..5b7c5a02 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -187,7 +187,7 @@ export async function handleCommand(roomId: string, event: { content: { body: st "!mjolnir resolve - Resolves a room alias to a room ID\n" + "!mjolnir since / [rooms...] [reason] - Apply an action ('kick', 'ban', 'mute', 'unmute' or 'show') to all users who joined a room since / (up to users)\n" + "!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 powerlevel [room alias/ID] - Sets the power level of the user in the specified room (or all protected rooms) - mjolnir will resist lowering the power level of the bot/users in the moderation room unless a --force argument is added\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\n" + "!mjolnir unsuspend - Unsuspend the specified user\n" + diff --git a/src/commands/SetPowerLevelCommand.ts b/src/commands/SetPowerLevelCommand.ts index ce3a8663..a042b3c8 100644 --- a/src/commands/SetPowerLevelCommand.ts +++ b/src/commands/SetPowerLevelCommand.ts @@ -29,19 +29,31 @@ export async function execSetPowerLevelCommand(roomId: string, event: any, mjoln ? [await mjolnir.client.resolveRoom(inRoom)] : mjolnir.protectedRoomsTracker.getProtectedRooms(); + let force = false; + if (parts[parts.length - 1] === "--force") { + force = true; + parts.pop(); + } + for (const targetRoomId of targetRooms) { try { - if (target === mjolnirId || mjolnir.moderators.checkMembership(target)) { - // don't let the bot demote itself - const currentLevels = await mjolnir.client.getRoomStateEvent(targetRoomId, "m.room.power_levels", ""); - const targetLevel = currentLevels["users"][mjolnirId]; - if (level < targetLevel) { - await mjolnir.managementRoomOutput.logMessage( - LogLevel.INFO, - "PowerLevelCommand", - `You are attempting to lower the bot/a moderator's power level: current level ${targetLevel}, requested level ${level}, aborting.`, + if (!force) { + if (target === mjolnirId || mjolnir.moderators.checkMembership(target)) { + // don't let the bot demote itself or members of moderation room + const currentLevels = await mjolnir.client.getRoomStateEvent( + targetRoomId, + "m.room.power_levels", + "", ); - return; + const targetLevel = currentLevels["users"][mjolnirId]; + if (level < targetLevel) { + await mjolnir.managementRoomOutput.logMessage( + LogLevel.INFO, + "PowerLevelCommand", + `You are attempting to lower the bot/a moderator's power level: current level ${targetLevel}, requested level ${level}, aborting. This check can be overriden with a --force argument at the end of the command.`, + ); + return; + } } } await mjolnir.client.setUserPowerLevel(target, targetRoomId, level); diff --git a/test/integration/commands/powerLevelCommandTest.ts b/test/integration/commands/powerLevelCommandTest.ts index 95f4e0f4..8ba9f6a9 100644 --- a/test/integration/commands/powerLevelCommandTest.ts +++ b/test/integration/commands/powerLevelCommandTest.ts @@ -16,6 +16,7 @@ limitations under the License. import { strict as assert } from "assert"; import { newTestUser } from "../clientHelper"; +import { getFirstReaction } from "./commandUtils"; describe("Test: power levels", function () { it("Does not allow the bot to demote itself or members of management room in a protected room.", async function () { @@ -83,4 +84,48 @@ describe("Test: power levels", function () { mod.stop(); }); + + it("Does allow the bot to demote itself or members of management room in a protected room with a --force argument.", async function () { + this.timeout(60000); + const mod = await newTestUser(this.config.homeserverUrl, { name: { contains: "force-moderator" } }); + const mod2 = await newTestUser(this.config.homeserverUrl, { name: { contains: "force-moderator2" } }); + await mod.joinRoom(this.config.managementRoom); + await mod2.joinRoom(this.config.managementRoom); + + const targetRoom = await mod.createRoom({ preset: "public_chat" }); + await this.mjolnir.client.joinRoom(targetRoom); + await mod2.joinRoom(targetRoom); + const botId = await this.mjolnir.client.getUserId(); + await mod.setUserPowerLevel(botId, targetRoom, 100); + const mod2Id = await mod2.getUserId(); + await mod.setUserPowerLevel(mod2Id, targetRoom, 75); + + await mod.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text.", + body: `!mjolnir rooms add ${targetRoom}`, + }); + + mod.start(); + await getFirstReaction(mod, this.mjolnir.managementRoomId, "✅", async () => { + return await mod.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text", + body: `!mjolnir powerlevel ${mod2Id} 50 ${targetRoom} --force`, + }); + }); + let currentLevels = await mod.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); + const mod2Level = currentLevels["users"][mod2Id]; + assert.equal(mod2Level, 50); + + await getFirstReaction(mod, this.mjolnir.managementRoomId, "✅", async () => { + return await mod.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text", + body: `!mjolnir powerlevel ${botId} 50 ${targetRoom} --force`, + }); + }); + currentLevels = await mod.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); + const botLevel = currentLevels["users"][botId]; + assert.equal(botLevel, 50); + + mod.stop(); + }); }); From 91a947e4ad6455b0d35a080203b893f8a8619bcd Mon Sep 17 00:00:00 2001 From: "H. Shay" Date: Tue, 5 Nov 2024 15:56:16 -0800 Subject: [PATCH 5/7] refuse to allow bot to lower own power level --- src/commands/SetPowerLevelCommand.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/commands/SetPowerLevelCommand.ts b/src/commands/SetPowerLevelCommand.ts index a042b3c8..e6fe8fe6 100644 --- a/src/commands/SetPowerLevelCommand.ts +++ b/src/commands/SetPowerLevelCommand.ts @@ -37,25 +37,33 @@ export async function execSetPowerLevelCommand(roomId: string, event: any, mjoln for (const targetRoomId of targetRooms) { try { - if (!force) { - if (target === mjolnirId || mjolnir.moderators.checkMembership(target)) { - // don't let the bot demote itself or members of moderation room - const currentLevels = await mjolnir.client.getRoomStateEvent( + const currentLevels = await mjolnir.client.getRoomStateEvent( targetRoomId, "m.room.power_levels", "", ); - const targetLevel = currentLevels["users"][mjolnirId]; - if (level < targetLevel) { + const currentLevel = currentLevels["users"][mjolnirId]; + if (!force) { + if (mjolnir.moderators.checkMembership(target)) { + // don't let the bot demote members of moderation room without --force arg + if (level < currentLevel) { await mjolnir.managementRoomOutput.logMessage( LogLevel.INFO, "PowerLevelCommand", - `You are attempting to lower the bot/a moderator's power level: current level ${targetLevel}, requested level ${level}, aborting. This check can be overriden with a --force argument at the end of the command.`, + `You are attempting to lower the bot/a moderator's power level: current level ${currentLevel}, requested level ${level}, aborting. This check can be overriden with a --force argument at the end of the command.`, ); return; } } } + if (target === mjolnirId && level < currentLevel) { + await mjolnir.managementRoomOutput.logMessage( + LogLevel.INFO, + "PowerLevelCommand", + `You are attempting to lower the bot power level: current level ${currentLevel}, requested level ${level}, aborting.`, + ); + return; + } await mjolnir.client.setUserPowerLevel(target, targetRoomId, level); } catch (e) { const message = e.message || (e.body ? e.body.error : ""); From b7ac833c88e68ecaa38d1d39be60c330c23b8178 Mon Sep 17 00:00:00 2001 From: "H. Shay" Date: Tue, 5 Nov 2024 16:57:28 -0800 Subject: [PATCH 6/7] lint --- src/commands/SetPowerLevelCommand.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/commands/SetPowerLevelCommand.ts b/src/commands/SetPowerLevelCommand.ts index e6fe8fe6..e6234e65 100644 --- a/src/commands/SetPowerLevelCommand.ts +++ b/src/commands/SetPowerLevelCommand.ts @@ -37,11 +37,7 @@ export async function execSetPowerLevelCommand(roomId: string, event: any, mjoln for (const targetRoomId of targetRooms) { try { - const currentLevels = await mjolnir.client.getRoomStateEvent( - targetRoomId, - "m.room.power_levels", - "", - ); + const currentLevels = await mjolnir.client.getRoomStateEvent(targetRoomId, "m.room.power_levels", ""); const currentLevel = currentLevels["users"][mjolnirId]; if (!force) { if (mjolnir.moderators.checkMembership(target)) { @@ -58,10 +54,10 @@ export async function execSetPowerLevelCommand(roomId: string, event: any, mjoln } if (target === mjolnirId && level < currentLevel) { await mjolnir.managementRoomOutput.logMessage( - LogLevel.INFO, - "PowerLevelCommand", - `You are attempting to lower the bot power level: current level ${currentLevel}, requested level ${level}, aborting.`, - ); + LogLevel.INFO, + "PowerLevelCommand", + `You are attempting to lower the bot power level: current level ${currentLevel}, requested level ${level}, aborting.`, + ); return; } await mjolnir.client.setUserPowerLevel(target, targetRoomId, level); From 11b4cde94f9ca2c8d379097487d33b51d2d6a720 Mon Sep 17 00:00:00 2001 From: "H. Shay" Date: Tue, 5 Nov 2024 17:57:27 -0800 Subject: [PATCH 7/7] amend test to reflect bot not demoting itself --- test/integration/commands/powerLevelCommandTest.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/test/integration/commands/powerLevelCommandTest.ts b/test/integration/commands/powerLevelCommandTest.ts index 8ba9f6a9..71127757 100644 --- a/test/integration/commands/powerLevelCommandTest.ts +++ b/test/integration/commands/powerLevelCommandTest.ts @@ -85,7 +85,7 @@ describe("Test: power levels", function () { mod.stop(); }); - it("Does allow the bot to demote itself or members of management room in a protected room with a --force argument.", async function () { + it("Does allow the bot to demote members of management room in a protected room with a --force argument.", async function () { this.timeout(60000); const mod = await newTestUser(this.config.homeserverUrl, { name: { contains: "force-moderator" } }); const mod2 = await newTestUser(this.config.homeserverUrl, { name: { contains: "force-moderator2" } }); @@ -116,16 +116,6 @@ describe("Test: power levels", function () { const mod2Level = currentLevels["users"][mod2Id]; assert.equal(mod2Level, 50); - await getFirstReaction(mod, this.mjolnir.managementRoomId, "✅", async () => { - return await mod.sendMessage(this.mjolnir.managementRoomId, { - msgtype: "m.text", - body: `!mjolnir powerlevel ${botId} 50 ${targetRoom} --force`, - }); - }); - currentLevels = await mod.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); - const botLevel = currentLevels["users"][botId]; - assert.equal(botLevel, 50); - mod.stop(); }); });