diff --git a/src/LogProxy.ts b/src/LogProxy.ts index 1ca0006..28e4d0a 100644 --- a/src/LogProxy.ts +++ b/src/LogProxy.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -16,6 +16,7 @@ limitations under the License. import { LogLevel, LogService } from "matrix-bot-sdk"; import config from "./config"; +import { replaceRoomIdsWithPills } from "./utils"; const levelToFn = { [LogLevel.DEBUG.toString()]: LogService.debug, @@ -24,12 +25,20 @@ const levelToFn = { [LogLevel.ERROR.toString()]: LogService.error, }; -export async function logMessage(level: LogLevel, module: string, message: string | any) { +export async function logMessage(level: LogLevel, module: string, message: string | any, additionalRoomIds: string[] | string = null) { + if (!additionalRoomIds) additionalRoomIds = []; + if (!Array.isArray(additionalRoomIds)) additionalRoomIds = [additionalRoomIds]; + 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); + + const roomIds = [config.managementRoom, ...additionalRoomIds]; + const client = config.RUNTIME.client; + + const evContent = await replaceRoomIdsWithPills(client, clientMessage, roomIds, "m.notice"); + await client.sendMessage(config.managementRoom, evContent); } levelToFn[level.toString()](module, message); diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index e3fa09c..80d9477 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -332,7 +332,7 @@ export class Mjolnir { // Ignore - probably haven't warned about it yet } - await logMessage(LogLevel.WARN, "Mjolnir", `Not protecting ${roomId} - it is a ban list that this bot did not create. Add the room as protected if it is supposed to be protected. This warning will not appear again.`); + await logMessage(LogLevel.WARN, "Mjolnir", `Not protecting ${roomId} - it is a ban list that this bot did not create. Add the room as protected if it is supposed to be protected. This warning will not appear again.`, roomId); await this.client.setAccountData(WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId, {warned: true}); } @@ -559,8 +559,7 @@ export class Mjolnir { 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]; - await logMessage(LogLevel.DEBUG, "Mjolnir", `Power levels changed in ${url} - checking permissions...`); + await logMessage(LogLevel.DEBUG, "Mjolnir", `Power levels changed in ${roomId} - checking permissions...`, roomId); const errors = await this.verifyPermissionsIn(roomId); const hadErrors = await this.printActionResult(errors); if (!hadErrors) { diff --git a/src/actions/ApplyAcl.ts b/src/actions/ApplyAcl.ts index 1abceef..d668aa4 100644 --- a/src/actions/ApplyAcl.ts +++ b/src/actions/ApplyAcl.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -49,12 +49,12 @@ export async function applyServerAcls(lists: BanList[], roomIds: string[], mjoln const errors: RoomUpdateError[] = []; for (const roomId of roomIds) { try { - await logMessage(LogLevel.DEBUG, "ApplyAcl", `Checking ACLs for ${roomId}`); + await logMessage(LogLevel.DEBUG, "ApplyAcl", `Checking ACLs for ${roomId}`, roomId); try { const currentAcl = await mjolnir.client.getRoomStateEvent(roomId, "m.room.server_acl", ""); if (acl.matches(currentAcl)) { - await logMessage(LogLevel.DEBUG, "ApplyAcl", `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`, roomId); continue; } } catch (e) { @@ -62,12 +62,12 @@ export async function applyServerAcls(lists: BanList[], roomIds: string[], mjoln } // We specifically use sendNotice to avoid having to escape HTML - await logMessage(LogLevel.DEBUG, "ApplyAcl", `Applying ACL in ${roomId}`); + await logMessage(LogLevel.DEBUG, "ApplyAcl", `Applying ACL in ${roomId}`, 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`); + await logMessage(LogLevel.WARN, "ApplyAcl", `Tried to apply ACL in ${roomId} but Mjolnir is running in no-op mode`, roomId); } } catch (e) { const message = e.message || (e.body ? e.body.error : ''); diff --git a/src/actions/ApplyBan.ts b/src/actions/ApplyBan.ts index ef49f47..e53d253 100644 --- a/src/actions/ApplyBan.ts +++ b/src/actions/ApplyBan.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -37,7 +37,7 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir for (const roomId of roomIds) { try { // We specifically use sendNotice to avoid having to escape HTML - await logMessage(LogLevel.DEBUG, "ApplyBan", `Updating member bans in ${roomId}`); + await logMessage(LogLevel.DEBUG, "ApplyBan", `Updating member bans in ${roomId}`, roomId); let members: { userId: string, membership: string }[]; @@ -65,7 +65,7 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir // User needs to be banned // We specifically use sendNotice to avoid having to escape HTML - await logMessage(LogLevel.INFO, "ApplyBan", `Banning ${member.userId} in ${roomId} for: ${userRule.reason}`); + await logMessage(LogLevel.INFO, "ApplyBan", `Banning ${member.userId} in ${roomId} for: ${userRule.reason}`, roomId); if (!config.noop) { // Always prioritize redactions above bans @@ -75,7 +75,7 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir await mjolnir.client.banUser(member.userId, roomId, userRule.reason); } else { - await logMessage(LogLevel.WARN, "ApplyBan", `Tried to ban ${member.userId} in ${roomId} but Mjolnir is running in no-op mode`); + await logMessage(LogLevel.WARN, "ApplyBan", `Tried to ban ${member.userId} in ${roomId} but Mjolnir is running in no-op mode`, roomId); } bansApplied++; diff --git a/src/commands/AddRemoveProtectedRoomsCommand.ts b/src/commands/AddRemoveProtectedRoomsCommand.ts index b674b3a..51aa42e 100644 --- a/src/commands/AddRemoveProtectedRoomsCommand.ts +++ b/src/commands/AddRemoveProtectedRoomsCommand.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { Mjolnir } from "../Mjolnir"; -import { LogLevel, LogService, RichReply } from "matrix-bot-sdk"; +import { LogLevel, LogService } from "matrix-bot-sdk"; import { logMessage } from "../LogProxy"; // !mjolnir rooms add @@ -33,7 +33,7 @@ export async function execRemoveProtectedRoom(roomId: string, event: any, mjolni await mjolnir.client.leaveRoom(protectedRoomId); } catch (e) { LogService.warn("AddRemoveProtectedRoomsCommand", e); - await logMessage(LogLevel.WARN, "AddRemoveProtectedRoomsCommand", `Failed to leave ${protectedRoomId} - the room is no longer being protected, but the bot could not leave`); + await logMessage(LogLevel.WARN, "AddRemoveProtectedRoomsCommand", `Failed to leave ${protectedRoomId} - the room is no longer being protected, but the bot could not leave`, protectedRoomId); } await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); } diff --git a/src/commands/SetPowerLevelCommand.ts b/src/commands/SetPowerLevelCommand.ts index 62ae120..0d98418 100644 --- a/src/commands/SetPowerLevelCommand.ts +++ b/src/commands/SetPowerLevelCommand.ts @@ -31,7 +31,7 @@ export async function execSetPowerLevelCommand(roomId: string, event: any, mjoln await mjolnir.client.setUserPowerLevel(victim, targetRoomId, level); } catch (e) { const message = e.message || (e.body ? e.body.error : ''); - await logMessage(LogLevel.ERROR, "SetPowerLevelCommand", `Failed to set power level of ${victim} to ${level} in ${targetRoomId}: ${message}`); + await logMessage(LogLevel.ERROR, "SetPowerLevelCommand", `Failed to set power level of ${victim} to ${level} in ${targetRoomId}: ${message}`, targetRoomId); LogService.error("SetPowerLevelCommand", e); } } diff --git a/src/commands/UnbanBanCommand.ts b/src/commands/UnbanBanCommand.ts index 16d230d..983640d 100644 --- a/src/commands/UnbanBanCommand.ts +++ b/src/commands/UnbanBanCommand.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -142,12 +142,12 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol const victim = member.membershipFor; if (member.membership !== 'ban') continue; if (rule.test(victim)) { - await logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Unbanning ${victim} in ${protectedRoomId}`); + await logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Unbanning ${victim} in ${protectedRoomId}`, 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`); + await logMessage(LogLevel.WARN, "UnbanBanCommand", `Attempted to unban ${victim} in ${protectedRoomId} but Mjolnir is running in no-op mode`, protectedRoomId); } unbannedSomeone = true; diff --git a/src/protections/BasicFlooding.ts b/src/protections/BasicFlooding.ts index 42c04ba..7012b70 100644 --- a/src/protections/BasicFlooding.ts +++ b/src/protections/BasicFlooding.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -69,14 +69,14 @@ export class BasicFlooding implements IProtection { await mjolnir.client.redactEvent(roomId, eventId, "spam"); } } else { - await logMessage(LogLevel.WARN, "BasicFlooding", `Tried to redact messages for ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`); + await logMessage(LogLevel.WARN, "BasicFlooding", `Tried to redact messages for ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); } - await logMessage(LogLevel.WARN, "BasicFlooding", `Banning ${event['sender']} in ${roomId} for flooding (${messageCount} messages in the last minute)`); + await logMessage(LogLevel.WARN, "BasicFlooding", `Banning ${event['sender']} in ${roomId} for flooding (${messageCount} messages in the last minute)`, roomId); if (!config.noop) { await mjolnir.client.banUser(event['sender'], roomId, "spam"); } else { - await logMessage(LogLevel.WARN, "BasicFlooding", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`); + await logMessage(LogLevel.WARN, "BasicFlooding", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); } // Free up some memory now that we're ready to handle it elsewhere diff --git a/src/protections/FirstMessageIsImage.ts b/src/protections/FirstMessageIsImage.ts index 44f2b20..7a381fd 100644 --- a/src/protections/FirstMessageIsImage.ts +++ b/src/protections/FirstMessageIsImage.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -61,14 +61,14 @@ export class FirstMessageIsImage implements IProtection { if (!config.noop) { await mjolnir.client.redactEvent(roomId, event['event_id'], "spam"); } else { - await logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`); + await logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); } await logMessage(LogLevel.WARN, "FirstMessageIsImage", `Banning ${event['sender']} for posting an image as the first thing after joining in ${roomId}.`); if (!config.noop) { await mjolnir.client.banUser(event['sender'], roomId, "spam"); } else { - await logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`); + await logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); } } } diff --git a/src/utils.ts b/src/utils.ts index 392f046..c2224c0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -14,9 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { LogLevel, LogService, MatrixClient, MatrixGlob } from "matrix-bot-sdk"; +import { + LogLevel, + LogService, + MatrixClient, + MatrixGlob, + MessageType, + Permalinks, + TextualMessageEventContent, + UserID +} from "matrix-bot-sdk"; import { logMessage } from "./LogProxy"; import config from "./config"; +import * as htmlEscape from "escape-html"; export function setToArray(set: Set): T[] { const arr: T[] = []; @@ -39,15 +49,15 @@ export function isTrueJoinEvent(event: any): boolean { export async function redactUserMessagesIn(client: MatrixClient, userIdOrGlob: string, targetRoomIds: string[], limit = 1000) { for (const targetRoomId of targetRoomIds) { - await logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Fetching sent messages for ${userIdOrGlob} in ${targetRoomId} to redact...`); + await logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Fetching sent messages for ${userIdOrGlob} in ${targetRoomId} to redact...`, targetRoomId); const eventsToRedact = await getMessagesByUserIn(client, userIdOrGlob, targetRoomId, limit); for (const victimEvent of eventsToRedact) { - await logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Redacting ${victimEvent['event_id']} in ${targetRoomId}`); + await logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Redacting ${victimEvent['event_id']} in ${targetRoomId}`, targetRoomId); if (!config.noop) { await client.redactEvent(targetRoomId, victimEvent['event_id']); } else { - await logMessage(LogLevel.WARN, "utils#redactUserMessagesIn", `Tried to redact ${victimEvent['event_id']} in ${targetRoomId} but Mjolnir is running in no-op mode`); + await logMessage(LogLevel.WARN, "utils#redactUserMessagesIn", `Tried to redact ${victimEvent['event_id']} in ${targetRoomId} but Mjolnir is running in no-op mode`, targetRoomId); } } } @@ -158,3 +168,43 @@ export async function getMessagesByUserIn(client: MatrixClient, sender: string, return messages; } + +export async function replaceRoomIdsWithPills(client: MatrixClient, text: string, roomIds: string[] | string, msgtype: MessageType = "m.text"): Promise { + if (!Array.isArray(roomIds)) roomIds = [roomIds]; + + const content: TextualMessageEventContent = { + body: text, + formatted_body: htmlEscape(text), + msgtype: msgtype, + format: "org.matrix.custom.html", + }; + + const escapeRegex = (v: string): string => { + return v.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + }; + + const viaServers = [(new UserID(await client.getUserId())).domain]; + for (const roomId of roomIds) { + const alias = (await getRoomAlias(client, roomId)) || roomId; + const regexRoomId = new RegExp(escapeRegex(roomId), "g"); + content.body = content.body.replace(regexRoomId, alias); + content.formatted_body = content.formatted_body.replace(regexRoomId, `${alias}`); + } + + return content; +} + +// TODO: Merge this function into js-bot-sdk +async function getRoomAlias(client: MatrixClient, roomId: string): Promise { + try { + const event = await client.getRoomStateEvent(roomId, "m.room.canonical_alias", ""); + if (!event) return null; + + const canonical = event['alias']; + const alt = event['alt_aliases'] || []; + + return canonical || alt[0]; + } catch (e) { + return null; // assume none + } +}