diff --git a/src/ErrorCache.ts b/src/ErrorCache.ts new file mode 100644 index 0000000..bc66479 --- /dev/null +++ b/src/ErrorCache.ts @@ -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; + } + } +} diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index 564b4e3..505f6be 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -22,6 +22,7 @@ import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler"; import { applyUserBans } from "./actions/ApplyBan"; import config from "./config"; import { logMessage } from "./LogProxy"; +import ErrorCache, { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "./ErrorCache"; export const STATE_NOT_STARTED = "not_started"; export const STATE_CHECKING_PERMISSIONS = "checking_permissions"; @@ -166,13 +167,13 @@ export class Mjolnir { this.banLists = banLists; } - public async verifyPermissions(verbose = true) { + public async verifyPermissions(verbose = true, printRegardless = false) { const errors: RoomUpdateError[] = []; for (const roomId of Object.keys(this.protectedRooms)) { 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) { const html = `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 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) { - 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) { - 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) { - 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 } catch (e) { LogService.error("Mjolnir", e); - errors.push({roomId, errorMessage: e.message || (e.body ? e.body.error : '')}); + errors.push({ + roomId, + errorMessage: e.message || (e.body ? e.body.error : ''), + errorKind: ERROR_KIND_FATAL, + }); } return errors; @@ -294,6 +315,7 @@ export class Mjolnir { if (event['sender'] === await this.client.getUserId()) return; // Ignore ourselves if (event['type'] === 'm.room.power_levels' && event['state_key'] === '') { // power levels were updated - recheck permissions + ErrorCache.resetError(roomId, ERROR_KIND_PERMISSION); const url = this.protectedRooms[roomId]; let html = `Power levels changed in ${roomId} - 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 (!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 text = ""; diff --git a/src/actions/ApplyAcl.ts b/src/actions/ApplyAcl.ts index 232daf8..1abceef 100644 --- a/src/actions/ApplyAcl.ts +++ b/src/actions/ApplyAcl.ts @@ -21,6 +21,7 @@ import { Mjolnir } from "../Mjolnir"; import config from "../config"; import { LogLevel } from "matrix-bot-sdk"; 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 @@ -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`); } } catch (e) { - errors.push({roomId, errorMessage: e.message || (e.body ? e.body.error : '')}); + const message = e.message || (e.body ? e.body.error : ''); + 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}); } } diff --git a/src/actions/ApplyBan.ts b/src/actions/ApplyBan.ts index 3e17722..36fbce4 100644 --- a/src/actions/ApplyBan.ts +++ b/src/actions/ApplyBan.ts @@ -20,6 +20,7 @@ import { Mjolnir } from "../Mjolnir"; import config from "../config"; import { logMessage } from "../LogProxy"; 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 @@ -80,7 +81,12 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir } } } catch (e) { - errors.push({roomId, errorMessage: e.message || (e.body ? e.body.error : '')}); + const message = e.message || (e.body ? e.body.error : ''); + errors.push({ + roomId, + errorMessage: message, + errorKind: message.includes("You don't have permission to ban") ? ERROR_KIND_PERMISSION : ERROR_KIND_FATAL, + }); } } diff --git a/src/commands/PermissionCheckCommand.ts b/src/commands/PermissionCheckCommand.ts index bc9e032..d3459f1 100644 --- a/src/commands/PermissionCheckCommand.ts +++ b/src/commands/PermissionCheckCommand.ts @@ -18,5 +18,5 @@ import { Mjolnir } from "../Mjolnir"; // !mjolnir verify export async function execPermissionCheckCommand(roomId: string, event: any, mjolnir: Mjolnir) { - return mjolnir.verifyPermissions(); + return mjolnir.verifyPermissions(true, true); } diff --git a/src/models/RoomUpdateError.ts b/src/models/RoomUpdateError.ts index bf40ccf..c5f1048 100644 --- a/src/models/RoomUpdateError.ts +++ b/src/models/RoomUpdateError.ts @@ -17,4 +17,5 @@ limitations under the License. export interface RoomUpdateError { roomId: string; errorMessage: string; + errorKind: string; }