Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't ban users in moderator room #544

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
23 changes: 23 additions & 0 deletions src/Mjolnir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,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: string[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users who are invited/joined after the bot starts up would get banned. Adding a cache would help ensure we don't miss anyone. We'd invalidate the cache on two conditions I think:

  1. Every 60 minutes.
  2. Whenever we see an m.room.member event in the management room. (we don't have to inspect the event: seeing it fly by should be enough to invalidate&repopulate)

For deployments like matrix.org this may mean we pick up on displayname changes quite often, but I'd consider that a feature for keeping the cache active.


/**
* Adds a listener to the client that will automatically accept invitations.
* @param {MatrixSendClient} client
Expand Down Expand Up @@ -180,6 +185,23 @@ export class Mjolnir {
"Mjolnir is starting up. Use !mjolnir to query status.",
);
Mjolnir.addJoinOnInviteListener(mjolnir, client, config);

const memberEvents = await mjolnir.client.getRoomMembers(
managementRoomId,
undefined,
["join"],
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
["ban", "leave"],
);
let moderators: string[] = [];
memberEvents.forEach((event) => {
moderators.push(event.stateKey);
const server = event.stateKey.split(":")[1];
if (!moderators.includes(server)) {
moderators.push(server);
}
});
mjolnir.moderators = moderators;

return mjolnir;
}

Expand Down Expand Up @@ -279,6 +301,7 @@ export class Mjolnir {
this.managementRoomOutput,
this.protectionManager,
config,
this.moderators,
);
}

Expand Down
10 changes: 10 additions & 0 deletions src/ProtectedRoomsSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export class ProtectedRoomsSet {
private readonly managementRoomOutput: ManagementRoomOutput,
private readonly protectionManager: ProtectionManager,
private readonly config: IConfig,
private readonly moderators: string[],
) {
for (const reason of this.config.automaticallyRedactForReasons) {
this.automaticRedactionReasons.push(new MatrixGlob(reason.toLowerCase()));
Expand Down Expand Up @@ -441,6 +442,15 @@ export class ProtectedRoomsSet {
);

if (!this.config.noop) {
if (this.moderators.includes(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);
Expand Down
11 changes: 9 additions & 2 deletions src/commands/CommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -184,8 +189,10 @@ export async function handleCommand(roomId: string, event: { content: { body: st
"!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 suspend <user ID> - Suspend the specified user" +
"!mjolnir unsuspend <user ID> - Unsuspend the specified user" +
"!mjolnir suspend <user ID> - Suspend the specified user\n" +
"!mjolnir unsuspend <user ID> - Unsuspend the specified user\n" +
"!mjolnir ignore <user ID/server name> - 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" +
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
"!mjolnir help - This menu\n";
const html = `<b>Mjolnir help:</b><br><pre><code>${htmlEscape(menu)}</code></pre>`;
const text = `Mjolnir help:\n${menu}`;
Expand Down
49 changes: 49 additions & 0 deletions src/commands/IgnoreCommand.ts
Original file line number Diff line number Diff line change
@@ -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 <user|server>
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.push(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:<ul>";
let text = "Ignored users:\n";

for (const name of mjolnir.moderators) {
html += `<li>${name}</li>`;
text += `* ${name}\n`;
}

html += "</ul>";

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"], "✅");
}
8 changes: 8 additions & 0 deletions src/commands/SinceCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ async function execSinceCommandAux(
case Action.Ban: {
for (let join of recentJoins) {
try {
if (mjolnir.moderators.includes(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) {
Expand Down
12 changes: 12 additions & 0 deletions src/commands/UnbanBanCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,18 @@ 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);
mjolnir.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"], "✅");
}
Expand Down
8 changes: 8 additions & 0 deletions src/protections/BasicFlooding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ export class BasicFlooding extends Protection {
roomId,
);
if (!mjolnir.config.noop) {
if (mjolnir.moderators.includes(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(
Expand Down
8 changes: 8 additions & 0 deletions src/protections/FirstMessageIsImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.includes(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(
Expand Down
8 changes: 8 additions & 0 deletions src/protections/ProtectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.includes(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");
Expand Down
10 changes: 10 additions & 0 deletions src/protections/TrustedReporters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.includes(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");
}
Expand Down
10 changes: 9 additions & 1 deletion src/report/ReportManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<string | undefined> {
if (manager.mjolnir.moderators.includes(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;
}
Expand Down
Loading