WIP: Redact events after all bans have been applied.

This commit is contained in:
gnuxie 2021-09-14 12:17:29 +01:00
parent 60741214f5
commit c5b5026d4d
4 changed files with 124 additions and 6 deletions

View File

@ -36,6 +36,7 @@ import { IProtection } from "./protections/IProtection";
import { PROTECTIONS } from "./protections/protections"; import { PROTECTIONS } from "./protections/protections";
import { AutomaticRedactionQueue } from "./queues/AutomaticRedactionQueue"; import { AutomaticRedactionQueue } from "./queues/AutomaticRedactionQueue";
import { Healthz } from "./health/healthz"; import { Healthz } from "./health/healthz";
import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue";
export const STATE_NOT_STARTED = "not_started"; export const STATE_NOT_STARTED = "not_started";
export const STATE_CHECKING_PERMISSIONS = "checking_permissions"; export const STATE_CHECKING_PERMISSIONS = "checking_permissions";
@ -53,7 +54,8 @@ export class Mjolnir {
private localpart: string; private localpart: string;
private currentState: string = STATE_NOT_STARTED; private currentState: string = STATE_NOT_STARTED;
private protections: IProtection[] = []; private protections: IProtection[] = [];
private redactionQueue = new AutomaticRedactionQueue(); private spamRedactionQueue = new AutomaticRedactionQueue();
private eventRedactionQueue = new EventRedactionQueue();
private automaticRedactionReasons: MatrixGlob[] = []; private automaticRedactionReasons: MatrixGlob[] = [];
private protectedJoinedRoomIds: string[] = []; private protectedJoinedRoomIds: string[] = [];
private explicitlyProtectedRoomIds: string[] = []; private explicitlyProtectedRoomIds: string[] = [];
@ -139,7 +141,7 @@ export class Mjolnir {
} }
public get redactionHandler(): AutomaticRedactionQueue { public get redactionHandler(): AutomaticRedactionQueue {
return this.redactionQueue; return this.spamRedactionQueue;
} }
public get automaticRedactGlobs(): MatrixGlob[] { public get automaticRedactGlobs(): MatrixGlob[] {
@ -493,8 +495,10 @@ export class Mjolnir {
const aclErrors = await applyServerAcls(this.banLists, Object.keys(this.protectedRooms), this); const aclErrors = await applyServerAcls(this.banLists, Object.keys(this.protectedRooms), this);
const banErrors = await applyUserBans(this.banLists, Object.keys(this.protectedRooms), this); const banErrors = await applyUserBans(this.banLists, Object.keys(this.protectedRooms), this);
const redactionErrors = await this.processRedactionQueue();
hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:"); hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:");
hadErrors = hadErrors || await this.printActionResult(banErrors, "Errors updating member bans:"); hadErrors = hadErrors || await this.printActionResult(banErrors, "Errors updating member bans:");
hadErrors = hadErrors || await this.printActionResult(redactionErrors, "Error updating redactions:");
if (!hadErrors && verbose) { if (!hadErrors && verbose) {
const html = `<font color="#00cc00">Done updating rooms - no errors</font>`; const html = `<font color="#00cc00">Done updating rooms - no errors</font>`;
@ -521,8 +525,10 @@ export class Mjolnir {
const aclErrors = await applyServerAcls(this.banLists, Object.keys(this.protectedRooms), this); const aclErrors = await applyServerAcls(this.banLists, Object.keys(this.protectedRooms), this);
const banErrors = await applyUserBans(this.banLists, Object.keys(this.protectedRooms), this); const banErrors = await applyUserBans(this.banLists, Object.keys(this.protectedRooms), this);
const redactionErrors = await this.processRedactionQueue();
hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:"); hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:");
hadErrors = hadErrors || await this.printActionResult(banErrors, "Errors updating member bans:"); hadErrors = hadErrors || await this.printActionResult(banErrors, "Errors updating member bans:");
hadErrors = hadErrors || await this.printActionResult(redactionErrors, "Error updating redactions:");
if (!hadErrors) { if (!hadErrors) {
const html = `<font color="#00cc00"><b>Done updating rooms - no errors</b></font>`; const html = `<font color="#00cc00"><b>Done updating rooms - no errors</b></font>`;
@ -575,7 +581,7 @@ export class Mjolnir {
// Run the event handlers - we always run this after protections so that the protections // Run the event handlers - we always run this after protections so that the protections
// can flag the event for redaction. // can flag the event for redaction.
await this.redactionQueue.handleEvent(roomId, event, this.client); await this.spamRedactionQueue.handleEvent(roomId, event, this.client);
if (event['type'] === 'm.room.power_levels' && event['state_key'] === '') { if (event['type'] === 'm.room.power_levels' && event['state_key'] === '') {
// power levels were updated - recheck permissions // power levels were updated - recheck permissions
@ -590,6 +596,8 @@ export class Mjolnir {
} else if (event['type'] === "m.room.member") { } else if (event['type'] === "m.room.member") {
// Only apply bans in the room we're looking at. // Only apply bans in the room we're looking at.
const errors = await applyUserBans(this.banLists, [roomId], this); const errors = await applyUserBans(this.banLists, [roomId], this);
// do we need room scoped redaction here? yes...
// I want to get an inital review before i check this bit.
await this.printActionResult(errors); await this.printActionResult(errors);
} }
} }
@ -658,4 +666,15 @@ export class Mjolnir {
message: message /* If `undefined`, we'll use Synapse's default message. */ message: message /* If `undefined`, we'll use Synapse's default message. */
}); });
} }
// This naming is horrible and clashes with the other redaction queue which isn't
// really the same thing. The old one is more about an ongoing user who we haven't
// banned, whereas this one is about redaction of users who aren't active.
public queueRedactUserMessagesIn(userId: string, roomId: string) {
this.eventRedactionQueue.add(new RedactUserInRoom(userId, roomId));
}
public async processRedactionQueue() {
return await this.eventRedactionQueue.process(this.client);
}
} }

