support compound consequences, switch WordList to consequences (#351)

This commit is contained in:
Jess Porter 2022-09-26 16:57:21 +01:00 committed by GitHub
parent 89b7ec1a18
commit f108935d07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 84 additions and 68 deletions

View File

@ -35,7 +35,7 @@ import { applyUserBans } from "./actions/ApplyBan";
import ErrorCache, { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "./ErrorCache";
import { Protection } from "./protections/IProtection";
import { PROTECTIONS } from "./protections/protections";
import { ConsequenceType, Consequence } from "./protections/consequence";
import { Consequence } from "./protections/consequence";
import { ProtectionSettingValidationError } from "./protections/ProtectionSettings";
import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue";
import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue";
@ -959,36 +959,37 @@ export class Mjolnir {
await this.printBanlistChanges(changes, policyList, true);
}
private async handleConsequence(protection: Protection, roomId: string, eventId: string, sender: string, consequence: Consequence) {
switch (consequence.type) {
case ConsequenceType.alert:
break;
case ConsequenceType.redact:
await this.client.redactEvent(roomId, eventId, "abuse detected");
break;
case ConsequenceType.ban:
private async handleConsequences(protection: Protection, roomId: string, eventId: string, sender: string, consequences: Consequence[]) {
for (const consequence of consequences) {
try {
if (consequence.name === "alert") {
/* take no additional action, just print the below message to management room */
} else if (consequence.name === "ban") {
await this.client.banUser(sender, roomId, "abuse detected");
break;
} else if (consequence.name === "redact") {
await this.client.redactEvent(roomId, eventId, "abuse detected");
} else {
throw new Error(`unknown consequence ${consequence.name}`);
}
let message = `protection ${protection.name} enacting ${ConsequenceType[consequence.type]}`
let message = `protection ${protection.name} enacting`
+ ` ${consequence.name}`
+ ` against ${htmlEscape(sender)}`
+ ` in ${htmlEscape(roomId)}`;
if (consequence.reason !== undefined) {
// even though internally-sourced, there's no promise that `consequence.reason`
// will never have user-supplied information, so escape it
message += ` (reason: ${htmlEscape(consequence.reason)})`;
}
+ ` in ${htmlEscape(roomId)}`
+ ` (reason: ${htmlEscape(consequence.reason)})`;
await this.client.sendMessage(this.managementRoomId, {
msgtype: "m.notice",
body: message,
[CONSEQUENCE_EVENT_DATA]: {
who: sender,
room: roomId,
type: ConsequenceType[consequence.type]
types: [consequence.name],
}
});
} catch (e) {
await this.logMessage(LogLevel.ERROR, "handleConsequences", `Failed to enact ${consequence.name} consequence: ${e}`);
}
}
}
private async handleEvent(roomId: string, event: any) {
@ -1018,9 +1019,9 @@ export class Mjolnir {
// Iterate all the enabled protections
for (const protection of this.enabledProtections) {
let consequence: Consequence | undefined = undefined;
let consequences: Consequence[] | undefined = undefined;
try {
consequence = await protection.handleEvent(this, roomId, event);
consequences = await protection.handleEvent(this, roomId, event);
} catch (e) {
const eventPermalink = Permalinks.forEvent(roomId, event['event_id']);
LogService.error("Mjolnir", "Error handling protection: " + protection.name);
@ -1030,8 +1031,8 @@ export class Mjolnir {
continue;
}
if (consequence !== undefined) {
await this.handleConsequence(protection, roomId, event["event_id"], event["sender"], consequence);
if (consequences !== undefined) {
await this.handleConsequences(protection, roomId, event["event_id"], event["sender"], consequences);
}
}

View File

@ -35,7 +35,7 @@ export abstract class Protection {
* Handle a single event from a protected room, to decide if we need to
* respond to it
*/
async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<Consequence | any> {
async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<Consequence[] | any> {
}
/*

View File

@ -15,6 +15,7 @@ limitations under the License.
*/
import { Protection } from "./IProtection";
import { ConsequenceBan, ConsequenceRedact } from "./consequence";
import { Mjolnir } from "../Mjolnir";
import { LogLevel, LogService } from "matrix-bot-sdk";
import { isTrueJoinEvent } from "../utils";
@ -95,21 +96,14 @@ export class WordList extends Protection {
}
}
// Perform the test
if (message && this.badWords!.test(message)) {
await mjolnir.logMessage(LogLevel.WARN, "WordList", `Banning ${event['sender']} for word list violation in ${roomId}.`);
if (!mjolnir.config.noop) {
await mjolnir.client.banUser(event['sender'], roomId, "Word list violation");
} else {
await mjolnir.logMessage(LogLevel.WARN, "WordList", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
if (!message) {
return;
}
// Redact the event
if (!mjolnir.config.noop) {
await mjolnir.client.redactEvent(roomId, event['event_id'], "spam");
} else {
await mjolnir.logMessage(LogLevel.WARN, "WordList", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
}
const matches = message.match(this.badWords!);
if (matches) {
const reason = `bad word: ${matches[0]}`;
return [new ConsequenceBan(reason), new ConsequenceRedact(reason)];
}
}
}

View File

@ -1,23 +1,44 @@
/*
* Distinct individual actions that can be caused as a result of detected abuse
*/
export enum ConsequenceType {
// effectively a no-op. just tell the management room
alert,
// redact the event that triggered this consequence
redact,
// ban the user that sent the event that triggered this consequence
ban
}
export class Consequence {
/*
* Action to take upon detection of abuse and an optional explanation of the detection
* A requested action to take against a user after detected abuse
*
* @param type Action to take
* @param name The name of the consequence being requested
* @param reason Brief explanation of why we're taking an action, printed to management room.
* this will be HTML escaped before printing, just in case it has user-provided data
*/
constructor(public readonly type: ConsequenceType, public readonly reason?: string) {}
constructor(public name: string, public reason: string) { }
}
export class ConsequenceAlert extends Consequence {
/*
* Request an alert to be created after detected abuse
*
* @param reason Brief explanation of why we're taking an action, printed to management room.
* this will be HTML escaped before printing, just in case it has user-provided data
*/
constructor(reason: string) {
super("alert", reason);
}
}
export class ConsequenceRedact extends Consequence {
/*
* Request a message redaction after detected abuse
*
* @param reason Brief explanation of why we're taking an action, printed to management room.
* this will be HTML escaped before printing, just in case it has user-provided data
*/
constructor(reason: string) {
super("redact", reason);
}
}
export class ConsequenceBan extends Consequence {
/*
* Request a ban after detected abuse
*
* @param reason Brief explanation of why we're taking an action, printed to management room.
* this will be HTML escaped before printing, just in case it has user-provided data
*/
constructor(reason: string) {
super("ban", reason);
}
}

View File

@ -4,7 +4,7 @@ import { Mjolnir } from "../../src/Mjolnir";
import { IProtection } from "../../src/protections/IProtection";
import { newTestUser, noticeListener } from "./clientHelper";
import { matrixClient, mjolnir } from "./mjolnirSetupUtils";
import { ConsequenceType, Consequence } from "../../src/protections/consequence";
import { ConsequenceBan, ConsequenceRedact } from "../../src/protections/consequence";
describe("Test: standard consequences", function() {
let badUser;
@ -33,7 +33,7 @@ describe("Test: standard consequences", function() {
settings = { };
handleEvent = async (mjolnir: Mjolnir, roomId: string, event: any) => {
if (event.content.body === "ngmWkF") {
return new Consequence(ConsequenceType.redact, "asd");
return [new ConsequenceRedact("asd")];
}
};
});
@ -77,7 +77,7 @@ describe("Test: standard consequences", function() {
settings = { };
handleEvent = async (mjolnir: Mjolnir, roomId: string, event: any) => {
if (event.content.body === "7Uga3d") {
return new Consequence(ConsequenceType.ban, "asd");
return [new ConsequenceBan("asd")];
}
};
});
@ -124,7 +124,7 @@ describe("Test: standard consequences", function() {
settings = { };
handleEvent = async (mjolnir: Mjolnir, roomId: string, event: any) => {
if (event.content.body === "8HUnwb") {
return new Consequence(ConsequenceType.ban, "asd");
return [new ConsequenceBan("asd")];
}
};
});