Skip to content

Commit

Permalink
Merge branch 'main' into shay/dont_demote
Browse files Browse the repository at this point in the history
  • Loading branch information
H-Shay authored Nov 6, 2024
2 parents 91a947e + dc138be commit a463831
Show file tree
Hide file tree
Showing 10 changed files with 442 additions and 48 deletions.
4 changes: 4 additions & 0 deletions config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ recordIgnoredInvites: false
# (see verboseLogging to adjust this a bit.)
managementRoom: "#moderators:example.org"

# Forward any messages mentioning the bot user to the mangement room. Repeated mentions within
# a 10 minute period are ignored.
forwardMentionsToManagementRoom: false

# Whether Mjolnir should log a lot more messages in the room,
# mainly involves "all-OK" messages, and debugging messages for when mjolnir checks bans in a room.
verboseLogging: true
Expand Down
51 changes: 42 additions & 9 deletions src/Mjolnir.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Copyright 2019-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.
Expand All @@ -14,7 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { extractRequestError, LogLevel, LogService, MembershipEvent } from "@vector-im/matrix-bot-sdk";
import {
extractRequestError,
LogLevel,
LogService,
MembershipEvent,
Permalinks,
UserID,
} from "@vector-im/matrix-bot-sdk";

import { ALL_RULE_TYPES as ALL_BAN_LIST_RULE_TYPES } from "./models/ListRule";
import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler";
Expand All @@ -34,6 +41,7 @@ import { RoomMemberManager } from "./RoomMembers";
import ProtectedRoomsConfig from "./ProtectedRoomsConfig";
import { MatrixEmitter, MatrixSendClient } from "./MatrixEmitter";
import { OpenMetrics } from "./webapis/OpenMetrics";
import { LRUCache } from "lru-cache";
import { ModCache } from "./ModCache";

