mirror of
https://github.com/matrix-org/mjolnir.git
synced 2024-10-01 01:36:06 -04:00
standard protection consequences
This commit is contained in:
parent
17dd0aa173
commit
85b15f26f3
@ -36,6 +36,7 @@ import config from "./config";
|
||||
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 { ProtectionSettingValidationError } from "./protections/ProtectionSettings";
|
||||
import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue";
|
||||
import { Healthz } from "./health/healthz";
|
||||
@ -62,6 +63,7 @@ const WATCHED_LISTS_EVENT_TYPE = "org.matrix.mjolnir.watched_lists";
|
||||
const ENABLED_PROTECTIONS_EVENT_TYPE = "org.matrix.mjolnir.enabled_protections";
|
||||
const PROTECTED_ROOMS_EVENT_TYPE = "org.matrix.mjolnir.protected_rooms";
|
||||
const WARN_UNPROTECTED_ROOM_EVENT_PREFIX = "org.matrix.mjolnir.unprotected_room_warning.for.";
|
||||
const CONSEQUENCE_EVENT_DATA = "org.matrix.mjolnir.consequence";
|
||||
|
||||
export class Mjolnir {
|
||||
private displayName: string;
|
||||
@ -836,6 +838,36 @@ export class Mjolnir {
|
||||
await this.printBanlistChanges(changes, banList, true);
|
||||
}
|
||||
|
||||
private async handleConsequence(protection: Protection, roomId: string, event: any, consequence: Consequence) {
|
||||
switch (consequence.type) {
|
||||
case ConsequenceType.alert:
|
||||
break;
|
||||
case ConsequenceType.redact:
|
||||
await this.client.redactEvent(roomId, event["event_id"], "abuse detected");
|
||||
break;
|
||||
case ConsequenceType.ban:
|
||||
await this.client.banUser(event["sender"], roomId, "abuse detected");
|
||||
break;
|
||||
}
|
||||
|
||||
let message = `protection ${protection.name} enacting ${ConsequenceType[consequence.type]} against ${htmlEscape(event["sender"])}`;
|
||||
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)})`;
|
||||
}
|
||||
|
||||
await this.client.sendMessage(this.managementRoomId, {
|
||||
msgtype: "m.notice",
|
||||
body: message,
|
||||
[CONSEQUENCE_EVENT_DATA]: {
|
||||
who: event["sender"],
|
||||
room: roomId,
|
||||
type: ConsequenceType[consequence.type]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleEvent(roomId: string, event: any) {
|
||||
// Check for UISI errors
|
||||
if (roomId === this.managementRoomId) {
|
||||
@ -863,14 +895,20 @@ export class Mjolnir {
|
||||
|
||||
// Iterate all the enabled protections
|
||||
for (const protection of this.enabledProtections) {
|
||||
let consequence: Consequence | undefined = undefined;
|
||||
try {
|
||||
await protection.handleEvent(this, roomId, event);
|
||||
consequence = 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", extractRequestError(e));
|
||||
await this.client.sendNotice(this.managementRoomId, "There was an error processing an event through a protection - see log for details. Event: " + eventPermalink);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (consequence !== undefined) {
|
||||
await this.handleConsequence(protection, roomId, event, consequence);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ import { Protection } from "./IProtection";
|
||||
import { NumberProtectionSetting } from "./ProtectionSettings";
|
||||
import { Mjolnir } from "../Mjolnir";
|
||||
import { LogLevel, LogService } from "matrix-bot-sdk";
|
||||
import { Consequence, ConsequenceType } from "./consequence";
|
||||
import config from "../config";
|
||||
|
||||
// if this is exceeded, we'll ban the user for spam and redact their messages
|
||||
@ -91,5 +92,7 @@ export class BasicFlooding extends Protection {
|
||||
if (forUser.length > this.settings.maxPerMinute.value * 2) {
|
||||
forUser.splice(0, forUser.length - (this.settings.maxPerMinute.value * 2) - 1);
|
||||
}
|
||||
|
||||
return new Consequence(ConsequenceType.ban, "flooding");
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import { Mjolnir } from "../Mjolnir";
|
||||
import { AbstractProtectionSetting } from "./ProtectionSettings";
|
||||
import { Consequence } from "./consequence";
|
||||
|
||||
/**
|
||||
* Represents a protection mechanism of sorts. Protections are intended to be
|
||||
@ -29,21 +30,18 @@ export abstract class Protection {
|
||||
enabled = false;
|
||||
abstract settings: { [setting: string]: AbstractProtectionSetting<any, any> };
|
||||
|
||||
|
||||
/*
|
||||
* Handle a single event from a protected room, to decide if we need to
|
||||
* respond to it
|
||||
*/
|
||||
handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
|
||||
return Promise.resolve(null);
|
||||
async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<Consequence | any> {
|
||||
}
|
||||
|
||||
/*
|
||||
* Handle a single reported event from a protecte room, to decide if we
|
||||
* need to respond to it
|
||||
*/
|
||||
handleReport(mjolnir: Mjolnir, roomId: string, reporterId: string, event: any, reason?: string): Promise<any> {
|
||||
return Promise.resolve(null);
|
||||
async handleReport(mjolnir: Mjolnir, roomId: string, reporterId: string, event: any, reason?: string): Promise<any> {
|
||||
}
|
||||
|
||||
/**
|
||||
|
18
src/protections/consequence.ts
Normal file
18
src/protections/consequence.ts
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
|
||||
export enum ConsequenceType {
|
||||
alert,
|
||||
redact,
|
||||
ban
|
||||
}
|
||||
|
||||
export class Consequence {
|
||||
/*
|
||||
* A description of an action to take when a protection detects abuse
|
||||
*
|
||||
* @param type Action to take
|
||||
* @param reason Brief explanation of why we're taking an action, printed to management room.
|
||||
* this wil be HTML escaped before printing, just in case it has user-provided data
|
||||
*/
|
||||
constructor(public readonly type: ConsequenceType, public readonly reason?: string) {}
|
||||
}
|
111
test/integration/standardConsequenceTest.ts
Normal file
111
test/integration/standardConsequenceTest.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { strict as assert } from "assert";
|
||||
|
||||
import config from "../../src/config";
|
||||
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";
|
||||
|
||||
describe("Test: standard consequences", function() {
|
||||
let badUser;
|
||||
this.beforeEach(async function () {
|
||||
badUser = await newTestUser({ name: { contains: "standard-consequences" }});
|
||||
await badUser.start();
|
||||
})
|
||||
this.afterEach(async function () {
|
||||
await badUser.stop();
|
||||
})
|
||||
it("Mjolnir applies a standard consequence redaction", async function() {
|
||||
this.timeout(20000);
|
||||
|
||||
let protectedRoomId = await this.mjolnir.client.createRoom({ invite: [await badUser.getUserId()] });
|
||||
await badUser.joinRoom(this.mjolnir.managementRoomId);
|
||||
await badUser.joinRoom(protectedRoomId);
|
||||
await this.mjolnir.addProtectedRoom(protectedRoomId);
|
||||
|
||||
await this.mjolnir.registerProtection(new class implements IProtection {
|
||||
name = "JY2TPN";
|
||||
description = "A test protection";
|
||||
settings = { };
|
||||
handleEvent = async (mjolnir: Mjolnir, roomId: string, event: any) => {
|
||||
if (event.content.body === "ngmWkF") {
|
||||
return new Consequence(ConsequenceType.redact, "asd");
|
||||
}
|
||||
};
|
||||
});
|
||||
await this.mjolnir.enableProtection("JY2TPN");
|
||||
|
||||
let reply = new Promise(async (resolve, reject) => {
|
||||
const messageId = await badUser.sendMessage(protectedRoomId, {msgtype: "m.text", body: "ngmWkF"});
|
||||
let redaction;
|
||||
badUser.on('room.event', (roomId, event) => {
|
||||
if (
|
||||
roomId === protectedRoomId
|
||||
&& event?.type === "m.room.redaction"
|
||||
&& event.redacts === messageId
|
||||
) {
|
||||
redaction = event
|
||||
}
|
||||
if (
|
||||
roomId === this.mjolnir.managementRoomId
|
||||
&& event?.type === "m.room.message"
|
||||
&& event?.content?.body?.startsWith("protection JY2TPN enacting redact against ")
|
||||
&& redaction !== undefined
|
||||
) {
|
||||
resolve([redaction, event])
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const [eventRedact, eventMessage] = await reply
|
||||
});
|
||||
it("Mjolnir applies a standard consequence ban", async function() {
|
||||
this.timeout(20000);
|
||||
|
||||
let protectedRoomId = await this.mjolnir.client.createRoom({ invite: [await badUser.getUserId()] });
|
||||
await badUser.joinRoom(this.mjolnir.managementRoomId);
|
||||
await badUser.joinRoom(protectedRoomId);
|
||||
await this.mjolnir.addProtectedRoom(protectedRoomId);
|
||||
|
||||
await this.mjolnir.registerProtection(new class implements IProtection {
|
||||
name = "0LxMTy";
|
||||
description = "A test protection";
|
||||
settings = { };
|
||||
handleEvent = async (mjolnir: Mjolnir, roomId: string, event: any) => {
|
||||
if (event.content.body === "7Uga3d") {
|
||||
return new Consequence(ConsequenceType.ban, "asd");
|
||||
}
|
||||
};
|
||||
});
|
||||
await this.mjolnir.enableProtection("0LxMTy");
|
||||
|
||||
let reply = new Promise(async (resolve, reject) => {
|
||||
const messageId = await badUser.sendMessage(protectedRoomId, {msgtype: "m.text", body: "7Uga3d"});
|
||||
let ban;
|
||||
badUser.on('room.leave', (roomId, event) => {
|
||||
if (
|
||||
roomId === protectedRoomId
|
||||
&& event?.type === "m.room.member"
|
||||
&& event.content?.membership === "ban"
|
||||
&& event.state_key === badUser.userId
|
||||
) {
|
||||
ban = event;
|
||||
}
|
||||
});
|
||||
badUser.on('room.event', (roomId, event) => {
|
||||
if (
|
||||
roomId === this.mjolnir.managementRoomId
|
||||
&& event?.type === "m.room.message"
|
||||
&& event?.content?.body?.startsWith("protection 0LxMTy enacting ban against ")
|
||||
&& ban !== undefined
|
||||
) {
|
||||
resolve([ban, event])
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const [eventBan, eventMessage] = await reply
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user