refactor (and simplify) how protections are stored in-memory

This commit is contained in:
jesopo 2022-02-02 15:31:11 +00:00
parent 8b23f93b71
commit ad3917c8b2
10 changed files with 201 additions and 166 deletions

View File

@ -59,7 +59,7 @@ export class Mjolnir {
private displayName: string; private displayName: string;
private localpart: string; private localpart: string;
private currentState: string = STATE_NOT_STARTED; private currentState: string = STATE_NOT_STARTED;
private protections: IProtection[] = []; public protections = new Map<string /* protection name */, IProtection>();
/** /**
* This is for users who are not listed on a watchlist, * This is for users who are not listed on a watchlist,
* but have been flagged by the automatic spam detection as suispicous * but have been flagged by the automatic spam detection as suispicous
@ -237,7 +237,7 @@ export class Mjolnir {
} }
public get enabledProtections(): IProtection[] { public get enabledProtections(): IProtection[] {
return this.protections; return [...this.protections.values()].filter(p => p.enabled);
} }
/** /**
@ -293,7 +293,7 @@ export class Mjolnir {
if (config.syncOnStartup) { if (config.syncOnStartup) {
await logMessage(LogLevel.INFO, "Mjolnir@startup", "Syncing lists..."); await logMessage(LogLevel.INFO, "Mjolnir@startup", "Syncing lists...");
await this.syncLists(config.verboseLogging); await this.syncLists(config.verboseLogging);
await this.enableProtections(); await this.registerProtections();
} }
this.currentState = STATE_RUNNING; this.currentState = STATE_RUNNING;
@ -375,32 +375,52 @@ export class Mjolnir {
} }
} }
private async getEnabledProtections() { /*
let enabled: string[] = []; * Take all the builtin protections, register them to set their enabled (or not) state and
try { * update their settings with any saved non-default values
const protections: { enabled: string[] } | null = await this.client.getAccountData(ENABLED_PROTECTIONS_EVENT_TYPE); */
if (protections && protections['enabled']) { private async registerProtections() {
for (const protection of protections['enabled']) { for (const protection of PROTECTIONS) {
enabled.push(protection);
}
}
} catch (e) {
LogService.warn("Mjolnir", extractRequestError(e));
}
return enabled;
}
private async enableProtections() {
for (const protection of await this.getEnabledProtections()) {
try { try {
this.enableProtection(protection, false); await this.registerProtection(protection);
} catch (e) { } catch (e) {
LogService.warn("Mjolnir", extractRequestError(e)); LogService.warn("Mjolnir", extractRequestError(e));
} }
} }
} }
/*
* Make a list of the names of enabled protections and save them in a state event
*/
private async saveEnabledProtections() {
const protections = this.enabledProtections.map(p => p.name);
await this.client.setAccountData(ENABLED_PROTECTIONS_EVENT_TYPE, { enabled: protections });
}
/*
* Enable a protection by name and persist its enable state in to a state event
*
* @param name The name of the protection whose settings we're enabling
*/
public async enableProtection(name: string) {
const protection = this.protections.get(name);
if (protection !== undefined) {
protection.enabled = true;
await this.saveEnabledProtections();
}
}
/*
* Disable a protection by name and remove it from the persistent list of enabled protections
*
* @param name The name of the protection whose settings we're disabling
*/
public async disableProtection(name: string) {
const protection = this.protections.get(name);
if (protection !== undefined) {
protection.enabled = false;
await this.saveEnabledProtections();
}
}
/* /*
* Read org.matrix.mjolnir.setting state event, find any saved settings for * Read org.matrix.mjolnir.setting state event, find any saved settings for
* the requested protectionName, then iterate and validate against their parser * the requested protectionName, then iterate and validate against their parser
@ -417,10 +437,10 @@ export class Mjolnir {
); );
} catch { } catch {
// setting does not exist, return empty object // setting does not exist, return empty object
return savedSettings; return {};
} }
const settingDefinitions = PROTECTIONS[protectionName].factory().settings; const settingDefinitions = this.protections.get(protectionName)?.settings ?? {};
const validatedSettings: { [setting: string]: any } = {} const validatedSettings: { [setting: string]: any } = {}
for (let [key, value] of Object.entries(savedSettings)) { for (let [key, value] of Object.entries(savedSettings)) {
if ( if (
@ -452,17 +472,21 @@ export class Mjolnir {
* @param changedSettings The settings to change and their values * @param changedSettings The settings to change and their values
*/ */
public async setProtectionSettings(protectionName: string, changedSettings: { [setting: string]: any }): Promise<any> { public async setProtectionSettings(protectionName: string, changedSettings: { [setting: string]: any }): Promise<any> {
const settingDefinitions = PROTECTIONS[protectionName].factory().settings; const protection = this.protections.get(protectionName);
if (protection === undefined) {
return;
}
const validatedSettings: { [setting: string]: any } = await this.getProtectionSettings(protectionName); const validatedSettings: { [setting: string]: any } = await this.getProtectionSettings(protectionName);
for (let [key, value] of Object.entries(changedSettings)) { for (let [key, value] of Object.entries(changedSettings)) {
if (!(key in settingDefinitions)) { if (!(key in protection.settings)) {
throw new ProtectionSettingValidationError(`Failed to find protection setting by name: ${key}`); throw new ProtectionSettingValidationError(`Failed to find protection setting by name: ${key}`);
} }
if (typeof(settingDefinitions[key].value) !== typeof(value)) { if (typeof(protection.settings[key].value) !== typeof(value)) {
throw new ProtectionSettingValidationError(`Invalid type for protection setting: ${key} (${typeof(value)})`); throw new ProtectionSettingValidationError(`Invalid type for protection setting: ${key} (${typeof(value)})`);
} }
if (!settingDefinitions[key].validate(value)) { if (!protection.settings[key].validate(value)) {
throw new ProtectionSettingValidationError(`Invalid value for protection setting: ${key} (${value})`); throw new ProtectionSettingValidationError(`Invalid value for protection setting: ${key} (${value})`);
} }
validatedSettings[key] = value; validatedSettings[key] = value;
@ -473,31 +497,40 @@ export class Mjolnir {
); );
} }
public async enableProtection(protectionName: string, persist = true): Promise<any> { /*
const definition = PROTECTIONS[protectionName]; * Given a protection object; add it to our list of protections, set whether it is enabled
if (!definition) throw new Error("Failed to find protection by name: " + protectionName); * and update its settings with any saved non-default values.
*
* @param protection The protection object we want to register
*/
public async registerProtection(protection: IProtection): Promise<any> {
this.protections.set(protection.name, protection)
const protection = definition.factory(); let enabledProtections: { enabled: string[] } | null = null;
this.protections.push(protection); try {
enabledProtections = await this.client.getAccountData(ENABLED_PROTECTIONS_EVENT_TYPE);
} catch {
// this setting either doesn't exist, or we failed to read it (bad network?)
// TODO: retry on certain failures?
}
protection.enabled = enabledProtections?.enabled.includes(protection.name) ?? false;
const savedSettings = await this.getProtectionSettings(protectionName); const savedSettings = await this.getProtectionSettings(protection.name);
for (let [key, value] of Object.entries(savedSettings)) { for (let [key, value] of Object.entries(savedSettings)) {
// this.getProtectionSettings() validates this data for us, so we don't need to // this.getProtectionSettings() validates this data for us, so we don't need to
protection.settings[key].setValue(value); protection.settings[key].setValue(value);
} }
if (persist) {
const existing = this.protections.map(p => p.name);
await this.client.setAccountData(ENABLED_PROTECTIONS_EVENT_TYPE, { enabled: existing });
}
} }
/*
public async disableProtection(protectionName: string): Promise<any> { * Given a protection object; remove it from our list of protections.
const idx = this.protections.findIndex(p => p.name === protectionName); *
if (idx >= 0) this.protections.splice(idx, 1); * @param protection The protection object we want to unregister
*/
const existing = this.protections.map(p => p.name); public unregisterProtection(protectionName: string) {
await this.client.setAccountData(ENABLED_PROTECTIONS_EVENT_TYPE, { enabled: existing }); if (!(protectionName in this.protections)) {
throw new Error("Failed to find protection by name: " + protectionName);
}
this.protections.delete(protectionName);
} }
public async watchList(roomRef: string): Promise<BanList | null> { public async watchList(roomRef: string): Promise<BanList | null> {
@ -779,8 +812,8 @@ export class Mjolnir {
if (Object.keys(this.protectedRooms).includes(roomId)) { if (Object.keys(this.protectedRooms).includes(roomId)) {
if (event['sender'] === await this.client.getUserId()) return; // Ignore ourselves if (event['sender'] === await this.client.getUserId()) return; // Ignore ourselves
// Iterate all the protections // Iterate all the enabled protections
for (const protection of this.protections) { for (const protection of this.enabledProtections) {
try { try {
await protection.handleEvent(this, roomId, event); await protection.handleEvent(this, roomId, event);
} catch (e) { } catch (e) {

View File

@ -17,7 +17,6 @@ limitations under the License.
import { htmlEscape } from "../utils"; import { htmlEscape } from "../utils";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { extractRequestError, LogService, RichReply } from "matrix-bot-sdk"; import { extractRequestError, LogService, RichReply } from "matrix-bot-sdk";
import { PROTECTIONS } from "../protections/protections";
import { isListSetting } from "../protections/ProtectionSettings"; import { isListSetting } from "../protections/ProtectionSettings";
// !mjolnir enable <protection> // !mjolnir enable <protection>
@ -51,12 +50,12 @@ enum ConfigAction {
*/ */
async function _execConfigChangeProtection(mjolnir: Mjolnir, parts: string[], action: ConfigAction): Promise<string> { async function _execConfigChangeProtection(mjolnir: Mjolnir, parts: string[], action: ConfigAction): Promise<string> {
const [protectionName, ...settingParts] = parts[0].split("."); const [protectionName, ...settingParts] = parts[0].split(".");
const protection = PROTECTIONS[protectionName]; const protection = mjolnir.protections.get(protectionName);
if (protection === undefined) { if (protection === undefined) {
return `Unknown protection ${protectionName}`; return `Unknown protection ${protectionName}`;
} }
const defaultSettings = protection.factory().settings const defaultSettings = protection.settings
const settingName = settingParts[0]; const settingName = settingParts[0];
const stringValue = parts[1]; const stringValue = parts[1];
@ -83,22 +82,16 @@ async function _execConfigChangeProtection(mjolnir: Mjolnir, parts: string[], ac
} }
} }
// we need this to show what the value used to be
const oldSettings = await mjolnir.getProtectionSettings(protectionName);
try { try {
await mjolnir.setProtectionSettings(protectionName, { [settingName]: value }); await mjolnir.setProtectionSettings(protectionName, { [settingName]: value });
} catch (e) { } catch (e) {
return `Failed to set setting: ${e.message}`; return `Failed to set setting: ${e.message}`;
} }
const enabledProtections = Object.fromEntries(mjolnir.enabledProtections.map(p => [p.name, p])); const oldValue = protection.settings[settingName].value;
if (protectionName in enabledProtections) { protection.settings[settingName].setValue(value);
// protection is currently loaded, so change the live setting value
enabledProtections[protectionName].settings[settingName].setValue(value);
}
return `Changed ${protectionName}.${settingName} to ${value} (was ${oldSettings[settingName]})`; return `Changed ${protectionName}.${settingName} to ${value} (was ${oldValue})`;
} }
/* /*
@ -146,7 +139,7 @@ export async function execConfigRemoveProtection(roomId: string, event: any, mjo
* !mjolnir get [protection name] * !mjolnir get [protection name]
*/ */
export async function execConfigGetProtection(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { export async function execConfigGetProtection(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
let pickProtections = Object.keys(PROTECTIONS); let pickProtections = Object.keys(mjolnir.protections);
if (parts.length < 3) { if (parts.length < 3) {
// no specific protectionName provided, show all of them. // no specific protectionName provided, show all of them.
@ -170,24 +163,19 @@ export async function execConfigGetProtection(roomId: string, event: any, mjolni
let anySettings = false; let anySettings = false;
for (const protectionName of pickProtections) { for (const protectionName of pickProtections) {
// get all available settings, their default values, and their parsers const protectionSettings = mjolnir.protections.get(protectionName)?.settings ?? {};
const availableSettings = PROTECTIONS[protectionName].factory().settings;
// get all saved non-default values
const savedSettings = await mjolnir.getProtectionSettings(protectionName);
if (Object.keys(availableSettings).length === 0) continue; if (Object.keys(protectionSettings).length === 0) {
continue;
}
const settingNames = Object.keys(PROTECTIONS[protectionName].factory().settings); const settingNames = Object.keys(protectionSettings);
// this means, within each protection name, setting names are sorted // this means, within each protection name, setting names are sorted
settingNames.sort(); settingNames.sort();
for (const settingName of settingNames) { for (const settingName of settingNames) {
anySettings = true; anySettings = true;
let value = availableSettings[settingName].value let value = protectionSettings[settingName].value
if (settingName in savedSettings)
// we have a non-default value for this setting, use it
value = savedSettings[settingName]
text += `* ${protectionName}.${settingName}: ${value}`; text += `* ${protectionName}.${settingName}: ${value}`;
// `protectionName` and `settingName` are user-provided but // `protectionName` and `settingName` are user-provided but
// validated against the names of existing protections and their // validated against the names of existing protections and their
@ -214,16 +202,15 @@ export async function execDisableProtection(roomId: string, event: any, mjolnir:
// !mjolnir protections // !mjolnir protections
export async function execListProtections(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { export async function execListProtections(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
const possibleProtections = Object.keys(PROTECTIONS);
const enabledProtections = mjolnir.enabledProtections.map(p => p.name); const enabledProtections = mjolnir.enabledProtections.map(p => p.name);
let html = "Available protections:<ul>"; let html = "Available protections:<ul>";
let text = "Available protections:\n"; let text = "Available protections:\n";
for (const protection of possibleProtections) { for (const [protectionName, protection] of mjolnir.protections) {
const emoji = enabledProtections.includes(protection) ? '🟢 (enabled)' : '🔴 (disabled)'; const emoji = enabledProtections.includes(protectionName) ? '🟢 (enabled)' : '🔴 (disabled)';
html += `<li>${emoji} <code>${protection}</code> - ${PROTECTIONS[protection].description}</li>`; html += `<li>${emoji} <code>${protectionName}</code> - ${protection.description}</li>`;
text += `* ${emoji} ${protection} - ${PROTECTIONS[protection].description}\n`; text += `* ${emoji} ${protectionName} - ${protection.description}\n`;
} }
html += "</ul>"; html += "</ul>";

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { IProtection } from "./IProtection"; import { Protection } from "./IProtection";
import { NumberProtectionSetting } from "./ProtectionSettings"; import { NumberProtectionSetting } from "./ProtectionSettings";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { LogLevel, LogService } from "matrix-bot-sdk"; import { LogLevel, LogService } from "matrix-bot-sdk";
@ -25,7 +25,7 @@ import config from "../config";
export const DEFAULT_MAX_PER_MINUTE = 10; export const DEFAULT_MAX_PER_MINUTE = 10;
const TIMESTAMP_THRESHOLD = 30000; // 30s out of phase const TIMESTAMP_THRESHOLD = 30000; // 30s out of phase
export class BasicFlooding implements IProtection { export class BasicFlooding extends Protection {
private lastEvents: { [roomId: string]: { [userId: string]: { originServerTs: number, eventId: string }[] } } = {}; private lastEvents: { [roomId: string]: { [userId: string]: { originServerTs: number, eventId: string }[] } } = {};
private recentlyBanned: string[] = []; private recentlyBanned: string[] = [];
@ -34,11 +34,13 @@ export class BasicFlooding implements IProtection {
maxPerMinute: new NumberProtectionSetting(DEFAULT_MAX_PER_MINUTE) maxPerMinute: new NumberProtectionSetting(DEFAULT_MAX_PER_MINUTE)
}; };
constructor() { }
public get name(): string { public get name(): string {
return 'BasicFloodingProtection'; return 'BasicFloodingProtection';
} }
public get description(): string {
return "If a user posts more than " + DEFAULT_MAX_PER_MINUTE + " messages in 60s they'll be " +
"banned for spam. This does not publish the ban to any of your ban lists.";
}
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> { public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
if (!this.lastEvents[roomId]) this.lastEvents[roomId] = {}; if (!this.lastEvents[roomId]) this.lastEvents[roomId] = {};

View File

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { IProtection } from "./IProtection"; import { Protection } from "./IProtection";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { LogLevel, LogService } from "matrix-bot-sdk"; import { LogLevel, LogService } from "matrix-bot-sdk";
import { logMessage } from "../LogProxy"; import { logMessage } from "../LogProxy";
import config from "../config"; import config from "../config";
import { isTrueJoinEvent } from "../utils"; import { isTrueJoinEvent } from "../utils";
export class FirstMessageIsImage implements IProtection { export class FirstMessageIsImage extends Protection {
private justJoined: { [roomId: string]: string[] } = {}; private justJoined: { [roomId: string]: string[] } = {};
private recentlyBanned: string[] = []; private recentlyBanned: string[] = [];
@ -29,11 +29,16 @@ export class FirstMessageIsImage implements IProtection {
settings = {}; settings = {};
constructor() { constructor() {
super();
} }
public get name(): string { public get name(): string {
return 'FirstMessageIsImageProtection'; return 'FirstMessageIsImageProtection';
} }
public get description(): string {
return "If the first thing a user does after joining is to post an image or video, " +
"they'll be banned for spam. This does not publish the ban to any of your ban lists.";
}
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> { public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
if (!this.justJoined[roomId]) this.justJoined[roomId] = []; if (!this.justJoined[roomId]) this.justJoined[roomId] = [];

View File

@ -25,6 +25,15 @@ import { AbstractProtectionSetting } from "./ProtectionSettings";
*/ */
export interface IProtection { export interface IProtection {
readonly name: string; readonly name: string;
readonly description: string;
enabled: boolean;
settings: { [setting: string]: AbstractProtectionSetting<any, any> }; settings: { [setting: string]: AbstractProtectionSetting<any, any> };
handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any>; handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any>;
} }
export abstract class Protection implements IProtection {
abstract readonly name: string
abstract readonly description: string;
enabled = false;
abstract settings: { [setting: string]: AbstractProtectionSetting<any, any> };
abstract handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any>;
}

View File

@ -14,22 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { IProtection } from "./IProtection"; import { Protection } from "./IProtection";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk"; import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk";
import { logMessage } from "../LogProxy"; import { logMessage } from "../LogProxy";
import config from "../config"; import config from "../config";
export class MessageIsMedia implements IProtection { export class MessageIsMedia extends Protection {
settings = {}; settings = {};
constructor() { constructor() {
super();
} }
public get name(): string { public get name(): string {
return 'MessageIsMediaProtection'; return 'MessageIsMediaProtection';
} }
public get description(): string {
return "If a user posts an image or video, that message will be redacted. No bans are issued.";
}
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> { public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
if (event['type'] === 'm.room.message') { if (event['type'] === 'm.room.message') {

View File

@ -14,22 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { IProtection } from "./IProtection"; import { Protection } from "./IProtection";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk"; import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk";
import { logMessage } from "../LogProxy"; import { logMessage } from "../LogProxy";
import config from "../config"; import config from "../config";
export class MessageIsVoice implements IProtection { export class MessageIsVoice extends Protection {
settings = {}; settings = {};
constructor() { constructor() {
super();
} }
public get name(): string { public get name(): string {
return 'MessageIsVoiceProtection'; return 'MessageIsVoiceProtection';
} }
public get description(): string {
return "If a user posts a voice message, that message will be redacted. No bans are issued.";
}
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> { public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
if (event['type'] === 'm.room.message' && event['content']) { if (event['type'] === 'm.room.message' && event['content']) {

View File

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { IProtection } from "./IProtection"; import { Protection } from "./IProtection";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { LogLevel, LogService } from "matrix-bot-sdk"; import { LogLevel, LogService } from "matrix-bot-sdk";
import { logMessage } from "../LogProxy"; import { logMessage } from "../LogProxy";
import config from "../config"; import config from "../config";
import { isTrueJoinEvent } from "../utils"; import { isTrueJoinEvent } from "../utils";
export class WordList implements IProtection { export class WordList extends Protection {
settings = {}; settings = {};
@ -29,6 +29,7 @@ export class WordList implements IProtection {
private badWords: RegExp; private badWords: RegExp;
constructor() { constructor() {
super();
// Create a mega-regex from all the tiny baby regexs // Create a mega-regex from all the tiny baby regexs
this.badWords = new RegExp( this.badWords = new RegExp(
"(" + config.protections.wordlist.words.join(")|(") + ")", "(" + config.protections.wordlist.words.join(")|(") + ")",
@ -39,6 +40,10 @@ export class WordList implements IProtection {
public get name(): string { public get name(): string {
return 'WordList'; return 'WordList';
} }
public get description(): string {
return "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.";
}
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> { public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {

View File

@ -16,40 +16,15 @@ limitations under the License.
import { FirstMessageIsImage } from "./FirstMessageIsImage"; import { FirstMessageIsImage } from "./FirstMessageIsImage";
import { IProtection } from "./IProtection"; import { IProtection } from "./IProtection";
import { BasicFlooding, DEFAULT_MAX_PER_MINUTE } from "./BasicFlooding"; import { BasicFlooding } from "./BasicFlooding";
import { WordList } from "./WordList"; import { WordList } from "./WordList";
import { MessageIsVoice } from "./MessageIsVoice"; import { MessageIsVoice } from "./MessageIsVoice";
import { MessageIsMedia } from "./MessageIsMedia"; import { MessageIsMedia } from "./MessageIsMedia";
export const PROTECTIONS: PossibleProtections = { export const PROTECTIONS: IProtection[] = [
[new FirstMessageIsImage().name]: { new FirstMessageIsImage(),
description: "If the first thing a user does after joining is to post an image or video, " + new BasicFlooding(),
"they'll be banned for spam. This does not publish the ban to any of your ban lists.", new WordList(),
factory: () => new FirstMessageIsImage(), new MessageIsVoice(),
}, new MessageIsMedia()
[new BasicFlooding().name]: { ];
description: "If a user posts more than " + DEFAULT_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(),
},
[new MessageIsVoice().name]: {
description: "If a user posts a voice message, that message will be redacted. No bans are issued.",
factory: () => new MessageIsVoice(),
},
[new MessageIsMedia().name]: {
description: "If a user posts an image or video, that message will be redacted. No bans are issued.",
factory: () => new MessageIsMedia(),
}
};
export interface PossibleProtections {
[name: string]: {
description: string;
factory: () => IProtection;
};
}

View File

@ -28,14 +28,11 @@ describe("Test: Protection settings", function() {
it("Mjolnir successfully saves valid protection setting values", async function() { it("Mjolnir successfully saves valid protection setting values", async function() {
this.timeout(20000); this.timeout(20000);
PROTECTIONS["05OVMS"] = { await this.mjolnir.registerProtection(new class implements IProtection {
description: "A test protection", name = "05OVMS";
factory: () => new class implements IProtection { description = "A test protection";
name = "05OVMS"; settings = { test: new NumberProtectionSetting(3) };
async handleEvent(mjolnir: Mjolnir, roomId: string, event: any) {}; });
settings = { test: new NumberProtectionSetting(3) };
}
};
await this.mjolnir.setProtectionSettings("05OVMS", { test: 123 }); await this.mjolnir.setProtectionSettings("05OVMS", { test: 123 });
assert.equal( assert.equal(
@ -46,17 +43,13 @@ describe("Test: Protection settings", function() {
it("Mjolnir should accumulate changed settings", async function() { it("Mjolnir should accumulate changed settings", async function() {
this.timeout(20000); this.timeout(20000);
PROTECTIONS["HPUjKN"] = { await this.mjolnir.registerProtection(new class implements IProtection {
description: "A test protection", name = "HPUjKN";
factory: () => new class implements IProtection { settings = {
name = "HPUjKN"; test1: new NumberProtectionSetting(3),
async handleEvent(mjolnir: Mjolnir, roomId: string, event: any) {}; test2: new NumberProtectionSetting(4)
settings = { };
test1: new NumberProtectionSetting(3), });
test2: new NumberProtectionSetting(4)
};
}
};
await this.mjolnir.setProtectionSettings("HPUjKN", { test1: 1 }); await this.mjolnir.setProtectionSettings("HPUjKN", { test1: 1 });
await this.mjolnir.setProtectionSettings("HPUjKN", { test2: 2 }); await this.mjolnir.setProtectionSettings("HPUjKN", { test2: 2 });
@ -68,14 +61,11 @@ describe("Test: Protection settings", function() {
this.timeout(20000); this.timeout(20000);
await client.joinRoom(config.managementRoom); await client.joinRoom(config.managementRoom);
PROTECTIONS["JY2TPN"] = { await this.mjolnir.registerProtection(new class implements IProtection {
description: "A test protection", name = "JY2TPN";
factory: () => new class implements IProtection { description = "A test protection";
name = "JY2TPN"; settings = { test: new StringProtectionSetting() };
async handleEvent(mjolnir: Mjolnir, roomId: string, event: any) {}; });
settings = { test: new StringProtectionSetting() };
}
};
let reply = new Promise((resolve, reject) => { let reply = new Promise((resolve, reject) => {
@ -96,14 +86,11 @@ describe("Test: Protection settings", function() {
this.timeout(20000); this.timeout(20000);
await client.joinRoom(config.managementRoom); await client.joinRoom(config.managementRoom);
PROTECTIONS["r33XyT"] = { await this.mjolnir.registerProtection(new class implements IProtection {
description: "A test protection", name = "r33XyT";
factory: () => new class implements IProtection { description = "A test protection";
name = "r33XyT"; settings = { test: new StringListProtectionSetting() };
async handleEvent(mjolnir: Mjolnir, roomId: string, event: any) {}; });
settings = { test: new StringListProtectionSetting() };
}
};
let reply = new Promise((resolve, reject) => { let reply = new Promise((resolve, reject) => {
@ -123,15 +110,11 @@ describe("Test: Protection settings", function() {
this.timeout(20000); this.timeout(20000);
await client.joinRoom(config.managementRoom); await client.joinRoom(config.managementRoom);
PROTECTIONS["oXzT0E"] = { await this.mjolnir.registerProtection(new class implements IProtection {
description: "A test protection", name = "oXzT0E";
factory: () => new class implements IProtection { description = "A test protection";
name = "oXzT0E"; settings = { test: new StringListProtectionSetting() };
async handleEvent(mjolnir: Mjolnir, roomId: string, event: any) {}; });
settings = { test: new StringListProtectionSetting() };
}
};
let reply = () => new Promise((resolve, reject) => { let reply = () => new Promise((resolve, reject) => {
client.on('room.message', noticeListener(this.mjolnir.managementRoomId, (event) => { client.on('room.message', noticeListener(this.mjolnir.managementRoomId, (event) => {
@ -148,5 +131,33 @@ describe("Test: Protection settings", function() {
assert.deepEqual(await this.mjolnir.getProtectionSettings("oXzT0E"), { "test": [] }); assert.deepEqual(await this.mjolnir.getProtectionSettings("oXzT0E"), { "test": [] });
}); });
it("Mjolnir will change a protection setting in-place", async function() {
this.timeout(20000);
await client.joinRoom(config.managementRoom);
await this.mjolnir.registerProtection(new class implements IProtection {
name = "d0sNrt";
description = "A test protection";
settings = { test: new StringProtectionSetting() };
});
let replyPromise = new Promise((resolve, reject) => {
let i = 0;
client.on('room.message', noticeListener(this.mjolnir.managementRoomId, (event) => {
if (event.content.body.includes("Changed d0sNrt.test ")) {
if (++i == 2) {
resolve(event);
}
}
}))
});
await client.sendMessage(this.mjolnir.managementRoomId, {msgtype: "m.text", body: "!mjolnir config set d0sNrt.test asd1"})
await client.sendMessage(this.mjolnir.managementRoomId, {msgtype: "m.text", body: "!mjolnir config set d0sNrt.test asd2"})
assert.equal(
(await replyPromise).content.body.split("\n", 3)[2],
"Changed d0sNrt.test to asd2 (was asd1)"
)
});
}); });