View File

@ -21,7 +21,6 @@ import config from "../config";
import { logMessage } from "../LogProxy"; import { logMessage } from "../LogProxy";
import { LogLevel } from "matrix-bot-sdk"; import { LogLevel } from "matrix-bot-sdk";
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache"; import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
import { redactUserMessagesIn } from "../utils";
/** /**
* Applies the member bans represented by the ban lists to the provided rooms, returning the * Applies the member bans represented by the ban lists to the provided rooms, returning the
@ -69,7 +68,7 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir
if (!config.noop) { if (!config.noop) {
await mjolnir.client.banUser(member.userId, roomId, userRule.reason); await mjolnir.client.banUser(member.userId, roomId, userRule.reason);
if (mjolnir.automaticRedactGlobs.find(g => g.test(userRule.reason.toLowerCase()))) { if (mjolnir.automaticRedactGlobs.find(g => g.test(userRule.reason.toLowerCase()))) {
await redactUserMessagesIn(mjolnir.client, member.userId, [roomId]); mjolnir.queueRedactUserMessagesIn(member.userId, roomId);
} }
} else { } else {
await logMessage(LogLevel.WARN, "ApplyBan", `Tried to ban ${member.userId} in ${roomId} but Mjolnir is running in no-op mode`, roomId); await logMessage(LogLevel.WARN, "ApplyBan", `Tried to ban ${member.userId} in ${roomId} but Mjolnir is running in no-op mode`, roomId);

View File

@ -13,7 +13,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
//// NOTE: This is a queue of users whose events should be redacted
////////// Not a queue of events to be redacted.
////////// This is also unrelated to the AutomaticRedactionReasons.
////////// It is as of writing only used by the flood/spam protections.
import { extractRequestError, LogLevel, LogService, MatrixClient, Permalinks } from "matrix-bot-sdk"; import { extractRequestError, LogLevel, LogService, MatrixClient, Permalinks } from "matrix-bot-sdk";
import { logMessage } from "../LogProxy"; import { logMessage } from "../LogProxy";
import config from "../config"; import config from "../config";

View File

@ -0,0 +1,97 @@
/*
Copyright 2019-2021 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.
*/
//// NOTE: This is a queue for events so that other protections can happen first (bans and ACL)
import { LogLevel, MatrixClient } from "matrix-bot-sdk"
import { ERROR_KIND_FATAL } from "../ErrorCache";
import { logMessage } from "../LogProxy";
import { RoomUpdateError } from "../models/RoomUpdateError";
import { redactUserMessagesIn } from "../utils";
export interface QueuedRedaction {
redact(client: MatrixClient): Promise<any>
redactionEqual(redaction: QueuedRedaction): boolean
report(e): RoomUpdateError
}
export class RedactUserInRoom implements QueuedRedaction {
userId: string;
roomId: string;
constructor(userId: string, roomId: string) {
this.userId = userId;
this.roomId = roomId;
}
public async redact(client: MatrixClient) {
await logMessage(LogLevel.DEBUG, "Mjolnir", `Redacting events from ${this.userId} in room ${this.roomId}.`);
await redactUserMessagesIn(client, this.userId, [this.roomId]);
}
public redactionEqual(redaction: QueuedRedaction): boolean {
if (redaction instanceof RedactUserInRoom) {
return redaction.userId === this.userId && redaction.roomId === this.roomId;
} else {
return false;
}
}
public report(e): RoomUpdateError {
const message = e.message || (e.body ? e.body.error : '<no message>');
return {
roomId: this.roomId,
errorMessage: message,
errorKind: ERROR_KIND_FATAL,
};
}
}
export class EventRedactionQueue {
private toRedact: Array<QueuedRedaction> = new Array<QueuedRedaction>();
public has(redaction: QueuedRedaction) {
return this.toRedact.find(r => r.redactionEqual(redaction));
}
public add(redaction: QueuedRedaction) {
if (this.has(redaction)) {
return;
} else {
this.toRedact.push(redaction);
}
}
public delete(redaction: QueuedRedaction) {
this.toRedact = this.toRedact.filter(r => r.redactionEqual(redaction));
}
public async process(client: MatrixClient): Promise<RoomUpdateError[]> {
const errors: RoomUpdateError[]= [];
// need to change this so it pops the array until empty
// otherwise this will be cringe.
for (const redaction of this.toRedact) {
try {
await redaction.redact(client);
} catch (e) {
errors.push(redaction.report(e));
} finally {
// FIXME: Need to figure out in which circumstances we want to retry.
this.delete(redaction);
}
}
return errors;
}
}