Merge pull request #56 from Alch-Emi/wordlist

Add a WordList protection
This commit is contained in:
Travis Ralston 2020-11-04 12:19:40 -07:00 committed by GitHub
commit e4e5c5e72d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 143 additions and 0 deletions

View File

@ -107,6 +107,25 @@ commands:
additionalPrefixes:
- "mjolnir_bot"
# Configuration specific to certain toggleable protections
protections:
# Configuration for the wordlist plugin, which can ban users based if they say certain
# blocked words shortly after joining.
wordlist:
# A list of words which should be monitored by the bot. These will match if any part
# of the word is present in the message in any case. e.g. "hello" also matches
# "HEllO". Additionally, regular expressions can be used.
words:
- "CaSe"
- "InSeNsAtIve"
- "WoRd"
- "LiSt"
# How long after a user joins the server should the bot monitor their messages. After
# this time, users can say words from the wordlist without being banned automatically.
# Set to zero to disable (users will always be banned if they say a bad word)
minutesBeforeTrusting: 20
# Options for monitoring the health of the bot
health:
# healthz options. These options are best for use in container environments

View File

@ -43,6 +43,12 @@ interface IConfig {
allowNoPrefix: boolean;
additionalPrefixes: string[];
};
protections: {
wordlist: {
words: string[];
minutesBeforeTrusting: number;
};
};
health: {
healthz: {
enabled: boolean;
@ -89,6 +95,12 @@ const defaultConfig: IConfig = {
allowNoPrefix: false,
additionalPrefixes: [],
},
protections: {
wordlist: {
words: [],
minutesBeforeTrusting: 20
}
},
health: {
healthz: {
enabled: false,

106
src/protections/WordList.ts Normal file
View File

@ -0,0 +1,106 @@
/*
Copyright 2020 Emi Tatsuo Simpson et al.
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";
import config from "../config";
import { isTrueJoinEvent } from "../utils";
export class WordList implements IProtection {
private justJoined: { [roomId: string]: { [username: string]: Date} } = {};
private badWords: RegExp;
constructor() {
// Create a mega-regex from all the tiny baby regexs
this.badWords = new RegExp(
"(" + config.protections.wordlist.words.join(")|(") + ")",
"i"
)
}
public get name(): string {
return 'WordList';
}
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
const content = event['content'] || {};
const minsBeforeTrusting = config.protections.wordlist.minutesBeforeTrusting;
if (minsBeforeTrusting > 0) {
if (!this.justJoined[roomId]) this.justJoined[roomId] = {};
// When a new member logs in, store the time they joined. This will be useful
// when we need to check if a message was sent within 20 minutes of joining
if (event['type'] === 'm.room.member') {
if (isTrueJoinEvent(event)) {
const now = new Date();
this.justJoined[roomId][event['state_key']] = now;
LogService.info("WordList", `${event['state_key']} joined ${roomId} at ${now.toDateString()}`);
} else if (content['membership'] === 'leave' || content['membership'] === 'ban') {
delete this.justJoined[roomId][event['sender']]
}
return;
}
}
if (event['type'] === 'm.room.message') {
const message = content['formatted_body'] || content['body'] || null;
// Check conditions first
if (minsBeforeTrusting > 0) {
const joinTime = this.justJoined[roomId][event['sender']]
if (joinTime) { // Disregard if the user isn't recently joined
// Check if they did join recently, was it within the timeframe
const now = new Date();
if (now.valueOf() - joinTime.valueOf() > minsBeforeTrusting * 60 * 1000) {
delete this.justJoined[roomId][event['sender']] // Remove the user
LogService.info("WordList", `${event['sender']} is no longer considered suspect`);
return
}
} else {
// The user isn't in the recently joined users list, no need to keep
// looking
return
}
}
// Perform the test
if (message && this.badWords.test(message)) {
await logMessage(LogLevel.WARN, "WordList", `Banning ${event['sender']} for word list violation in ${roomId}.`);
if (!config.noop) {
await mjolnir.client.banUser(event['sender'], roomId, "Word list violation");
} else {
await logMessage(LogLevel.WARN, "WordList", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
}
// Redact the event
if (!config.noop) {
await mjolnir.client.redactEvent(roomId, event['event_id'], "spam");
} else {
await logMessage(LogLevel.WARN, "WordList", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
}
}
}
}
}

View File

@ -17,6 +17,7 @@ limitations under the License.
import { FirstMessageIsImage } from "./FirstMessageIsImage";
import { IProtection } from "./IProtection";
import { BasicFlooding, MAX_PER_MINUTE } from "./BasicFlooding";
import { WordList } from "./WordList";
export const PROTECTIONS: PossibleProtections = {
[new FirstMessageIsImage().name]: {
@ -28,6 +29,11 @@ export const PROTECTIONS: PossibleProtections = {
description: "If a user posts more than " + MAX_PER_MINUTE + " messages in 60s they'll be " +
"banned for spam. This does not publish the ban to any of your ban lists.",
factory: () => new BasicFlooding(),
},
[new WordList().name]: {
description: "If a user posts a monitored word a set amount of time after joining, they " +
"will be banned from that room. This will not publish the ban to a ban list.",
factory: () => new WordList(),
}
};