mjolnir/src/protections/BasicFlooding.ts

92 lines
3.9 KiB
TypeScript
Raw Normal View History

2019-12-05 02:07:04 +00:00
/*
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
2019-12-05 02:07:04 +00:00
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.
*/
import { IProtection } from "./IProtection";
import { Mjolnir } from "../Mjolnir";
import { LogLevel, LogService } from "matrix-bot-sdk";
import { logMessage } from "../LogProxy";
2019-12-10 02:20:47 +00:00
import config from "../config";
2019-12-05 02:07:04 +00:00
export const MAX_PER_MINUTE = 10; // if this is exceeded, we'll ban the user for spam and redact their messages
const TIMESTAMP_THRESHOLD = 30000; // 30s out of phase
export class BasicFlooding implements IProtection {
private lastEvents: { [roomId: string]: { [userId: string]: { originServerTs: number, eventId: string }[] } } = {};
private recentlyBanned: string[] = [];
2019-12-05 02:07:04 +00:00
constructor() {
}
public get name(): string {
return 'BasicFloodingProtection';
}
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
if (!this.lastEvents[roomId]) this.lastEvents[roomId] = {};
const forRoom = this.lastEvents[roomId];
if (!forRoom[event['sender']]) forRoom[event['sender']] = [];
let forUser = forRoom[event['sender']];
if ((new Date()).getTime() - event['origin_server_ts'] > TIMESTAMP_THRESHOLD) {
LogService.warn("BasicFlooding", `${event['event_id']} is more than ${TIMESTAMP_THRESHOLD}ms out of phase - rewriting event time to be 'now'`);
event['origin_server_ts'] = (new Date()).getTime();
}
forUser.push({originServerTs: event['origin_server_ts'], eventId: event['event_id']});
// Do some math to see if the user is spamming
let messageCount = 0;
for (const prevEvent of forUser) {
if ((new Date()).getTime() - prevEvent.originServerTs > 60000) continue; // not important
messageCount++;
}
if (messageCount >= MAX_PER_MINUTE) {
// 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
2019-12-05 02:07:04 +00:00
// Redact all the things the user said too
2019-12-10 02:20:47 +00:00
if (!config.noop) {
for (const eventId of forUser.map(e => e.eventId)) {
await mjolnir.client.redactEvent(roomId, eventId, "spam");
}
} else {
await logMessage(LogLevel.WARN, "BasicFlooding", `Tried to redact messages for ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
2019-12-05 02:07:04 +00:00
}
await logMessage(LogLevel.WARN, "BasicFlooding", `Banning ${event['sender']} in ${roomId} for flooding (${messageCount} messages in the last minute)`, roomId);
2019-12-10 02:20:47 +00:00
if (!config.noop) {
await mjolnir.client.banUser(event['sender'], roomId, "spam");
} else {
await logMessage(LogLevel.WARN, "BasicFlooding", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
2019-12-10 02:20:47 +00:00
}
// Free up some memory now that we're ready to handle it elsewhere
2019-12-05 02:07:04 +00:00
forUser = forRoom[event['sender']] = []; // reset the user's list
}
// Trim the oldest messages off the user's history if it's getting large
2019-12-05 02:28:49 +00:00
if (forUser.length > MAX_PER_MINUTE * 2) {
forUser.splice(0, forUser.length - (MAX_PER_MINUTE * 2) - 1);
2019-12-05 02:07:04 +00:00
}
}
}