Avoid spamming the management room with errors

The intervals are arbitrarily defined.

Fixes https://github.com/matrix-org/mjolnir/issues/10
This commit is contained in:
Travis Ralston 2019-11-06 19:17:11 -07:00
parent 30e186ca9c
commit 82214c6cd8
6 changed files with 110 additions and 11 deletions

59
src/ErrorCache.ts Normal file
View File

@ -0,0 +1,59 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
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.
*/
export const ERROR_KIND_PERMISSION = "permission";
export const ERROR_KIND_FATAL = "fatal";
const TRIGGER_INTERVALS = {
[ERROR_KIND_PERMISSION]: 3 * 60 * 60 * 1000, // 3 hours
[ERROR_KIND_FATAL]: 15 * 60 * 1000, // 15 minutes
};
export default class ErrorCache {
private static roomsToErrors: { [roomId: string]: { [kind: string]: number } } = {};
private constructor() {
}
public static resetError(roomId: string, kind: string) {
if (!ErrorCache.roomsToErrors[roomId]) {
ErrorCache.roomsToErrors[roomId] = {};
}
ErrorCache.roomsToErrors[roomId][kind] = 0;
}
public static triggerError(roomId: string, kind: string): boolean {
if (!ErrorCache.roomsToErrors[roomId]) {
ErrorCache.roomsToErrors[roomId] = {};
}
const triggers = ErrorCache.roomsToErrors[roomId];
if (!triggers[kind]) {
triggers[kind] = 0;
}
const lastTriggerTime = triggers[kind];
const now = new Date().getTime();
const interval = TRIGGER_INTERVALS[kind];
if ((now - lastTriggerTime) >= interval) {
triggers[kind] = now;
return true;
} else {
return false;
}
}
}

View File

