diff --git a/src/LogProxy.ts b/src/LogProxy.ts new file mode 100644 index 0000000..1ca0006 --- /dev/null +++ b/src/LogProxy.ts @@ -0,0 +1,36 @@ +/* +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. +*/ + +import { LogLevel, LogService } from "matrix-bot-sdk"; +import config from "./config"; + +const levelToFn = { + [LogLevel.DEBUG.toString()]: LogService.debug, + [LogLevel.INFO.toString()]: LogService.info, + [LogLevel.WARN.toString()]: LogService.warn, + [LogLevel.ERROR.toString()]: LogService.error, +}; + +export async function logMessage(level: LogLevel, module: string, message: string | any) { + if (config.RUNTIME.client && (config.verboseLogging || LogLevel.INFO.includes(level))) { + let clientMessage = message; + if (level === LogLevel.WARN) clientMessage = `⚠ | ${message}`; + if (level === LogLevel.ERROR) clientMessage = `‼ | ${message}`; + await config.RUNTIME.client.sendNotice(config.managementRoom, clientMessage); + } + + levelToFn[level.toString()](module, message); +} diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index 83df629..564b4e3 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -14,13 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { LogService, MatrixClient, Permalinks } from "matrix-bot-sdk"; +import { LogLevel, LogService, MatrixClient, Permalinks } from "matrix-bot-sdk"; import BanList, { ALL_RULE_TYPES } from "./models/BanList"; import { applyServerAcls } from "./actions/ApplyAcl"; import { RoomUpdateError } from "./models/RoomUpdateError"; import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler"; import { applyUserBans } from "./actions/ApplyBan"; import config from "./config"; +import { logMessage } from "./LogProxy"; export const STATE_NOT_STARTED = "not_started"; export const STATE_CHECKING_PERMISSIONS = "checking_permissions"; @@ -37,14 +38,13 @@ export class Mjolnir { constructor( public readonly client: MatrixClient, - public readonly managementRoomId: string, public readonly protectedRooms: { [roomId: string]: string }, private banLists: BanList[], ) { client.on("room.event", this.handleEvent.bind(this)); client.on("room.message", async (roomId, event) => { - if (roomId !== managementRoomId) return; + if (roomId !== config.managementRoom) return; if (!event['content']) return; const content = event['content']; @@ -55,6 +55,7 @@ export class Mjolnir { // rewrite the event body to make the prefix uniform (in case the bot has spaces in its display name) event['content']['body'] = COMMAND_PREFIX + content['body'].substring(prefixUsed.length); + LogService.info("Mjolnir", `Command being run by ${event['sender']}: ${event['content']['body']}`); await client.sendReadReceipt(roomId, event['event_id']); return handleCommand(roomId, event, this); @@ -83,25 +84,19 @@ export class Mjolnir { return this.client.start().then(async () => { this.currentState = STATE_CHECKING_PERMISSIONS; if (config.verifyPermissionsOnStartup) { - if (config.verboseLogging) { - await this.client.sendNotice(this.managementRoomId, "Checking permissions..."); - } + await logMessage(LogLevel.INFO, "Mjolnir@startup", "Checking permissions..."); await this.verifyPermissions(config.verboseLogging); } }).then(async () => { this.currentState = STATE_SYNCING; if (config.syncOnStartup) { - if (config.verboseLogging) { - await this.client.sendNotice(this.managementRoomId, "Syncing lists..."); - } + await logMessage(LogLevel.INFO, "Mjolnir@startup", "Syncing lists..."); await this.buildWatchedBanLists(); await this.syncLists(config.verboseLogging); } }).then(async () => { this.currentState = STATE_RUNNING; - if (config.verboseLogging) { - await this.client.sendNotice(this.managementRoomId, "Startup complete."); - } + await logMessage(LogLevel.INFO, "Mjolnir@startup", "Startup complete. Now monitoring rooms."); }); } @@ -181,7 +176,7 @@ export class Mjolnir { if (!hadErrors && verbose) { const html = `All permissions look OK.`; const text = "All permissions look OK."; - await this.client.sendMessage(this.managementRoomId, { + await this.client.sendMessage(config.managementRoom, { msgtype: "m.notice", body: text, format: "org.matrix.custom.html", @@ -257,7 +252,7 @@ export class Mjolnir { if (!hadErrors && verbose) { const html = `Done updating rooms - no errors`; const text = "Done updating rooms - no errors"; - await this.client.sendMessage(this.managementRoomId, { + await this.client.sendMessage(config.managementRoom, { msgtype: "m.notice", body: text, format: "org.matrix.custom.html", @@ -285,7 +280,7 @@ export class Mjolnir { if (!hadErrors) { const html = `Done updating rooms - no errors`; const text = "Done updating rooms - no errors"; - await this.client.sendMessage(this.managementRoomId, { + await this.client.sendMessage(config.managementRoom, { msgtype: "m.notice", body: text, format: "org.matrix.custom.html", @@ -302,7 +297,7 @@ export class Mjolnir { const url = this.protectedRooms[roomId]; let html = `Power levels changed in ${roomId} - checking permissions...`; let text = `Power levels changed in ${url} - checking permissions...`; - await this.client.sendMessage(this.managementRoomId, { + await this.client.sendMessage(config.managementRoom, { msgtype: "m.notice", body: text, format: "org.matrix.custom.html", @@ -313,7 +308,7 @@ export class Mjolnir { if (!hadErrors) { html = `All permissions look OK.`; text = "All permissions look OK."; - await this.client.sendMessage(this.managementRoomId, { + await this.client.sendMessage(config.managementRoom, { msgtype: "m.notice", body: text, format: "org.matrix.custom.html", @@ -358,7 +353,7 @@ export class Mjolnir { format: "org.matrix.custom.html", formatted_body: html, }; - await this.client.sendMessage(this.managementRoomId, message); + await this.client.sendMessage(config.managementRoom, message); return true; } } diff --git a/src/actions/ApplyAcl.ts b/src/actions/ApplyAcl.ts index dda4927..232daf8 100644 --- a/src/actions/ApplyAcl.ts +++ b/src/actions/ApplyAcl.ts @@ -19,6 +19,8 @@ import { ServerAcl } from "../models/ServerAcl"; import { RoomUpdateError } from "../models/RoomUpdateError"; import { Mjolnir } from "../Mjolnir"; import config from "../config"; +import { LogLevel } from "matrix-bot-sdk"; +import { logMessage } from "../LogProxy"; /** * Applies the server ACLs represented by the ban lists to the provided rooms, returning the @@ -40,35 +42,31 @@ export async function applyServerAcls(lists: BanList[], roomIds: string[], mjoln if (config.verboseLogging) { // We specifically use sendNotice to avoid having to escape HTML - await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Constructed server ACL:\n${JSON.stringify(finalAcl, null, 2)}`); + await mjolnir.client.sendNotice(config.managementRoom, `Constructed server ACL:\n${JSON.stringify(finalAcl, null, 2)}`); } const errors: RoomUpdateError[] = []; for (const roomId of roomIds) { try { - if (config.verboseLogging) { - await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Checking ACLs for ${roomId}`); - } + await logMessage(LogLevel.DEBUG, "ApplyAcl", `Checking ACLs for ${roomId}`); try { const currentAcl = await mjolnir.client.getRoomStateEvent(roomId, "m.room.server_acl", ""); if (acl.matches(currentAcl)) { - if (config.verboseLogging) { - await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Skipping ACLs for ${roomId} because they are already the right ones`); - } + await logMessage(LogLevel.DEBUG, "ApplyAcl", `Skipping ACLs for ${roomId} because they are already the right ones`); continue; } } catch (e) { // ignore - assume no ACL } - if (config.verboseLogging) { - // We specifically use sendNotice to avoid having to escape HTML - await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Applying ACL in ${roomId}`); - } + // We specifically use sendNotice to avoid having to escape HTML + await logMessage(LogLevel.DEBUG, "ApplyAcl", `Applying ACL in ${roomId}`); if (!config.noop) { await mjolnir.client.sendStateEvent(roomId, "m.room.server_acl", "", finalAcl); + } else { + 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 : '')}); diff --git a/src/actions/ApplyBan.ts b/src/actions/ApplyBan.ts index a80fda9..6d4b991 100644 --- a/src/actions/ApplyBan.ts +++ b/src/actions/ApplyBan.ts @@ -18,6 +18,8 @@ import BanList from "../models/BanList"; import { RoomUpdateError } from "../models/RoomUpdateError"; import { Mjolnir } from "../Mjolnir"; import config from "../config"; +import { logMessage } from "../LogProxy"; +import { LogLevel } from "matrix-bot-sdk"; /** * Applies the member bans represented by the ban lists to the provided rooms, returning the @@ -32,10 +34,8 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir let bansApplied = 0; for (const roomId of roomIds) { try { - if (config.verboseLogging) { - // We specifically use sendNotice to avoid having to escape HTML - await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Updating member bans in ${roomId}`); - } + // We specifically use sendNotice to avoid having to escape HTML + await logMessage(LogLevel.DEBUG, "ApplyBan", `Updating member bans in ${roomId}`); const state = await mjolnir.client.getRoomState(roomId); const members = state.filter(s => s['type'] === 'm.room.member' && !!s['state_key']); @@ -54,13 +54,13 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir if (userRule.isMatch(member['state_key'])) { // User needs to be banned - if (config.verboseLogging) { - // We specifically use sendNotice to avoid having to escape HTML - await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Banning ${member['state_key']} in ${roomId} for: ${userRule.reason}`); - } + // We specifically use sendNotice to avoid having to escape HTML + await logMessage(LogLevel.DEBUG, "ApplyBan", `Banning ${member['state_key']} in ${roomId} for: ${userRule.reason}`); if (!config.noop) { await mjolnir.client.banUser(member['state_key'], roomId, userRule.reason); + } else { + await logMessage(LogLevel.WARN, "ApplyBan", `Tried to ban ${member['state_key']} in ${roomId} but Mjolnir is running in no-op mode`); } bansApplied++; @@ -79,7 +79,7 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir if (bansApplied > 0) { const html = `Banned ${bansApplied} people`; const text = `Banned ${bansApplied} people`; - await this.client.sendMessage(mjolnir.managementRoomId, { + await this.client.sendMessage(config.managementRoom, { msgtype: "m.notice", body: text, format: "org.matrix.custom.html", diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index 3e338c7..5bc43a9 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -83,6 +83,6 @@ export async function handleCommand(roomId: string, event: any, mjolnir: Mjolnir const text = "There was an error processing your command - see console/log for details"; const reply = RichReply.createFor(roomId, event, text, text); reply["msgtype"] = "m.notice"; - return mjolnir.client.sendMessage(roomId, reply); + return await mjolnir.client.sendMessage(roomId, reply); } } diff --git a/src/commands/ImportCommand.ts b/src/commands/ImportCommand.ts index 784102a..d7e67c3 100644 --- a/src/commands/ImportCommand.ts +++ b/src/commands/ImportCommand.ts @@ -18,6 +18,7 @@ import { Mjolnir } from "../Mjolnir"; import { RichReply } from "matrix-bot-sdk"; import { RECOMMENDATION_BAN, recommendationToStable } from "../models/ListRule"; import { RULE_SERVER, RULE_USER, ruleTypeToStable } from "../models/BanList"; +import config from "../config"; // !mjolnir import export async function execImportCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { @@ -42,7 +43,7 @@ export async function execImportCommand(roomId: string, event: any, mjolnir: Mjo if (content['membership'] === 'ban') { const reason = content['reason'] || ''; - await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Adding user ${stateEvent['state_key']} to ban list`); + await mjolnir.client.sendNotice(config.managementRoom, `Adding user ${stateEvent['state_key']} to ban list`); const recommendation = recommendationToStable(RECOMMENDATION_BAN); const ruleContent = { @@ -60,7 +61,7 @@ export async function execImportCommand(roomId: string, event: any, mjolnir: Mjo for (const server of content['deny']) { const reason = ""; - await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Adding server ${server} to ban list`); + await mjolnir.client.sendNotice(config.managementRoom, `Adding server ${server} to ban list`); const recommendation = recommendationToStable(RECOMMENDATION_BAN); const ruleContent = { diff --git a/src/commands/RedactCommand.ts b/src/commands/RedactCommand.ts index aa13c6b..a5dbe33 100644 --- a/src/commands/RedactCommand.ts +++ b/src/commands/RedactCommand.ts @@ -17,6 +17,8 @@ limitations under the License. import { Mjolnir } from "../Mjolnir"; import { getMessagesByUserSinceLastJoin } from "../utils"; import config from "../config"; +import { logMessage } from "../LogProxy"; +import { LogLevel } from "matrix-bot-sdk"; // !mjolnir redact [room alias] export async function execRedactCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { @@ -28,17 +30,15 @@ export async function execRedactCommand(roomId: string, event: any, mjolnir: Mjo const targetRoomIds = roomAlias ? [roomAlias] : Object.keys(mjolnir.protectedRooms); for (const targetRoomId of targetRoomIds) { - if (config.verboseLogging) { - await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Fetching sent messages for ${userId} in ${targetRoomId} to redact...`); - } + await logMessage(LogLevel.DEBUG, "RedactCommand", `Fetching sent messages for ${userId} in ${targetRoomId} to redact...`); const eventsToRedact = await getMessagesByUserSinceLastJoin(mjolnir.client, userId, targetRoomId); for (const victimEvent of eventsToRedact) { - if (config.verboseLogging) { - await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Redacting ${victimEvent['event_id']} in ${targetRoomId}`); - } + await logMessage(LogLevel.DEBUG, "RedactCommand", `Redacting ${victimEvent['event_id']} in ${targetRoomId}`); if (!config.noop) { await mjolnir.client.redactEvent(targetRoomId, victimEvent['event_id']); + } else { + await logMessage(LogLevel.WARN, "RedactCommand", `Tried to redact ${victimEvent['event_id']} in ${targetRoomId} but Mjolnir is running in no-op mode`); } } } diff --git a/src/commands/UnbanBanCommand.ts b/src/commands/UnbanBanCommand.ts index ea0d69e..85f81b8 100644 --- a/src/commands/UnbanBanCommand.ts +++ b/src/commands/UnbanBanCommand.ts @@ -16,10 +16,11 @@ limitations under the License. import { Mjolnir } from "../Mjolnir"; import { RULE_ROOM, RULE_SERVER, RULE_USER, ruleTypeToStable, USER_RULE_TYPES } from "../models/BanList"; -import { RichReply } from "matrix-bot-sdk"; +import { LogLevel, RichReply } from "matrix-bot-sdk"; import { RECOMMENDATION_BAN, recommendationToStable } from "../models/ListRule"; import { MatrixGlob } from "matrix-bot-sdk/lib/MatrixGlob"; import config from "../config"; +import { logMessage } from "../LogProxy"; function parseBits(parts: string[]): { listShortcode: string, entityType: string, ruleType: string, glob: string, reason: string } { const shortcode = parts[2].toLowerCase(); @@ -98,7 +99,7 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol if (USER_RULE_TYPES.includes(bits.ruleType) && parts.length > 5 && parts[5] === 'true') { const rule = new MatrixGlob(bits.glob); - await mjolnir.client.sendNotice(mjolnir.managementRoomId, "Unbanning users that match glob: " + bits.glob); + await logMessage(LogLevel.INFO, "UnbanBanCommand", "Unbanning users that match glob: " + bits.glob); let unbannedSomeone = false; for (const protectedRoomId of Object.keys(mjolnir.protectedRooms)) { const members = await mjolnir.client.getMembers(protectedRoomId, null, ['ban'], null); @@ -106,21 +107,21 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol const victim = member['state_key']; if (!member['content'] || member['content']['membership'] !== 'ban') continue; if (rule.test(victim)) { - if (config.verboseLogging) { - await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Unbanning ${victim} in ${protectedRoomId}`); - } + await logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Unbanning ${victim} in ${protectedRoomId}`); + if (!config.noop) { await mjolnir.client.unbanUser(victim, protectedRoomId); + } else { + await logMessage(LogLevel.WARN, "UnbanBanCommand", `Attempted to unban ${victim} in ${protectedRoomId} but Mjolnir is running in no-op mode`); } + unbannedSomeone = true; } } } if (unbannedSomeone) { - if (config.verboseLogging) { - await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Syncing lists to ensure no users were accidentally unbanned`); - } + await logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Syncing lists to ensure no users were accidentally unbanned`); await mjolnir.syncLists(config.verboseLogging); } } diff --git a/src/config.ts b/src/config.ts index 32ce24b..50a6b2e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,6 +15,7 @@ limitations under the License. */ import * as config from "config"; +import { MatrixClient } from "matrix-bot-sdk"; interface IConfig { homeserverUrl: string; @@ -33,6 +34,14 @@ interface IConfig { verifyPermissionsOnStartup: boolean; noop: boolean; protectedRooms: string[]; // matrix.to urls + + /** + * Config options only set at runtime. Try to avoid using the objects + * here as much as possible. + */ + RUNTIME: { + client: MatrixClient; + }; } export default config; diff --git a/src/index.ts b/src/index.ts index 6ff9685..d1be023 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,9 @@ import { import config from "./config"; import BanList from "./models/BanList"; import { Mjolnir } from "./Mjolnir"; +import { logMessage } from "./LogProxy"; + +config.RUNTIME = {client: null}; LogService.setLogger(new RichConsoleLogger()); LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG)); @@ -45,6 +48,8 @@ LogService.info("index", "Starting bot..."); client = new MatrixClient(config.homeserverUrl, config.accessToken, storage); } + config.RUNTIME.client = client; + if (config.autojoin) { AutojoinRoomsMixin.setupOnClient(client); } @@ -68,9 +73,9 @@ LogService.info("index", "Starting bot..."); } // Ensure we're also in the management room - const managementRoomId = await client.joinRoom(config.managementRoom); - await client.sendNotice(managementRoomId, "Mjolnir is starting up. Use !mjolnir to query status."); + config.managementRoom = await client.joinRoom(config.managementRoom); + await logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status."); - const bot = new Mjolnir(client, managementRoomId, protectedRooms, banLists); + const bot = new Mjolnir(client, protectedRooms, banLists); await bot.start(); })();