Add command to elevate a user (or the bot) as room administrator (#219)

This commit is contained in:
Marco Cirillo 2022-03-07 10:14:06 +01:00 committed by GitHub
parent 65af82d46f
commit 97df4d5f61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 197 additions and 0 deletions

3
.gitignore vendored
View File

@ -72,3 +72,6 @@ typings/
# Python packing directories.
mjolnir.egg-info/
# VS
.vs

View File

@ -95,6 +95,16 @@ protectedRooms:
# Manually add these rooms to the protected rooms list if you want them protected.
protectAllJoinedRooms: false
# Server administration commands, these commands will only work if Mjolnir is
# a global server administrator
admin:
# 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:
# If true, Mjolnir will respond to commands like !help and !ban instead of

View File

@ -96,6 +96,16 @@ protectedRooms: []
# Manually add these rooms to the protected rooms list if you want them protected.
protectAllJoinedRooms: false
# Server administration commands, these commands will only work if Mjolnir is
# a global server administrator
admin:
# 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
commands:
# If true, Mjolnir will respond to commands like !help and !ban instead of

View File

@ -1011,6 +1011,23 @@ 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<any> {
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) {
this.eventRedactionQueue.add(new RedactUserInRoom(userId, roomId));
}

View File

@ -37,6 +37,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";
@ -110,6 +111,8 @@ export async function handleCommand(roomId: string, event: { content: { body: st
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 = "" +
@ -148,6 +151,7 @@ export async function handleCommand(roomId: string, event: { content: { body: st
"!mjolnir resolve <room alias> - Resolves a room alias to a room ID\n" +
"!mjolnir shutdown room <room alias/ID> [message] - Uses the bot's account to shut down a room, preventing access to the room on this server\n" +
"!mjolnir powerlevel <user ID> <power level> [room alias/ID] - Sets the power level of the user in the specified room (or all protected rooms)\n" +
"!mjolnir make admin <room alias> [user alias/ID] - Make the specified user or the bot itself admin of the room\n" +
"!mjolnir help - This menu\n";
const html = `<b>Mjolnir help:</b><br><pre><code>${htmlEscape(menu)}</code></pre>`;
const text = `Mjolnir help:\n${menu}`;

View File

@ -0,0 +1,43 @@
/*
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.
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 config from "../config";
import { Mjolnir } from "../Mjolnir";
import { RichReply } from "matrix-bot-sdk";
// !mjolnir make admin <room> [<user ID>]
export async function execMakeRoomAdminCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
const isAdmin = await mjolnir.isSynapseAdmin();
if (!config.admin?.enableMakeRoomAdminCommand || !isAdmin) {
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);
return;
}
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'], '✅');
}
}

View File

@ -48,6 +48,9 @@ interface IConfig {
fasterMembershipChecks: boolean;
automaticallyRedactForReasons: string[]; // case-insensitive globs
protectAllJoinedRooms: boolean;
admin?: {
enableMakeRoomAdminCommand?: boolean;
}
commands: {
allowNoPrefix: boolean;
additionalPrefixes: string[];

View File

@ -0,0 +1,107 @@
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();
this.userC?.stop();
});
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" } });
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);
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 userA.start();
await userA.joinRoom(targetRoom);
powerLevels = await mjolnir.getRoomStateEvent(targetRoom, "m.room.power_levels", "");
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}` });
});
} 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 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 () {
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" } });
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, userCId] });
LogService.debug("makeadminTest", `User A creating targetRoom: ${targetRoom}; and inviting ${userBId} and ${userCId}`);
try {
await userB.start();
await userC.start();
await userB.joinRoom(targetRoom);
await userC.joinRoom(targetRoom);
} finally {
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", "");
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}` });
});
} finally {
await moderator.stop();
}
LogService.debug("makeadminTest", `Making User B admin`);
powerLevels = await userA.getRoomStateEvent(targetRoom, "m.room.power_levels", "");
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.");
});
});