From f9e3c33935566b2346c872c63d1a26405e8f7461 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 9 Dec 2019 19:15:51 -0700 Subject: [PATCH] Don't spam protection warnings, and ensure the user is redacted We now always prioritize redaction over ban to ensure that the user gets removed from our rooms. This also means that the second image posted by a spammer is redacted after join. This commit also improves the messaging a bit. --- src/Mjolnir.ts | 14 +++++++- src/protections/BasicFlooding.ts | 16 +++++++-- src/protections/FirstMessageIsImage.ts | 15 +++++++-- src/protections/IProtection.ts | 4 ++- src/queues/AutomaticRedactionQueue.ts | 46 ++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 src/queues/AutomaticRedactionQueue.ts diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index ede9013..7ba2b5d 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -25,6 +25,7 @@ import { logMessage } from "./LogProxy"; import ErrorCache, { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "./ErrorCache"; import { IProtection } from "./protections/IProtection"; import { PROTECTIONS } from "./protections/protections"; +import { AutomaticRedactionQueue } from "./queues/AutomaticRedactionQueue"; export const STATE_NOT_STARTED = "not_started"; export const STATE_CHECKING_PERMISSIONS = "checking_permissions"; @@ -40,6 +41,7 @@ export class Mjolnir { private localpart: string; private currentState: string = STATE_NOT_STARTED; private protections: IProtection[] = []; + private redactionQueue = new AutomaticRedactionQueue(); constructor( public readonly client: MatrixClient, @@ -89,6 +91,10 @@ export class Mjolnir { return this.protections; } + public get redactionHandler(): AutomaticRedactionQueue { + return this.redactionQueue; + } + public start() { return this.client.start().then(async () => { this.currentState = STATE_CHECKING_PERMISSIONS; @@ -395,12 +401,18 @@ export class Mjolnir { try { await protection.handleEvent(this, roomId, event); } catch (e) { + const eventPermalink = Permalinks.forEvent(roomId, event['event_id']); LogService.error("Mjolnir", "Error handling protection: " + protection.name); + LogService.error("Mjolnir", "Failed event: " + eventPermalink); LogService.error("Mjolnir", e); - await this.client.sendNotice(config.managementRoom, "There was an error processing an event through a protection - see log for details."); + await this.client.sendNotice(config.managementRoom, "There was an error processing an event through a protection - see log for details. Event: " + eventPermalink); } } + // Run the event handlers - we always run this after protections so that the protections + // can flag the event for redaction. + await this.redactionQueue.handleEvent(roomId, event, this.client); + if (event['type'] === 'm.room.power_levels' && event['state_key'] === '') { // power levels were updated - recheck permissions ErrorCache.resetError(roomId, ERROR_KIND_PERMISSION); diff --git a/src/protections/BasicFlooding.ts b/src/protections/BasicFlooding.ts index 7014014..873bc38 100644 --- a/src/protections/BasicFlooding.ts +++ b/src/protections/BasicFlooding.ts @@ -24,7 +24,8 @@ const TIMESTAMP_THRESHOLD = 30000; // 30s out of phase export class BasicFlooding implements IProtection { - public lastEvents: { [roomId: string]: { [userId: string]: { originServerTs: number, eventId: string }[] } } = {}; + private lastEvents: { [roomId: string]: { [userId: string]: { originServerTs: number, eventId: string }[] } } = {}; + private recentlyBanned: string[] = []; constructor() { } @@ -55,12 +56,21 @@ export class BasicFlooding implements IProtection { } if (messageCount >= MAX_PER_MINUTE) { - await logMessage(LogLevel.WARN, "BasicFlooding", `Banning ${event['sender']} in ${roomId} for flooding (${messageCount} messages in the last minute)`); - await mjolnir.client.banUser(event['sender'], roomId, "spam"); + // Prioritize redaction over ban - we can always keep redacting what the user said. + + if (this.recentlyBanned.includes(event['sender'])) return; // already handled (will be redacted) + mjolnir.redactionHandler.addUser(event['sender']); + this.recentlyBanned.push(event['sender']); // flag to reduce spam + // Redact all the things the user said too for (const eventId of forUser.map(e => e.eventId)) { await mjolnir.client.redactEvent(roomId, eventId, "spam"); } + + await logMessage(LogLevel.WARN, "BasicFlooding", `Banning ${event['sender']} in ${roomId} for flooding (${messageCount} messages in the last minute)`); + await mjolnir.client.banUser(event['sender'], roomId, "spam"); + + // Free up some memory now that we're ready to handle it elsewhere forUser = forRoom[event['sender']] = []; // reset the user's list } diff --git a/src/protections/FirstMessageIsImage.ts b/src/protections/FirstMessageIsImage.ts index 857fdff..7d5ed13 100644 --- a/src/protections/FirstMessageIsImage.ts +++ b/src/protections/FirstMessageIsImage.ts @@ -21,7 +21,8 @@ import { logMessage } from "../LogProxy"; export class FirstMessageIsImage implements IProtection { - public justJoined: { [roomId: string]: string[] } = {}; + private justJoined: { [roomId: string]: string[] } = {}; + private recentlyBanned: string[] = []; constructor() { } @@ -55,9 +56,17 @@ export class FirstMessageIsImage implements IProtection { const formattedBody = content['formatted_body'] || ''; const isMedia = msgtype === 'm.image' || msgtype === 'm.video' || formattedBody.toLowerCase().includes(' = new Set(); + + constructor() { + } + + public addUser(userId: string) { + this.usersToRedact.add(userId); + } + + public isUserQueued(userId: string): boolean { + return this.usersToRedact.has(userId); + } + + public async handleEvent(roomId: string, event: any, mjolnirClient: MatrixClient) { + if (this.isUserQueued(event['sender'])) { + const permalink = Permalinks.forEvent(roomId, event['event_id']); + try { + LogService.info("AutomaticRedactionQueue", `Redacting event because the user is listed as bad: ${permalink}`) + await mjolnirClient.redactEvent(roomId, event['event_id']); + } catch (e) { + logMessage(LogLevel.WARN, "AutomaticRedactionQueue", `Unable to redact message: ${permalink}`); + LogService.warn("AutomaticRedactionQueue", e); + } + } + } +}