@ -22,6 +22,7 @@ import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler";
import { applyUserBans } from "./actions/ApplyBan"; import { applyUserBans } from "./actions/ApplyBan";
import config from "./config"; import config from "./config";
import { logMessage } from "./LogProxy"; import { logMessage } from "./LogProxy";
import ErrorCache, { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "./ErrorCache";
export const STATE_NOT_STARTED = "not_started"; export const STATE_NOT_STARTED = "not_started";
export const STATE_CHECKING_PERMISSIONS = "checking_permissions"; export const STATE_CHECKING_PERMISSIONS = "checking_permissions";
@ -166,13 +167,13 @@ export class Mjolnir {
this.banLists = banLists; this.banLists = banLists;
} }
public async verifyPermissions(verbose = true) { public async verifyPermissions(verbose = true, printRegardless = false) {
const errors: RoomUpdateError[] = []; const errors: RoomUpdateError[] = [];
for (const roomId of Object.keys(this.protectedRooms)) { for (const roomId of Object.keys(this.protectedRooms)) {
errors.push(...(await this.verifyPermissionsIn(roomId))); errors.push(...(await this.verifyPermissionsIn(roomId)));
} }
const hadErrors = await this.printActionResult(errors, "Permission errors in protected rooms:"); const hadErrors = await this.printActionResult(errors, "Permission errors in protected rooms:", printRegardless);
if (!hadErrors && verbose) { if (!hadErrors && verbose) {
const html = `<font color="#00cc00">All permissions look OK.</font>`; const html = `<font color="#00cc00">All permissions look OK.</font>`;
const text = "All permissions look OK."; const text = "All permissions look OK.";
@ -216,22 +217,42 @@ export class Mjolnir {
// Wants: ban, kick, redact, m.room.server_acl // Wants: ban, kick, redact, m.room.server_acl
if (userLevel < ban) { if (userLevel < ban) {
errors.push({roomId, errorMessage: `Missing power level for bans: ${userLevel} < ${ban}`}); errors.push({
roomId,
errorMessage: `Missing power level for bans: ${userLevel} < ${ban}`,
errorKind: ERROR_KIND_PERMISSION,
});
} }
if (userLevel < kick) { if (userLevel < kick) {
errors.push({roomId, errorMessage: `Missing power level for kicks: ${userLevel} < ${kick}`}); errors.push({
roomId,
errorMessage: `Missing power level for kicks: ${userLevel} < ${kick}`,
errorKind: ERROR_KIND_PERMISSION,
});
} }
if (userLevel < redact) { if (userLevel < redact) {
errors.push({roomId, errorMessage: `Missing power level for redactions: ${userLevel} < ${redact}`}); errors.push({
roomId,
errorMessage: `Missing power level for redactions: ${userLevel} < ${redact}`,
errorKind: ERROR_KIND_PERMISSION,
});
} }
if (userLevel < aclLevel) { if (userLevel < aclLevel) {
errors.push({roomId, errorMessage: `Missing power level for server ACLs: ${userLevel} < ${aclLevel}`}); errors.push({
roomId,
errorMessage: `Missing power level for server ACLs: ${userLevel} < ${aclLevel}`,
errorKind: ERROR_KIND_PERMISSION,
});
} }
// Otherwise OK // Otherwise OK
} catch (e) { } catch (e) {
LogService.error("Mjolnir", e); LogService.error("Mjolnir", e);
errors.push({roomId, errorMessage: e.message || (e.body ? e.body.error : '<no message>')}); errors.push({
roomId,
errorMessage: e.message || (e.body ? e.body.error : '<no message>'),
errorKind: ERROR_KIND_FATAL,
});
} }
return errors; return errors;
@ -294,6 +315,7 @@ export class Mjolnir {
if (event['sender'] === await this.client.getUserId()) return; // Ignore ourselves if (event['sender'] === await this.client.getUserId()) return; // Ignore ourselves
if (event['type'] === 'm.room.power_levels' && event['state_key'] === '') { if (event['type'] === 'm.room.power_levels' && event['state_key'] === '') {
// power levels were updated - recheck permissions // power levels were updated - recheck permissions
ErrorCache.resetError(roomId, ERROR_KIND_PERMISSION);
const url = this.protectedRooms[roomId]; const url = this.protectedRooms[roomId];
let html = `Power levels changed in <a href="${url}">${roomId}</a> - checking permissions...`; let html = `Power levels changed in <a href="${url}">${roomId}</a> - checking permissions...`;
let text = `Power levels changed in ${url} - checking permissions...`; let text = `Power levels changed in ${url} - checking permissions...`;
@ -329,9 +351,17 @@ export class Mjolnir {
} }
} }
private async printActionResult(errors: RoomUpdateError[], title: string = null) { private async printActionResult(errors: RoomUpdateError[], title: string = null, logAnyways = false) {
if (errors.length <= 0) return false; if (errors.length <= 0) return false;
if (!logAnyways) {
errors = errors.filter(e => ErrorCache.triggerError(e.roomId, e.errorKind));
if (errors.length <= 0) {
LogService.warn("Mjolnir", "Multiple errors are happening, however they are muted. Please check the management room.");
return true;
}
}
let html = ""; let html = "";
let text = ""; let text = "";

View File

@ -21,6 +21,7 @@ import { Mjolnir } from "../Mjolnir";
import config from "../config"; import config from "../config";
import { LogLevel } from "matrix-bot-sdk"; import { LogLevel } from "matrix-bot-sdk";
import { logMessage } from "../LogProxy"; import { logMessage } from "../LogProxy";
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
/** /**
* Applies the server ACLs represented by the ban lists to the provided rooms, returning the * Applies the server ACLs represented by the ban lists to the provided rooms, returning the
@ -69,7 +70,9 @@ export async function applyServerAcls(lists: BanList[], roomIds: string[], mjoln
await logMessage(LogLevel.WARN, "ApplyAcl", `Tried to apply ACL in ${roomId} but Mjolnir is running in no-op mode`); await logMessage(LogLevel.WARN, "ApplyAcl", `Tried to apply ACL in ${roomId} but Mjolnir is running in no-op mode`);
} }
} catch (e) { } catch (e) {
errors.push({roomId, errorMessage: e.message || (e.body ? e.body.error : '<no message>')}); const message = e.message || (e.body ? e.body.error : '<no message>');
const kind = message.includes("You don't have permission to post that to the room") ? ERROR_KIND_PERMISSION : ERROR_KIND_FATAL;
errors.push({roomId, errorMessage: message, errorKind: kind});
} }
} }

View File

@ -20,6 +20,7 @@ import { Mjolnir } from "../Mjolnir";
import config from "../config"; import config from "../config";
import { logMessage } from "../LogProxy"; import { logMessage } from "../LogProxy";
import { LogLevel } from "matrix-bot-sdk"; import { LogLevel } from "matrix-bot-sdk";
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
/** /**
* Applies the member bans represented by the ban lists to the provided rooms, returning the * Applies the member bans represented by the ban lists to the provided rooms, returning the
@ -80,7 +81,12 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir
} }
} }
} catch (e) { } catch (e) {
errors.push({roomId, errorMessage: e.message || (e.body ? e.body.error : '<no message>')}); const message = e.message || (e.body ? e.body.error : '<no message>');
errors.push({
roomId,
errorMessage: message,
errorKind: message.includes("You don't have permission to ban") ? ERROR_KIND_PERMISSION : ERROR_KIND_FATAL,
});
} }
} }

View File

@ -18,5 +18,5 @@ import { Mjolnir } from "../Mjolnir";
// !mjolnir verify // !mjolnir verify
export async function execPermissionCheckCommand(roomId: string, event: any, mjolnir: Mjolnir) { export async function execPermissionCheckCommand(roomId: string, event: any, mjolnir: Mjolnir) {
return mjolnir.verifyPermissions(); return mjolnir.verifyPermissions(true, true);
} }

View File

@ -17,4 +17,5 @@ limitations under the License.
export interface RoomUpdateError { export interface RoomUpdateError {
roomId: string; roomId: string;
errorMessage: string; errorMessage: string;
errorKind: string;
} }