From 9a31f5a6f7b4abcab2ab96f56024cefd4d9b4264 Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Sun, 22 Aug 2021 14:18:29 +0200 Subject: [PATCH 01/13] Add Visual Studio ignores --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 2d50e9dc..68e477ce 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,6 @@ typings/ # Python packing directories. mjolnir.egg-info/ + +# VS +.vs From 5400b0f8be47c557d4747cb8667df5131348d514 Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Sun, 22 Aug 2021 14:21:18 +0200 Subject: [PATCH 02/13] Add commands to elevate a user or the bot as room admin --- src/Mjolnir.ts | 7 ++++++ src/commands/CommandHandler.ts | 4 ++++ src/commands/MakeRoomAdminCommand.ts | 33 ++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 src/commands/MakeRoomAdminCommand.ts diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index b134afd9..6897e460 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -658,4 +658,11 @@ export class Mjolnir { message: message /* If `undefined`, we'll use Synapse's default message. */ }); } + + public async makeUserRoomAdmin(roomId: string, userId?: string): Promise { + const endpoint = `/_synapse/admin/v1/rooms/${roomId}/make_room_admin`; + return await this.client.doRequest("POST", endpoint, null, { + user_id: userId || await this.client.getUserId(), /* if not specified make the bot administrator */ + }); + } } diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index d26005f1..e26d577a 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -36,6 +36,7 @@ import { execSetPowerLevelCommand } from "./SetPowerLevelCommand"; import { execShutdownRoomCommand } from "./ShutdownRoomCommand"; import { execAddAliasCommand, execMoveAliasCommand, execRemoveAliasCommand, execResolveCommand } from "./AliasCommands"; import { execKickCommand } from "./KickCommand"; +import { execMakeRoomAdminCommand } from "./MakeRoomAdminCommand"; export const COMMAND_PREFIX = "!mjolnir"; @@ -100,6 +101,8 @@ export async function handleCommand(roomId: string, event: any, mjolnir: Mjolnir return await execShutdownRoomCommand(roomId, event, mjolnir, parts); } else if (parts[1] === 'kick' && parts.length > 2) { return await execKickCommand(roomId, event, mjolnir, parts); + } else if (parts[1] === 'make' && parts[2] === 'admin' && parts.length > 3) { + return await execMakeRoomAdminCommand(roomId, event, mjolnir, parts); } else { // Help menu const menu = "" + @@ -133,6 +136,7 @@ export async function handleCommand(roomId: string, event: any, mjolnir: Mjolnir "!mjolnir resolve - Resolves a room alias to a room ID\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 make admin [room alias/ID] - Make the specified user or the bot itself admin of the room\n" + "!mjolnir help - This menu\n"; const html = `Mjolnir help:
${htmlEscape(menu)}
`; const text = `Mjolnir help:\n${menu}`; diff --git a/src/commands/MakeRoomAdminCommand.ts b/src/commands/MakeRoomAdminCommand.ts new file mode 100644 index 00000000..2ac03778 --- /dev/null +++ b/src/commands/MakeRoomAdminCommand.ts @@ -0,0 +1,33 @@ +/* +Copyright 2021 Marco Cirillo + +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 { RichReply } from "matrix-bot-sdk"; + +// !mjolnir make admin [] +export async function execMakeRoomAdminCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { + const isAdmin = await mjolnir.isSynapseAdmin(); + if (!isAdmin) { + const message = "I am not a Synapse administrator, or the endpoint is blocked"; + const reply = RichReply.createFor(roomId, event, message, message); + reply['msgtype'] = "m.notice"; + mjolnir.client.sendMessage(roomId, reply); + return; + } + + await mjolnir.makeUserRoomAdmin(await mjolnir.client.resolveRoom(parts[3]), parts[4]); + await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); +} From eaf8399bfa196839a729e9a54370e652405894dd Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Wed, 6 Oct 2021 22:08:07 +0200 Subject: [PATCH 03/13] Document makeUserRoomAdmin() --- src/Mjolnir.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index c99805f9..326b217d 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -775,6 +775,12 @@ export class Mjolnir { }); } + /** + * Make a user administrator via the Synapse Admin API + * @param roomId the room where the user (or the bot) shall be made administrator. + * @param userId optionally specify the user mxID to be made administrator, if not specified the bot mxID will be used. + * @returns The list of errors encountered, for reporting to the management room. + */ public async makeUserRoomAdmin(roomId: string, userId?: string): Promise { const endpoint = `/_synapse/admin/v1/rooms/${roomId}/make_room_admin`; return await this.client.doRequest("POST", endpoint, null, { From 30a9f69e24884257f89f3517668ecd3571ed5a4b Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Wed, 9 Feb 2022 12:40:53 +0100 Subject: [PATCH 04/13] Add make admin command tests. --- .../commands/makedminCommandTest.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 test/integration/commands/makedminCommandTest.ts diff --git a/test/integration/commands/makedminCommandTest.ts b/test/integration/commands/makedminCommandTest.ts new file mode 100644 index 00000000..74a9ec97 --- /dev/null +++ b/test/integration/commands/makedminCommandTest.ts @@ -0,0 +1,78 @@ +import { strict as assert } from "assert"; + +import config from "../../../src/config"; +import { newTestUser } from "../clientHelper"; +import { PowerLevelAction } from "matrix-bot-sdk/lib/models/PowerLevelAction"; +import { LogService } from "matrix-bot-sdk"; +import { getFirstReaction } from "./commandUtils"; + +describe("Test: The make admin command", function () { + afterEach(function () { + this.moderator?.stop(); + this.userA?.stop(); + this.userB?.stop(); + }); + + it('Mjölnir make the bot self room administrator', async function () { + this.timeout(60000); + const mjolnir = config.RUNTIME.client!; + const mjolnirUserId = await mjolnir.getUserId(); + const moderator = await newTestUser({ name: { contains: "moderator" } }); + this.moderator = moderator; + + await moderator.joinRoom(config.managementRoom); + LogService.debug("makeadminTest", `Joining managementRoom: ${config.managementRoom}`); + let targetRoom = await moderator.createRoom({ invite: [mjolnirUserId] }); + LogService.debug("makeadminTest", `moderator creating targetRoom: ${targetRoom}; and inviting ${mjolnirUserId}`); + await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}` }); + LogService.debug("makeadminTest", `Adding targetRoom: ${targetRoom}`); + try { + await moderator.start(); + await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { + LogService.debug("makeadminTest", `Sending: !mjolnir make admin ${targetRoom}`); + return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir make admin ${targetRoom}` }); + }); + } finally { + await moderator.stop(); + } + LogService.debug("makeadminTest", `Making self admin`); + + assert.ok(await mjolnir.userHasPowerLevelForAction(mjolnirUserId, targetRoom, PowerLevelAction.Ban), "Bot user is now room admin."); + }); + + it('Mjölnir make the tester room administrator', async function () { + this.timeout(60000); + const mjolnir = config.RUNTIME.client!; + const moderator = await newTestUser({ name: { contains: "moderator" } }); + const userA = await newTestUser({ name: { contains: "a" } }); + const userB = await newTestUser({ name: { contains: "b" } }); + const userBId = await userB.getUserId(); + this.moderator = moderator; + this.userA = userA; + this.userB = userB; + + await moderator.joinRoom(this.mjolnir.managementRoomId); + LogService.debug("makeadminTest", `Joining managementRoom: ${this.mjolnir.managementRoomId}`); + let targetRoom = await userA.createRoom({ invite: [userBId] }); + LogService.debug("makeadminTest", `User A creating targetRoom: ${targetRoom}; and inviting ${userBId}`); + try { + await userB.start(); + userB.joinRoom(targetRoom); + } finally { + LogService.debug("makeadminTest", `${userBId} joining targetRoom: ${targetRoom}`); + await userB.stop(); + } + try { + await moderator.start(); + await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { + LogService.debug("makeadminTest", `Sending: !mjolnir make admin ${targetRoom} ${userBId}`); + return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text.', body: `!mjolnir make admin ${targetRoom} ${userBId}` }); + }); + } finally { + await moderator.stop(); + } + LogService.debug("makeadminTest", `Making User B admin`); + + await assert.ok(await userA.userHasPowerLevelForAction(userBId, targetRoom, PowerLevelAction.Ban), "User B is now room admin."); + }); +}); From 1cfc21af524a40e53ad3d5cbc2817d03c04474ec Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Wed, 9 Feb 2022 12:57:40 +0100 Subject: [PATCH 05/13] Correct msgtype. --- test/integration/commands/makedminCommandTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/commands/makedminCommandTest.ts b/test/integration/commands/makedminCommandTest.ts index 74a9ec97..b9d203c5 100644 --- a/test/integration/commands/makedminCommandTest.ts +++ b/test/integration/commands/makedminCommandTest.ts @@ -66,7 +66,7 @@ describe("Test: The make admin command", function () { await moderator.start(); await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { LogService.debug("makeadminTest", `Sending: !mjolnir make admin ${targetRoom} ${userBId}`); - return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text.', body: `!mjolnir make admin ${targetRoom} ${userBId}` }); + return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir make admin ${targetRoom} ${userBId}` }); }); } finally { await moderator.stop(); From ded25827ed282dccd09557d42da55f5c32fbf3bb Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Thu, 10 Feb 2022 02:24:40 +0100 Subject: [PATCH 06/13] Update copyright header. --- src/commands/MakeRoomAdminCommand.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/MakeRoomAdminCommand.ts b/src/commands/MakeRoomAdminCommand.ts index 2ac03778..4b921802 100644 --- a/src/commands/MakeRoomAdminCommand.ts +++ b/src/commands/MakeRoomAdminCommand.ts @@ -1,5 +1,5 @@ /* -Copyright 2021 Marco Cirillo +Copyright 2021, 2022 Marco Cirillo Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From ef2d3dcacaf500024602874778e3dba0e6761adc Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Thu, 10 Feb 2022 03:00:15 +0100 Subject: [PATCH 07/13] Update test to pre-assert power levels of Mjolnir and User B. Also use getRoomStateEvent to fetch the actual m.room.power_levels state event. --- test/integration/commands/makedminCommandTest.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/integration/commands/makedminCommandTest.ts b/test/integration/commands/makedminCommandTest.ts index b9d203c5..f8990281 100644 --- a/test/integration/commands/makedminCommandTest.ts +++ b/test/integration/commands/makedminCommandTest.ts @@ -19,6 +19,7 @@ describe("Test: The make admin command", function () { const mjolnirUserId = await mjolnir.getUserId(); const moderator = await newTestUser({ name: { contains: "moderator" } }); this.moderator = moderator; + let powerLevels: any; await moderator.joinRoom(config.managementRoom); LogService.debug("makeadminTest", `Joining managementRoom: ${config.managementRoom}`); @@ -28,6 +29,10 @@ describe("Test: The make admin command", function () { LogService.debug("makeadminTest", `Adding targetRoom: ${targetRoom}`); try { await moderator.start(); + powerLevels = await mjolnir.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); + if (powerLevels["users"][mjolnirUserId] !== 0) { + assert.fail(`Bot is already an admin of ${targetRoom}`); + } await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { LogService.debug("makeadminTest", `Sending: !mjolnir make admin ${targetRoom}`); return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir make admin ${targetRoom}` }); @@ -37,7 +42,8 @@ describe("Test: The make admin command", function () { } LogService.debug("makeadminTest", `Making self admin`); - assert.ok(await mjolnir.userHasPowerLevelForAction(mjolnirUserId, targetRoom, PowerLevelAction.Ban), "Bot user is now room admin."); + powerLevels = await mjolnir.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); + assert.equal(powerLevels["users"][mjolnirUserId], 100, "Bot user is not room admin."); }); it('Mjölnir make the tester room administrator', async function () { @@ -50,6 +56,7 @@ describe("Test: The make admin command", function () { this.moderator = moderator; this.userA = userA; this.userB = userB; + let powerLevels: any; await moderator.joinRoom(this.mjolnir.managementRoomId); LogService.debug("makeadminTest", `Joining managementRoom: ${this.mjolnir.managementRoomId}`); @@ -64,6 +71,10 @@ describe("Test: The make admin command", function () { } try { await moderator.start(); + powerLevels = await userA.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); + if (powerLevels["users"][userBId] !== 0) { + assert.fail(`Bot is already an admin of ${targetRoom}`); + } await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { LogService.debug("makeadminTest", `Sending: !mjolnir make admin ${targetRoom} ${userBId}`); return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir make admin ${targetRoom} ${userBId}` }); @@ -73,6 +84,7 @@ describe("Test: The make admin command", function () { } LogService.debug("makeadminTest", `Making User B admin`); - await assert.ok(await userA.userHasPowerLevelForAction(userBId, targetRoom, PowerLevelAction.Ban), "User B is now room admin."); + powerLevels = await userA.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); + assert.equal(powerLevels, 100, "User B is not room admin."); }); }); From 6864d6d43562617c23da6f357a5a4ab93ab81272 Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Thu, 10 Feb 2022 03:22:50 +0100 Subject: [PATCH 08/13] Add additional actors and fix a few tidbits. --- .../commands/makedminCommandTest.ts | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/test/integration/commands/makedminCommandTest.ts b/test/integration/commands/makedminCommandTest.ts index f8990281..3dd3b6b8 100644 --- a/test/integration/commands/makedminCommandTest.ts +++ b/test/integration/commands/makedminCommandTest.ts @@ -11,14 +11,18 @@ describe("Test: The make admin command", function () { this.moderator?.stop(); this.userA?.stop(); this.userB?.stop(); + this.userC?.stop(); }); it('Mjölnir make the bot self room administrator', async function () { - this.timeout(60000); + this.timeout(90000); const mjolnir = config.RUNTIME.client!; const mjolnirUserId = await mjolnir.getUserId(); const moderator = await newTestUser({ name: { contains: "moderator" } }); + const userA = await newTestUser({ name: { contains: "a" } }); + const userAId = await userA.getUserId(); this.moderator = moderator; + this.userA = userA; let powerLevels: any; await moderator.joinRoom(config.managementRoom); @@ -29,6 +33,8 @@ describe("Test: The make admin command", function () { LogService.debug("makeadminTest", `Adding targetRoom: ${targetRoom}`); try { await moderator.start(); + await userA.start(); + await userA.joinRoom(targetRoom); powerLevels = await mjolnir.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); if (powerLevels["users"][mjolnirUserId] !== 0) { assert.fail(`Bot is already an admin of ${targetRoom}`); @@ -39,41 +45,49 @@ describe("Test: The make admin command", function () { }); } finally { await moderator.stop(); + await userA.stop(); } LogService.debug("makeadminTest", `Making self admin`); powerLevels = await mjolnir.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); assert.equal(powerLevels["users"][mjolnirUserId], 100, "Bot user is not room admin."); + assert.equal(powerLevels["users"][userAId], 0, "User A must not be room admin."); }); it('Mjölnir make the tester room administrator', async function () { - this.timeout(60000); + this.timeout(90000); const mjolnir = config.RUNTIME.client!; const moderator = await newTestUser({ name: { contains: "moderator" } }); const userA = await newTestUser({ name: { contains: "a" } }); const userB = await newTestUser({ name: { contains: "b" } }); + const userC = await newTestUser({ name: { contains: "c" } }); const userBId = await userB.getUserId(); + const userCId = await userC.getUserId(); this.moderator = moderator; this.userA = userA; this.userB = userB; + this.userC = userC; let powerLevels: any; await moderator.joinRoom(this.mjolnir.managementRoomId); LogService.debug("makeadminTest", `Joining managementRoom: ${this.mjolnir.managementRoomId}`); - let targetRoom = await userA.createRoom({ invite: [userBId] }); - LogService.debug("makeadminTest", `User A creating targetRoom: ${targetRoom}; and inviting ${userBId}`); + let targetRoom = await userA.createRoom({ invite: [userBId, userCId] }); + LogService.debug("makeadminTest", `User A creating targetRoom: ${targetRoom}; and inviting ${userBId} and ${userCId}`); try { await userB.start(); - userB.joinRoom(targetRoom); + await userC.start(); + await userB.joinRoom(targetRoom); + await userC.joinRoom(targetRoom); } finally { - LogService.debug("makeadminTest", `${userBId} joining targetRoom: ${targetRoom}`); + LogService.debug("makeadminTest", `${userBId} and ${userCId} joining targetRoom: ${targetRoom}`); await userB.stop(); + await userC.stop(); } try { await moderator.start(); powerLevels = await userA.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); if (powerLevels["users"][userBId] !== 0) { - assert.fail(`Bot is already an admin of ${targetRoom}`); + assert.fail(`User B is already an admin of ${targetRoom}`); } await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { LogService.debug("makeadminTest", `Sending: !mjolnir make admin ${targetRoom} ${userBId}`); @@ -85,6 +99,7 @@ describe("Test: The make admin command", function () { LogService.debug("makeadminTest", `Making User B admin`); powerLevels = await userA.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); - assert.equal(powerLevels, 100, "User B is not room admin."); + assert.equal(powerLevels["users"][userBId], 100, "User B is not room admin."); + assert.equal(powerLevels["users"][userCId], 0, "User C must not be room admin."); }); }); From 468ba9275eeffc44a5dba3744f1622d3932f68b5 Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Thu, 10 Feb 2022 08:47:52 +0100 Subject: [PATCH 09/13] Change comparison to check if PL is 100. --- test/integration/commands/makedminCommandTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/commands/makedminCommandTest.ts b/test/integration/commands/makedminCommandTest.ts index 3dd3b6b8..e8e9f48d 100644 --- a/test/integration/commands/makedminCommandTest.ts +++ b/test/integration/commands/makedminCommandTest.ts @@ -36,7 +36,7 @@ describe("Test: The make admin command", function () { await userA.start(); await userA.joinRoom(targetRoom); powerLevels = await mjolnir.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); - if (powerLevels["users"][mjolnirUserId] !== 0) { + if (powerLevels["users"][mjolnirUserId] === 100) { assert.fail(`Bot is already an admin of ${targetRoom}`); } await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { @@ -86,7 +86,7 @@ describe("Test: The make admin command", function () { try { await moderator.start(); powerLevels = await userA.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); - if (powerLevels["users"][userBId] !== 0) { + if (powerLevels["users"][userBId] === 100) { assert.fail(`User B is already an admin of ${targetRoom}`); } await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { From 8161802cddcedfacd28ed39c36d8e8a9e8236e27 Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Thu, 10 Feb 2022 09:40:36 +0100 Subject: [PATCH 10/13] userX shall be either 0 or undefined. --- test/integration/commands/makedminCommandTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/commands/makedminCommandTest.ts b/test/integration/commands/makedminCommandTest.ts index e8e9f48d..df08bb33 100644 --- a/test/integration/commands/makedminCommandTest.ts +++ b/test/integration/commands/makedminCommandTest.ts @@ -51,7 +51,7 @@ describe("Test: The make admin command", function () { powerLevels = await mjolnir.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); assert.equal(powerLevels["users"][mjolnirUserId], 100, "Bot user is not room admin."); - assert.equal(powerLevels["users"][userAId], 0, "User A must not be room admin."); + assert.equal(powerLevels["users"][userAId], (0 || undefined), "User A must not be room admin."); }); it('Mjölnir make the tester room administrator', async function () { @@ -100,6 +100,6 @@ describe("Test: The make admin command", function () { powerLevels = await userA.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); assert.equal(powerLevels["users"][userBId], 100, "User B is not room admin."); - assert.equal(powerLevels["users"][userCId], 0, "User C must not be room admin."); + assert.equal(powerLevels["users"][userCId], (0 || undefined), "User C must not be room admin."); }); }); From 77918f3993c7e6163d8849e7b47ce76c6e9e1062 Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Thu, 10 Feb 2022 15:26:11 +0100 Subject: [PATCH 11/13] Add configuration option to enable and disable the command. Also update tests to call done() in case the command is not enabled in the config. --- config/default.yaml | 8 ++++++++ config/harness.yaml | 8 ++++++++ src/commands/MakeRoomAdminCommand.ts | 7 ++++--- src/config.ts | 3 +++ test/integration/commands/makedminCommandTest.ts | 6 ++++++ 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/config/default.yaml b/config/default.yaml index 34000993..0a52ef42 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -95,6 +95,14 @@ protectedRooms: # Manually add these rooms to the protected rooms list if you want them protected. protectAllJoinedRooms: false +# Server administration commands +admin: + # The make admin command allows you to use the Synapse admin API to force + # a room dministrator of the target room which is local to Mjolnir's HomeServer + # to change the Power Level of either a target user or bot itself to room + # administrator. + enableMakeRoomAdminCommand: true + # Misc options for command handling and commands commands: # If true, Mjolnir will respond to commands like !help and !ban instead of diff --git a/config/harness.yaml b/config/harness.yaml index 434c4b81..0b81a70a 100644 --- a/config/harness.yaml +++ b/config/harness.yaml @@ -96,6 +96,14 @@ protectedRooms: [] # Manually add these rooms to the protected rooms list if you want them protected. protectAllJoinedRooms: false +# Server administration commands +admin: + # The make admin command allows you to use the Synapse admin API to force + # a room dministrator of the target room which is local to Mjolnir's HomeServer + # to change the Power Level of either a target user or bot itself to room + # administrator. + enableMakeRoomAdminCommand: true + # Misc options for command handling and commands commands: # If true, Mjolnir will respond to commands like !help and !ban instead of diff --git a/src/commands/MakeRoomAdminCommand.ts b/src/commands/MakeRoomAdminCommand.ts index 4b921802..601035c7 100644 --- a/src/commands/MakeRoomAdminCommand.ts +++ b/src/commands/MakeRoomAdminCommand.ts @@ -14,18 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ +import config from "../config"; import { Mjolnir } from "../Mjolnir"; import { RichReply } from "matrix-bot-sdk"; // !mjolnir make admin [] export async function execMakeRoomAdminCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { const isAdmin = await mjolnir.isSynapseAdmin(); - if (!isAdmin) { - const message = "I am not a Synapse administrator, or the endpoint is blocked"; + if (!config.admin?.enableMakeRoomAdminCommand || !isAdmin) { + const message = "The command is either not enabled in the config, or I can't perform the requested operation"; const reply = RichReply.createFor(roomId, event, message, message); reply['msgtype'] = "m.notice"; mjolnir.client.sendMessage(roomId, reply); - return; + return; } await mjolnir.makeUserRoomAdmin(await mjolnir.client.resolveRoom(parts[3]), parts[4]); diff --git a/src/config.ts b/src/config.ts index d6eaa9c9..9fc6f317 100644 --- a/src/config.ts +++ b/src/config.ts @@ -48,6 +48,9 @@ interface IConfig { fasterMembershipChecks: boolean; automaticallyRedactForReasons: string[]; // case-insensitive globs protectAllJoinedRooms: boolean; + admin?: { + enableMakeRoomAdminCommand: boolean; + } commands: { allowNoPrefix: boolean; additionalPrefixes: string[]; diff --git a/test/integration/commands/makedminCommandTest.ts b/test/integration/commands/makedminCommandTest.ts index df08bb33..b788ff31 100644 --- a/test/integration/commands/makedminCommandTest.ts +++ b/test/integration/commands/makedminCommandTest.ts @@ -16,6 +16,9 @@ describe("Test: The make admin command", function () { it('Mjölnir make the bot self room administrator', async function () { this.timeout(90000); + if (!config.admin?.enableMakeRoomAdminCommand) { + done(); + } const mjolnir = config.RUNTIME.client!; const mjolnirUserId = await mjolnir.getUserId(); const moderator = await newTestUser({ name: { contains: "moderator" } }); @@ -56,6 +59,9 @@ describe("Test: The make admin command", function () { it('Mjölnir make the tester room administrator', async function () { this.timeout(90000); + if (!config.admin?.enableMakeRoomAdminCommand) { + done(); + } const mjolnir = config.RUNTIME.client!; const moderator = await newTestUser({ name: { contains: "moderator" } }); const userA = await newTestUser({ name: { contains: "a" } }); From d08cf2ff211f696bfa5bc4170fbf7892d42baf3f Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Fri, 11 Feb 2022 12:54:16 +0100 Subject: [PATCH 12/13] Implement some of the reviewed changes. - Textual description/error messages changes - Make the command disabled by default --- config/default.yaml | 14 ++++++++------ config/harness.yaml | 12 +++++++----- src/commands/CommandHandler.ts | 2 +- src/commands/MakeRoomAdminCommand.ts | 2 +- src/config.ts | 2 +- test/integration/commands/makedminCommandTest.ts | 16 ++++++---------- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/config/default.yaml b/config/default.yaml index 0a52ef42..f6ac602e 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -95,13 +95,15 @@ protectedRooms: # Manually add these rooms to the protected rooms list if you want them protected. protectAllJoinedRooms: false -# Server administration commands +# Server administration commands, these commands will only work if Mjolnir is +# a global server administrator admin: - # The make admin command allows you to use the Synapse admin API to force - # a room dministrator of the target room which is local to Mjolnir's HomeServer - # to change the Power Level of either a target user or bot itself to room - # administrator. - enableMakeRoomAdminCommand: true + # The `make admin` upgrades the powerlevel of a specified user (or the bot itself) + # of a room to make them admin of the room (powerlevel 100). + # + # This only works if the room has at least one admin on the local homeserver + # (the homeserver specified in `homeserverUrl` in this file). + enableMakeRoomAdminCommand: false # Misc options for command handling and commands commands: diff --git a/config/harness.yaml b/config/harness.yaml index 0b81a70a..e3c3a9db 100644 --- a/config/harness.yaml +++ b/config/harness.yaml @@ -96,12 +96,14 @@ protectedRooms: [] # Manually add these rooms to the protected rooms list if you want them protected. protectAllJoinedRooms: false -# Server administration commands +# Server administration commands, these commands will only work if Mjolnir is +# a global server administrator admin: - # The make admin command allows you to use the Synapse admin API to force - # a room dministrator of the target room which is local to Mjolnir's HomeServer - # to change the Power Level of either a target user or bot itself to room - # administrator. + # The `make admin` upgrades the powerlevel of a specified user (or the bot itself) + # of a room to make them admin of the room (powerlevel 100). + # + # This only works if the room has at least one admin on the local homeserver + # (the homeserver specified in `homeserverUrl` in this file). enableMakeRoomAdminCommand: true # Misc options for command handling and commands diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index 233bc800..b5cc278a 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -149,7 +149,7 @@ export async function handleCommand(roomId: string, event: { content: { body: st "!mjolnir resolve - Resolves a room alias to a room ID\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 make admin [room alias/ID] - Make the specified user or the bot itself admin of the room\n" + + "!mjolnir make admin [user alias/ID] - Make the specified user or the bot itself admin of the room\n" + "!mjolnir help - This menu\n"; const html = `Mjolnir help:
${htmlEscape(menu)}
`; const text = `Mjolnir help:\n${menu}`; diff --git a/src/commands/MakeRoomAdminCommand.ts b/src/commands/MakeRoomAdminCommand.ts index 601035c7..aed0b543 100644 --- a/src/commands/MakeRoomAdminCommand.ts +++ b/src/commands/MakeRoomAdminCommand.ts @@ -22,7 +22,7 @@ import { RichReply } from "matrix-bot-sdk"; export async function execMakeRoomAdminCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { const isAdmin = await mjolnir.isSynapseAdmin(); if (!config.admin?.enableMakeRoomAdminCommand || !isAdmin) { - const message = "The command is either not enabled in the config, or I can't perform the requested operation"; + const message = "Either the command is disabled or I am not running as homeserver administrator."; const reply = RichReply.createFor(roomId, event, message, message); reply['msgtype'] = "m.notice"; mjolnir.client.sendMessage(roomId, reply); diff --git a/src/config.ts b/src/config.ts index 9fc6f317..9a605605 100644 --- a/src/config.ts +++ b/src/config.ts @@ -49,7 +49,7 @@ interface IConfig { automaticallyRedactForReasons: string[]; // case-insensitive globs protectAllJoinedRooms: boolean; admin?: { - enableMakeRoomAdminCommand: boolean; + enableMakeRoomAdminCommand?: boolean; } commands: { allowNoPrefix: boolean; diff --git a/test/integration/commands/makedminCommandTest.ts b/test/integration/commands/makedminCommandTest.ts index b788ff31..87e11522 100644 --- a/test/integration/commands/makedminCommandTest.ts +++ b/test/integration/commands/makedminCommandTest.ts @@ -39,9 +39,7 @@ describe("Test: The make admin command", function () { await userA.start(); await userA.joinRoom(targetRoom); powerLevels = await mjolnir.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); - if (powerLevels["users"][mjolnirUserId] === 100) { - assert.fail(`Bot is already an admin of ${targetRoom}`); - } + assert.notEqual(powerLevels["users"][mjolnirUserId], 100, `Bot should not yet be an admin of ${targetRoom}`); await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { LogService.debug("makeadminTest", `Sending: !mjolnir make admin ${targetRoom}`); return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir make admin ${targetRoom}` }); @@ -53,8 +51,8 @@ describe("Test: The make admin command", function () { LogService.debug("makeadminTest", `Making self admin`); powerLevels = await mjolnir.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); - assert.equal(powerLevels["users"][mjolnirUserId], 100, "Bot user is not room admin."); - assert.equal(powerLevels["users"][userAId], (0 || undefined), "User A must not be room admin."); + assert.equal(powerLevels["users"][mjolnirUserId], 100, "Bot should be a room admin."); + assert.equal(powerLevels["users"][userAId], (0 || undefined), "User A is not supposed to be a room admin."); }); it('Mjölnir make the tester room administrator', async function () { @@ -92,9 +90,7 @@ describe("Test: The make admin command", function () { try { await moderator.start(); powerLevels = await userA.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); - if (powerLevels["users"][userBId] === 100) { - assert.fail(`User B is already an admin of ${targetRoom}`); - } + assert.notEqual(powerLevels["users"][userBId], 100, `User B should not yet be an admin of ${targetRoom}`); await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { LogService.debug("makeadminTest", `Sending: !mjolnir make admin ${targetRoom} ${userBId}`); return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir make admin ${targetRoom} ${userBId}` }); @@ -105,7 +101,7 @@ describe("Test: The make admin command", function () { LogService.debug("makeadminTest", `Making User B admin`); powerLevels = await userA.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); - assert.equal(powerLevels["users"][userBId], 100, "User B is not room admin."); - assert.equal(powerLevels["users"][userCId], (0 || undefined), "User C must not be room admin."); + assert.equal(powerLevels["users"][userBId], 100, "User B should be a room admin."); + assert.equal(powerLevels["users"][userCId], (0 || undefined), "User C is not supposed to be a room admin."); }); }); From 189c874880f0418b43b56f15590bf9bfe7413b29 Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Fri, 11 Feb 2022 14:31:06 +0100 Subject: [PATCH 13/13] Have the request error extracted and notify the admin room. --- src/Mjolnir.ts | 12 ++++++++---- src/commands/MakeRoomAdminCommand.ts | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index 3a1b8fc5..ebc73b2f 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -971,10 +971,14 @@ export class Mjolnir { * @returns The list of errors encountered, for reporting to the management room. */ public async makeUserRoomAdmin(roomId: string, userId?: string): Promise { - const endpoint = `/_synapse/admin/v1/rooms/${roomId}/make_room_admin`; - return await this.client.doRequest("POST", endpoint, null, { - user_id: userId || await this.client.getUserId(), /* if not specified make the bot administrator */ - }); + try { + const endpoint = `/_synapse/admin/v1/rooms/${roomId}/make_room_admin`; + return await this.client.doRequest("POST", endpoint, null, { + user_id: userId || await this.client.getUserId(), /* if not specified make the bot administrator */ + }); + } catch (e) { + return extractRequestError(e); + } } public queueRedactUserMessagesIn(userId: string, roomId: string) { diff --git a/src/commands/MakeRoomAdminCommand.ts b/src/commands/MakeRoomAdminCommand.ts index aed0b543..5165dd36 100644 --- a/src/commands/MakeRoomAdminCommand.ts +++ b/src/commands/MakeRoomAdminCommand.ts @@ -26,9 +26,18 @@ export async function execMakeRoomAdminCommand(roomId: string, event: any, mjoln const reply = RichReply.createFor(roomId, event, message, message); reply['msgtype'] = "m.notice"; mjolnir.client.sendMessage(roomId, reply); - return; + return; } - await mjolnir.makeUserRoomAdmin(await mjolnir.client.resolveRoom(parts[3]), parts[4]); - await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); + let err = await mjolnir.makeUserRoomAdmin(await mjolnir.client.resolveRoom(parts[3]), parts[4]); + if (err instanceof Error || typeof (err) === "string") { + const errMsg = "Failed to process command:"; + const message = typeof (err) === "string" ? `${errMsg}: ${err}` : `${errMsg}: ${err.message}`; + const reply = RichReply.createFor(roomId, event, message, message); + reply['msgtype'] = "m.notice"; + mjolnir.client.sendMessage(roomId, reply); + return; + } else { + await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); + } }