export const STATE_NOT_STARTED = "not_started";
Expand All @@ -58,7 +66,7 @@ export class Mjolnir {
*/
private unlistedUserRedactionQueue = new UnlistedUserRedactionQueue();

private protectedRoomsConfig: ProtectedRoomsConfig;
public readonly protectedRoomsConfig: ProtectedRoomsConfig;
public readonly protectedRoomsTracker: ProtectedRoomsSet;
private webapis: WebAPIs;
private openMetrics: OpenMetrics;
Expand All @@ -82,6 +90,11 @@ export class Mjolnir {

public readonly policyListManager: PolicyListManager;

public readonly lastBotMentionForRoomId = new LRUCache<string, true>({
ttl: 1000 * 60 * 8, // 8 minutes
ttlAutopurge: true,
});

/**
* Members of the moderator room and others who should not be banned, ACL'd etc.
*/
Expand Down Expand Up @@ -186,9 +199,6 @@ 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;
}

Expand All @@ -210,10 +220,31 @@ export class Mjolnir {

matrixEmitter.on("room.message", async (roomId, event) => {
const eventContent = event.content;
if (roomId !== this.managementRoomId) return;
if (typeof eventContent !== "object") return;

const { msgtype, body: originalBody, sender, event_id } = eventContent;
if (this.config.forwardMentionsToManagementRoom && this.protectedRoomsTracker.isProtectedRoom(roomId)) {
if (eventContent?.["m.mentions"]?.user_ids?.includes(this.clientUserId)) {
LogService.info("Mjolnir", `Bot mentioned ${roomId} by ${event.sender}`);
// Bot mentioned in a public room.
if (this.lastBotMentionForRoomId.has(roomId)) {
// Mentioned too recently, ignore.
return;
}
this.lastBotMentionForRoomId.set(roomId, true);
const permalink = Permalinks.forEvent(roomId, event.event_id, [
new UserID(this.clientUserId).domain,
]);
await this.managementRoomOutput.logMessage(
LogLevel.INFO,
"Mjolnir",
`Bot mentioned ${roomId} by ${event.sender} in ${permalink}`,
roomId,
);
}
}

const { msgtype, body: originalBody, sender } = eventContent;
const eventId = event.event_id;
if (msgtype !== "m.text" || typeof originalBody !== "string") {
return;
}
Expand Down Expand Up @@ -245,7 +276,7 @@ export class Mjolnir {
eventContent.body = COMMAND_PREFIX + restOfBody;
LogService.info("Mjolnir", `Command being run by ${sender}: ${eventContent.body}`);

client.sendReadReceipt(roomId, event_id).catch((e: any) => {
client.sendReadReceipt(roomId, eventId).catch((e: any) => {
LogService.warn("Mjolnir", "Error sending read receipt: ", e);
});
return handleCommand(roomId, event, this);
Expand Down Expand Up @@ -287,6 +318,8 @@ export class Mjolnir {
this.protectionManager = new ProtectionManager(this);

this.managementRoomOutput = new ManagementRoomOutput(managementRoomId, client, config);

this.moderators = new ModCache(client, matrixEmitter, managementRoomId);
this.protectedRoomsTracker = new ProtectedRoomsSet(
client,
clientUserId,
Expand Down
84 changes: 54 additions & 30 deletions src/commands/CommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,35 +146,49 @@ export async function handleCommand(roomId: string, event: { content: { body: st
return await execIgnoreCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === "ignored") {
return await execListIgnoredCommand(roomId, event, mjolnir, parts);
} else {
} else if (parts[1] === "help") {
// Help menu
const menu =
const protectionMenu =
"" +
"!mjolnir - Print status information\n" +
"!mjolnir status - Print status information\n" +
"!mjolnir status protection <protection> [subcommand] - Print status information for a protection\n" +
"!mjolnir ban <list shortcode> <user|room|server> <glob> [reason] - Adds an entity to the ban list\n" +
"!mjolnir unban <list shortcode> <user|room|server> <glob> [apply] - Removes an entity from the ban list. If apply is 'true', the users matching the glob will actually be unbanned\n" +
"!mjolnir redact <user ID> [room alias/ID] [limit] - Redacts messages by the sender in the target room (or all rooms), up to a maximum number of events in the backlog (default 1000)\n" +
"!mjolnir redact <event permalink> - Redacts a message by permalink\n" +
"!mjolnir kick <glob> [room alias/ID] [reason] - Kicks a user or all of those matching a glob in a particular room or all protected rooms\n" +
"!mjolnir rules - Lists the rules currently in use by Mjolnir\n" +
"!mjolnir rules matching <user|room|server> - Lists the rules in use that will match this entity e.g. `!rules matching @foo:example.com` will show all the user and server rules, including globs, that match this user\n" +
"!mjolnir sync - Force updates of all lists and re-apply rules\n" +
"!mjolnir verify - Ensures Mjolnir can moderate all your rooms\n" +
"!mjolnir list create <shortcode> <alias localpart> - Creates a new ban list with the given shortcode and alias\n" +
"!mjolnir watch <room alias/ID> - Watches a ban list\n" +
"!mjolnir unwatch <room alias/ID> - Unwatches a ban list\n" +
"!mjolnir import <room alias/ID> <list shortcode> - Imports bans and ACLs into the given list\n" +
"!mjolnir default <shortcode> - Sets the default list for commands\n" +
"!mjolnir deactivate <user ID> - Deactivates a user ID\n" +
"!mjolnir protections - List all available protections\n" +
"!mjolnir enable <protection> - Enables a particular protection\n" +
"!mjolnir disable <protection> - Disables a particular protection\n" +
"!mjolnir config set <protection>.<setting> [value] - Change a protection setting\n" +
"!mjolnir config add <protection>.<setting> [value] - Add a value to a list protection setting\n" +
"!mjolnir config remove <protection>.<setting> [value] - Remove a value from a list protection setting\n" +
"!mjolnir config get [protection] - List protection settings\n" +
"!mjolnir config get [protection] - List protection settings\n";

const actionMenu =
"" +
"!mjolnir ban <list shortcode> <user|room|server> <glob> [reason] - Adds an entity to the ban list\n" +
"!mjolnir unban <list shortcode> <user|room|server> <glob> [apply] - Removes an entity from the ban list. If apply is 'true', the users matching the glob will be manually unbanned in each protected room.\n" +
"!mjolnir redact <user ID> [room alias/ID] [limit] - Redacts messages by the sender in the target room (or all rooms), up to a maximum number of events in the backlog (default 1000)\n" +
"!mjolnir redact <event permalink> - Redacts a message by permalink\n" +
"!mjolnir kick <glob> [room alias/ID] [reason] - Kicks a user or all of those matching a glob in a particular room or all protected rooms\n" +
"!mjolnir deactivate <user ID> - Deactivates a user ID\n" +
"!mjolnir since <date>/<duration> <action> <limit> [rooms...] [reason] - Apply an action ('kick', 'ban', 'mute', 'unmute' or 'show') to all users who joined a room since <date>/<duration> (up to <limit> users)\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) - 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 <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\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" +
"!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";

const policyListMenu =
"" +
"!mjolnir list create <shortcode> <alias localpart> - Creates a new ban list with the given shortcode and alias\n" +
"!mjolnir watch <room alias/ID> - Watches a ban list\n" +
"!mjolnir unwatch <room alias/ID> - Unwatches a ban list\n" +
"!mjolnir import <room alias/ID> <list shortcode> - Imports bans and ACLs into the given list\n" +
"!mjolnir default <shortcode> - Sets the default list for commands\n" +
"!mjolnir rules - Lists the rules currently in use by Mjolnir\n" +
"!mjolnir rules matching <user|room|server> - Lists the rules in use that will match this entity e.g. `!rules matching @foo:example.com` will show all the user and server rules, including globs, that match this user\n" +
"!mjolnir sync - Force updates of all lists and re-apply rules\n";

const roomsMenu =
"" +
"!mjolnir rooms - Lists all the protected rooms\n" +
"!mjolnir rooms add <room alias/ID> - Adds a protected room (may cause high server load)\n" +
"!mjolnir rooms remove <room alias/ID> - Removes a protected room\n" +
Expand All @@ -185,20 +199,30 @@ export async function handleCommand(roomId: string, event: { content: { body: st
"!mjolnir alias add <room alias> <target room alias/ID> - Adds <room alias> to <target room>\n" +
"!mjolnir alias remove <room alias> - Deletes the room alias from whatever room it is attached to\n" +
"!mjolnir resolve <room alias> - Resolves a room alias to a room ID\n" +
"!mjolnir since <date>/<duration> <action> <limit> [rooms...] [reason] - Apply an action ('kick', 'ban', 'mute', 'unmute' or 'show') to all users who joined a room since <date>/<duration> (up to <limit> users)\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) - 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 <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\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" +
"!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";

const botMenu =
"" +
"!mjolnir - Print status information\n" +
"!mjolnir status - Print status information\n" +
"!mjolnir verify - Ensures Mjolnir can moderate all your rooms\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}`;

const html = `<h3>Mjolnir help menu:</h3><br />
<b>Protection Actions/Options:</b><pre><code>${htmlEscape(protectionMenu)}</code></pre><br />
<b>Moderation Actions:</b><pre><code>${htmlEscape(actionMenu)}</code></pre><br />
<b>Policy List Options/Actions:</b><pre><code>${htmlEscape(policyListMenu)}</code></pre><br />
<b>Room Managment:</b><pre><code>${htmlEscape(roomsMenu)}</code></pre><br />
<b>Bot Status and Management:</b><pre><code>${htmlEscape(botMenu)}</code></pre>`;
const text = `Mjolnir help menu:\n Protection Actions/Options:\n ${protectionMenu} \n Moderation Actions: ${actionMenu}\n Policy List Options/Actions: \n ${policyListMenu} \n Room Management: ${roomsMenu} \n Bot Status and Management: \n ${botMenu} `;
const reply = RichReply.createFor(roomId, event, text, html);
reply["msgtype"] = "m.notice";
return await mjolnir.client.sendMessage(roomId, reply);
} else {
return await mjolnir.client.sendMessage(roomId, {
msgtype: "m.text",
body: "Unknown command - use `!mjolnir help` to display the help menu.",
});
}
} catch (e) {
LogService.error("CommandHandler", extractRequestError(e));
Expand Down
17 changes: 16 additions & 1 deletion src/commands/ShutdownRoomCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ limitations under the License.
*/

import { Mjolnir } from "../Mjolnir";
import { RichReply } from "@vector-im/matrix-bot-sdk";
import { LogLevel, RichReply } from "@vector-im/matrix-bot-sdk";

// !mjolnir shutdown room <room> [<message>]
export async function execShutdownRoomCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
Expand All @@ -31,6 +31,21 @@ export async function execShutdownRoomCommand(roomId: string, event: any, mjolni
return;
}

let protectedRooms;
if (mjolnir.config.protectAllJoinedRooms) {
protectedRooms = await mjolnir.client.getJoinedRooms();
} else {
protectedRooms = mjolnir.protectedRoomsConfig.getExplicitlyProtectedRooms();
}
if (protectedRooms.includes(target)) {
await mjolnir.managementRoomOutput.logMessage(
LogLevel.INFO,
"ShutdownRoomCommand",
"You are attempting to shutdown a room that mjolnir currently protects, aborting.",
);
return;
}

await mjolnir.shutdownSynapseRoom(await mjolnir.client.resolveRoom(target), reason);
await mjolnir.client.unstableApis.addReactionToEvent(roomId, event["event_id"], "✅");
}
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export interface IConfig {
acceptInvitesFromSpace: string;
recordIgnoredInvites: boolean;
managementRoom: string;
forwardMentionsToManagementRoom: boolean;
verboseLogging: boolean;
logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
syncOnStartup: boolean;
Expand Down Expand Up @@ -209,6 +210,7 @@ const defaultConfig: IConfig = {
autojoinOnlyIfManager: true,
recordIgnoredInvites: false,
managementRoom: "!noop:example.org",
forwardMentionsToManagementRoom: false,
verboseLogging: false,
logLevel: "INFO",
syncOnStartup: true,
Expand Down
Loading

0 comments on commit a463831

Please sign in to comment.