From 21aabc879a88f62d4afd7e381d4454f05fc6819c Mon Sep 17 00:00:00 2001 From: Gnuxie <50846879+Gnuxie@users.noreply.github.com> Date: Tue, 9 Aug 2022 11:29:27 +0100 Subject: [PATCH] Stop the config being global (in almost all contexts). (#334) * Stop the config being global (in almost all contexts). * make sure unit test has a config * Make failing word list more visible * Only use Healthz from index.ts Not really sure how useful it is anyways? --- src/Mjolnir.ts | 38 +++-- src/actions/ApplyAcl.ts | 5 +- src/actions/ApplyBan.ts | 5 +- src/commands/KickCommand.ts | 5 +- src/commands/MakeRoomAdminCommand.ts | 3 +- src/commands/UnbanBanCommand.ts | 7 +- src/config.ts | 2 +- src/health/healthz.ts | 3 +- src/index.ts | 3 +- src/protections/BasicFlooding.ts | 5 +- src/protections/FirstMessageIsImage.ts | 5 +- src/protections/JoinWaveShortCircuit.ts | 3 +- src/protections/MessageIsMedia.ts | 3 +- src/protections/MessageIsVoice.ts | 3 +- src/protections/TrustedReporters.ts | 3 +- src/protections/WordList.ts | 28 ++-- src/queues/UnlistedUserRedactionQueue.ts | 3 +- src/report/ReportManager.ts | 4 +- src/utils.ts | 3 +- src/webapis/WebAPIs.ts | 28 ++-- test/commands/UnbanBanCommandTest.ts | 6 +- test/integration/banListTest.ts | 185 +++++++++++------------ test/integration/clientHelper.ts | 2 +- test/integration/fixtures.ts | 2 +- test/integration/manualLaunchScript.ts | 3 +- test/integration/mjolnirSetupUtils.ts | 10 +- 26 files changed, 179 insertions(+), 188 deletions(-) diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index 5070bdb..1d8ea88 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -32,14 +32,12 @@ 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 ErrorCache, { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "./ErrorCache"; import { Protection } from "./protections/IProtection"; import { PROTECTIONS } from "./protections/protections"; import { ConsequenceType, Consequence } from "./protections/consequence"; import { ProtectionSettingValidationError } from "./protections/ProtectionSettings"; import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue"; -import { Healthz } from "./health/healthz"; import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue"; import { htmlEscape } from "./utils"; import { ReportManager } from "./report/ReportManager"; @@ -50,6 +48,7 @@ import RuleServer from "./models/RuleServer"; import { RoomMemberManager } from "./RoomMembers"; import { ProtectedRoomActivityTracker } from "./queues/ProtectedRoomActivityTracker"; import { ThrottlingQueue } from "./queues/ThrottlingQueue"; +import { IConfig } from "./config"; import PolicyList, { ListRuleChange } from "./models/PolicyList"; const levelToFn = { @@ -162,7 +161,7 @@ export class Mjolnir { * @param {MatrixClient} client The client for Mjolnir to use. * @returns A new Mjolnir instance that can be started without further setup. */ - static async setupMjolnirFromConfig(client: MatrixClient): Promise { + static async setupMjolnirFromConfig(client: MatrixClient, config: IConfig): Promise { const policyLists: PolicyList[] = []; const protectedRooms: { [roomId: string]: string } = {}; const joinedRooms = await client.getJoinedRooms(); @@ -188,7 +187,7 @@ export class Mjolnir { } const ruleServer = config.web.ruleServer ? new RuleServer() : null; - const mjolnir = new Mjolnir(client, managementRoomId, protectedRooms, policyLists, ruleServer); + const mjolnir = new Mjolnir(client, managementRoomId, config, protectedRooms, policyLists, ruleServer); await mjolnir.logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status."); Mjolnir.addJoinOnInviteListener(mjolnir, client, config); return mjolnir; @@ -197,6 +196,7 @@ export class Mjolnir { constructor( public readonly client: MatrixClient, public readonly managementRoomId: string, + public readonly config: IConfig, /* * All the rooms that Mjolnir is protecting and their permalinks. * If `config.protectAllJoinedRooms` is specified, then `protectedRooms` will be all joined rooms except watched banlists that we can't protect (because they aren't curated by us). @@ -208,7 +208,7 @@ export class Mjolnir { ) { this.explicitlyProtectedRoomIds = Object.keys(this.protectedRooms); - for (const reason of config.automaticallyRedactForReasons) { + for (const reason of this.config.automaticallyRedactForReasons) { this.automaticRedactionReasons.push(new MatrixGlob(reason.toLowerCase())); } @@ -276,7 +276,7 @@ export class Mjolnir { console.log("Creating Web APIs"); const reportManager = new ReportManager(this); reportManager.on("report.new", this.handleReport.bind(this)); - this.webapis = new WebAPIs(reportManager, this.ruleServer); + this.webapis = new WebAPIs(reportManager, this.config, this.ruleServer); if (config.pollReports) { this.reportPoller = new ReportPoller(this, reportManager); } @@ -356,20 +356,19 @@ export class Mjolnir { await this.buildWatchedPolicyLists(); this.applyUnprotectedRooms(); - if (config.verifyPermissionsOnStartup) { + if (this.config.verifyPermissionsOnStartup) { await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Checking permissions..."); - await this.verifyPermissions(config.verboseLogging); + await this.verifyPermissions(this.config.verboseLogging); } this.currentState = STATE_SYNCING; - if (config.syncOnStartup) { + if (this.config.syncOnStartup) { await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Syncing lists..."); - await this.syncLists(config.verboseLogging); + await this.syncLists(this.config.verboseLogging); await this.registerProtections(); } this.currentState = STATE_RUNNING; - Healthz.isHealthy = true; await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Startup complete. Now monitoring rooms."); } catch (err) { try { @@ -399,14 +398,13 @@ export class Mjolnir { if (!additionalRoomIds) additionalRoomIds = []; if (!Array.isArray(additionalRoomIds)) additionalRoomIds = [additionalRoomIds]; - if (config.RUNTIME.client && (config.verboseLogging || LogLevel.INFO.includes(level))) { + if (this.config.verboseLogging || LogLevel.INFO.includes(level)) { let clientMessage = message; if (level === LogLevel.WARN) clientMessage = `⚠ | ${message}`; if (level === LogLevel.ERROR) clientMessage = `‼ | ${message}`; - const client = config.RUNTIME.client; - const managementRoomId = await client.resolveRoom(config.managementRoom); - const roomIds = [managementRoomId, ...additionalRoomIds]; + const client = this.client; + const roomIds = [this.managementRoomId, ...additionalRoomIds]; let evContent: TextualMessageEventContent = { body: message, @@ -418,7 +416,7 @@ export class Mjolnir { evContent = await replaceRoomIdsWithPills(this, clientMessage, new Set(roomIds), "m.notice"); } - await client.sendMessage(managementRoomId, evContent); + await client.sendMessage(this.managementRoomId, evContent); } levelToFn[level.toString()](module, message); @@ -443,7 +441,7 @@ export class Mjolnir { const rooms = (additionalProtectedRooms?.rooms ?? []); rooms.push(roomId); await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, { rooms: rooms }); - await this.syncLists(config.verboseLogging); + await this.syncLists(this.config.verboseLogging); } public async removeProtectedRoom(roomId: string) { @@ -465,7 +463,7 @@ export class Mjolnir { } private async resyncJoinedRooms(withSync = true) { - if (!config.protectAllJoinedRooms) return; + if (!this.config.protectAllJoinedRooms) return; const joinedRoomIds = (await this.client.getJoinedRooms()).filter(r => r !== this.managementRoomId); const oldRoomIdsSet = new Set(this.protectedJoinedRoomIds); @@ -491,7 +489,7 @@ export class Mjolnir { this.applyUnprotectedRooms(); if (withSync) { - await this.syncLists(config.verboseLogging); + await this.syncLists(this.config.verboseLogging); } } @@ -718,7 +716,7 @@ export class Mjolnir { } public async warnAboutUnprotectedPolicyListRoom(roomId: string) { - if (!config.protectAllJoinedRooms) return; // doesn't matter + if (!this.config.protectAllJoinedRooms) return; // doesn't matter if (this.explicitlyProtectedRoomIds.includes(roomId)) return; // explicitly protected const createEvent = new CreateEvent(await this.client.getRoomStateEvent(roomId, "m.room.create", "")); diff --git a/src/actions/ApplyAcl.ts b/src/actions/ApplyAcl.ts index 13c7f4d..917be55 100644 --- a/src/actions/ApplyAcl.ts +++ b/src/actions/ApplyAcl.ts @@ -18,7 +18,6 @@ import PolicyList from "../models/PolicyList"; import { ServerAcl } from "../models/ServerAcl"; import { RoomUpdateError } from "../models/RoomUpdateError"; import { Mjolnir } from "../Mjolnir"; -import config from "../config"; import { LogLevel, UserID } from "matrix-bot-sdk"; import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache"; @@ -57,7 +56,7 @@ async function _applyServerAcls(lists: PolicyList[], roomIds: string[], mjolnir: mjolnir.logMessage(LogLevel.WARN, "ApplyAcl", `Mjölnir has detected and removed an ACL that would exclude itself. Please check the ACL lists.`); } - if (config.verboseLogging) { + if (mjolnir.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)}`); } @@ -80,7 +79,7 @@ async function _applyServerAcls(lists: PolicyList[], roomIds: string[], mjolnir: // We specifically use sendNotice to avoid having to escape HTML await mjolnir.logMessage(LogLevel.DEBUG, "ApplyAcl", `Applying ACL in ${roomId}`, roomId); - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.sendStateEvent(roomId, "m.room.server_acl", "", finalAcl); } else { await mjolnir.logMessage(LogLevel.WARN, "ApplyAcl", `Tried to apply ACL in ${roomId} but Mjolnir is running in no-op mode`, roomId); diff --git a/src/actions/ApplyBan.ts b/src/actions/ApplyBan.ts index c76d651..e842cc6 100644 --- a/src/actions/ApplyBan.ts +++ b/src/actions/ApplyBan.ts @@ -17,7 +17,6 @@ limitations under the License. import PolicyList from "../models/PolicyList"; import { RoomUpdateError } from "../models/RoomUpdateError"; import { Mjolnir } from "../Mjolnir"; -import config from "../config"; import { LogLevel } from "matrix-bot-sdk"; import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache"; @@ -38,7 +37,7 @@ export async function applyUserBans(lists: PolicyList[], roomIds: string[], mjol let members: { userId: string, membership: string }[]; - if (config.fasterMembershipChecks) { + if (mjolnir.config.fasterMembershipChecks) { const memberIds = await mjolnir.client.getJoinedRoomMembers(roomId); members = memberIds.map(u => { return { userId: u, membership: "join" }; @@ -64,7 +63,7 @@ export async function applyUserBans(lists: PolicyList[], roomIds: string[], mjol // We specifically use sendNotice to avoid having to escape HTML await mjolnir.logMessage(LogLevel.INFO, "ApplyBan", `Banning ${member.userId} in ${roomId} for: ${userRule.reason}`, roomId); - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.banUser(member.userId, roomId, userRule.reason); if (mjolnir.automaticRedactGlobs.find(g => g.test(userRule.reason.toLowerCase()))) { mjolnir.queueRedactUserMessagesIn(member.userId, roomId); diff --git a/src/commands/KickCommand.ts b/src/commands/KickCommand.ts index f841ef4..dde837d 100644 --- a/src/commands/KickCommand.ts +++ b/src/commands/KickCommand.ts @@ -16,7 +16,6 @@ limitations under the License. import { Mjolnir } from "../Mjolnir"; import { LogLevel, MatrixGlob, RichReply } from "matrix-bot-sdk"; -import config from "../config"; // !mjolnir kick [room] [reason] export async function execKickCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { @@ -30,7 +29,7 @@ export async function execKickCommand(roomId: string, event: any, mjolnir: Mjoln parts.pop(); } - if (config.commands.confirmWildcardBan && /[*?]/.test(glob) && !force) { + if (mjolnir.config.commands.confirmWildcardBan && /[*?]/.test(glob) && !force) { let replyMessage = "Wildcard bans require an addition `--force` argument to confirm"; const reply = RichReply.createFor(roomId, event, replyMessage, replyMessage); reply["msgtype"] = "m.notice"; @@ -60,7 +59,7 @@ export async function execKickCommand(roomId: string, event: any, mjolnir: Mjoln if (kickRule.test(victim)) { await mjolnir.logMessage(LogLevel.DEBUG, "KickCommand", `Removing ${victim} in ${protectedRoomId}`, protectedRoomId); - if (!config.noop) { + if (!mjolnir.config.noop) { try { await mjolnir.taskQueue.push(async () => { return mjolnir.client.kickUser(victim, protectedRoomId, reason); diff --git a/src/commands/MakeRoomAdminCommand.ts b/src/commands/MakeRoomAdminCommand.ts index 5165dd3..ea748cf 100644 --- a/src/commands/MakeRoomAdminCommand.ts +++ b/src/commands/MakeRoomAdminCommand.ts @@ -14,14 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import config from "../config"; import { Mjolnir } from "../Mjolnir"; import { RichReply } from "matrix-bot-sdk"; // !mjolnir make admin [] export async function execMakeRoomAdminCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { const isAdmin = await mjolnir.isSynapseAdmin(); - if (!config.admin?.enableMakeRoomAdminCommand || !isAdmin) { + if (!mjolnir.config.admin?.enableMakeRoomAdminCommand || !isAdmin) { const message = "Either the command is disabled or I am not running as homeserver administrator."; const reply = RichReply.createFor(roomId, event, message, message); reply['msgtype'] = "m.notice"; diff --git a/src/commands/UnbanBanCommand.ts b/src/commands/UnbanBanCommand.ts index 8caba37..3ea6ee0 100644 --- a/src/commands/UnbanBanCommand.ts +++ b/src/commands/UnbanBanCommand.ts @@ -18,7 +18,6 @@ import { Mjolnir } from "../Mjolnir"; import PolicyList from "../models/PolicyList"; import { extractRequestError, LogLevel, LogService, MatrixGlob, RichReply } from "matrix-bot-sdk"; import { Recommendation, RULE_ROOM, RULE_SERVER, RULE_USER, USER_RULE_TYPES } from "../models/ListRule"; -import config from "../config"; import { DEFAULT_LIST_EVENT_TYPE } from "./SetDefaultBanListCommand"; interface Arguments { @@ -95,7 +94,7 @@ export async function parseArguments(roomId: string, event: any, mjolnir: Mjolni else if (!ruleType) replyMessage = "Please specify the type as either 'user', 'room', or 'server'"; else if (!entity) replyMessage = "No entity found"; - if (config.commands.confirmWildcardBan && /[*?]/.test(entity) && !force) { + if (mjolnir.config.commands.confirmWildcardBan && /[*?]/.test(entity) && !force) { replyMessage = "Wildcard bans require an additional `--force` argument to confirm"; } @@ -150,7 +149,7 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol if (rule.test(victim)) { await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Unbanning ${victim} in ${protectedRoomId}`, protectedRoomId); - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.unbanUser(victim, protectedRoomId); } else { await mjolnir.logMessage(LogLevel.WARN, "UnbanBanCommand", `Attempted to unban ${victim} in ${protectedRoomId} but Mjolnir is running in no-op mode`, protectedRoomId); @@ -163,7 +162,7 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol if (unbannedSomeone) { await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Syncing lists to ensure no users were accidentally unbanned`); - await mjolnir.syncLists(config.verboseLogging); + await mjolnir.syncLists(mjolnir.config.verboseLogging); } } diff --git a/src/config.ts b/src/config.ts index c286f51..37daa92 100644 --- a/src/config.ts +++ b/src/config.ts @@ -25,7 +25,7 @@ import { MatrixClient } from "matrix-bot-sdk"; // The object is magically generated by external lib `config` // from the file specified by `NODE_ENV`, e.g. production.yaml // or harness.yaml. -interface IConfig { +export interface IConfig { homeserverUrl: string; rawHomeserverUrl: string; accessToken: string; diff --git a/src/health/healthz.ts b/src/health/healthz.ts index 7f00309..c99e8d8 100644 --- a/src/health/healthz.ts +++ b/src/health/healthz.ts @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import config from "../config"; import * as http from "http"; import { LogService } from "matrix-bot-sdk"; +// allowed to use the global configuration since this is only intended to be used by `src/index.ts`. +import config from '../config'; export class Healthz { private static healthCode: number; diff --git a/src/index.ts b/src/index.ts index 0ed071f..4f69218 100644 --- a/src/index.ts +++ b/src/index.ts @@ -56,13 +56,14 @@ if (config.health.healthz.enabled) { patchMatrixClient(); config.RUNTIME.client = client; - bot = await Mjolnir.setupMjolnirFromConfig(client); + bot = await Mjolnir.setupMjolnirFromConfig(client, config); } catch (err) { console.error(`Failed to setup mjolnir from the config ${config.dataPath}: ${err}`); throw err; } try { await bot.start(); + Healthz.isHealthy = true; } catch (err) { console.error(`Mjolnir failed to start: ${err}`); throw err; diff --git a/src/protections/BasicFlooding.ts b/src/protections/BasicFlooding.ts index 323c739..66c82fa 100644 --- a/src/protections/BasicFlooding.ts +++ b/src/protections/BasicFlooding.ts @@ -18,7 +18,6 @@ import { Protection } from "./IProtection"; import { NumberProtectionSetting } from "./ProtectionSettings"; import { Mjolnir } from "../Mjolnir"; import { LogLevel, LogService } from "matrix-bot-sdk"; -import config from "../config"; // if this is exceeded, we'll ban the user for spam and redact their messages export const DEFAULT_MAX_PER_MINUTE = 10; @@ -64,7 +63,7 @@ export class BasicFlooding extends Protection { if (messageCount >= this.settings.maxPerMinute.value) { await mjolnir.logMessage(LogLevel.WARN, "BasicFlooding", `Banning ${event['sender']} in ${roomId} for flooding (${messageCount} messages in the last minute)`, roomId); - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.banUser(event['sender'], roomId, "spam"); } else { await mjolnir.logMessage(LogLevel.WARN, "BasicFlooding", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); @@ -75,7 +74,7 @@ export class BasicFlooding extends Protection { this.recentlyBanned.push(event['sender']); // flag to reduce spam // Redact all the things the user said too - if (!config.noop) { + if (!mjolnir.config.noop) { for (const eventId of forUser.map(e => e.eventId)) { await mjolnir.client.redactEvent(roomId, eventId, "spam"); } diff --git a/src/protections/FirstMessageIsImage.ts b/src/protections/FirstMessageIsImage.ts index 4f3c168..a1cc588 100644 --- a/src/protections/FirstMessageIsImage.ts +++ b/src/protections/FirstMessageIsImage.ts @@ -17,7 +17,6 @@ limitations under the License. import { Protection } from "./IProtection"; import { Mjolnir } from "../Mjolnir"; import { LogLevel, LogService } from "matrix-bot-sdk"; -import config from "../config"; import { isTrueJoinEvent } from "../utils"; export class FirstMessageIsImage extends Protection { @@ -58,7 +57,7 @@ export class FirstMessageIsImage extends Protection { const isMedia = msgtype === 'm.image' || msgtype === 'm.video' || formattedBody.toLowerCase().includes('= this.settings.maxPer.value) { await mjolnir.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Setting ${roomId} to invite-only as more than ${this.settings.maxPer.value} users have joined over the last ${this.settings.timescaleMinutes.value} minutes (since ${this.joinBuckets[roomId].lastBucketStart})`, roomId); - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.sendStateEvent(roomId, "m.room.join_rules", "", {"join_rule": "invite"}) } else { await mjolnir.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Tried to set ${roomId} to invite-only, but Mjolnir is running in no-op mode`, roomId); diff --git a/src/protections/MessageIsMedia.ts b/src/protections/MessageIsMedia.ts index a33cfbb..6d4c759 100644 --- a/src/protections/MessageIsMedia.ts +++ b/src/protections/MessageIsMedia.ts @@ -17,7 +17,6 @@ limitations under the License. import { Protection } from "./IProtection"; import { Mjolnir } from "../Mjolnir"; import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk"; -import config from "../config"; export class MessageIsMedia extends Protection { @@ -43,7 +42,7 @@ export class MessageIsMedia extends Protection { if (isMedia) { await mjolnir.logMessage(LogLevel.WARN, "MessageIsMedia", `Redacting event from ${event['sender']} for posting an image/video. ${Permalinks.forEvent(roomId, event['event_id'], [new UserID(await mjolnir.client.getUserId()).domain])}`); // Redact the event - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.redactEvent(roomId, event['event_id'], "Images/videos are not permitted here"); } else { await mjolnir.logMessage(LogLevel.WARN, "MessageIsMedia", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); diff --git a/src/protections/MessageIsVoice.ts b/src/protections/MessageIsVoice.ts index 490e1bc..3b9382f 100644 --- a/src/protections/MessageIsVoice.ts +++ b/src/protections/MessageIsVoice.ts @@ -17,7 +17,6 @@ limitations under the License. import { Protection } from "./IProtection"; import { Mjolnir } from "../Mjolnir"; import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk"; -import config from "../config"; export class MessageIsVoice extends Protection { @@ -40,7 +39,7 @@ export class MessageIsVoice extends Protection { if (event['content']['org.matrix.msc3245.voice'] === undefined) return; await mjolnir.logMessage(LogLevel.INFO, "MessageIsVoice", `Redacting event from ${event['sender']} for posting a voice message. ${Permalinks.forEvent(roomId, event['event_id'], [new UserID(await mjolnir.client.getUserId()).domain])}`); // Redact the event - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.redactEvent(roomId, event['event_id'], "Voice messages are not permitted here"); } else { await mjolnir.logMessage(LogLevel.WARN, "MessageIsVoice", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); diff --git a/src/protections/TrustedReporters.ts b/src/protections/TrustedReporters.ts index 7f831f7..99c1a18 100644 --- a/src/protections/TrustedReporters.ts +++ b/src/protections/TrustedReporters.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import config from "../config"; import { Protection } from "./IProtection"; import { MXIDListProtectionSetting, NumberProtectionSetting } from "./ProtectionSettings"; import { Mjolnir } from "../Mjolnir"; @@ -83,7 +82,7 @@ export class TrustedReporters extends Protection { if (met.length > 0) { - await mjolnir.client.sendMessage(config.managementRoom, { + await mjolnir.client.sendMessage(mjolnir.config.managementRoom, { msgtype: "m.notice", body: `message ${event.id} reported by ${[...reporters].join(', ')}. ` + `actions: ${met.join(', ')}` diff --git a/src/protections/WordList.ts b/src/protections/WordList.ts index b089213..9decad1 100644 --- a/src/protections/WordList.ts +++ b/src/protections/WordList.ts @@ -17,7 +17,6 @@ limitations under the License. import { Protection } from "./IProtection"; import { Mjolnir } from "../Mjolnir"; import { LogLevel, LogService } from "matrix-bot-sdk"; -import config from "../config"; import { isTrueJoinEvent } from "../utils"; export class WordList extends Protection { @@ -25,15 +24,10 @@ export class WordList extends Protection { settings = {}; private justJoined: { [roomId: string]: { [username: string]: Date} } = {}; - private badWords: RegExp; + private badWords?: RegExp; constructor() { super(); - // Create a mega-regex from all the tiny baby regexs - this.badWords = new RegExp( - "(" + config.protections.wordlist.words.join(")|(") + ")", - "i" - ) } public get name(): string { @@ -47,7 +41,7 @@ export class WordList extends Protection { public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise { const content = event['content'] || {}; - const minsBeforeTrusting = config.protections.wordlist.minutesBeforeTrusting; + const minsBeforeTrusting = mjolnir.config.protections.wordlist.minutesBeforeTrusting; if (minsBeforeTrusting > 0) { if (!this.justJoined[roomId]) this.justJoined[roomId] = {}; @@ -89,19 +83,29 @@ export class WordList extends Protection { return } } - + if (this.badWords === null) { + // Create a mega-regex from all the tiny baby regexs + try { + this.badWords = new RegExp( + "(" + mjolnir.config.protections.wordlist.words.join(")|(") + ")", + "i" + ); + } catch (ex) { + await mjolnir.logMessage(LogLevel.ERROR, "WordList", `Could not produce a regex from the word list:\n${ex}.`) + } + } // Perform the test - if (message && this.badWords.test(message)) { + if (message && this.badWords!.test(message)) { await mjolnir.logMessage(LogLevel.WARN, "WordList", `Banning ${event['sender']} for word list violation in ${roomId}.`); - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.banUser(event['sender'], roomId, "Word list violation"); } else { await mjolnir.logMessage(LogLevel.WARN, "WordList", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); } // Redact the event - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.redactEvent(roomId, event['event_id'], "spam"); } else { await mjolnir.logMessage(LogLevel.WARN, "WordList", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); diff --git a/src/queues/UnlistedUserRedactionQueue.ts b/src/queues/UnlistedUserRedactionQueue.ts index f774459..91ac878 100644 --- a/src/queues/UnlistedUserRedactionQueue.ts +++ b/src/queues/UnlistedUserRedactionQueue.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ import { extractRequestError, LogLevel, LogService, Permalinks } from "matrix-bot-sdk"; -import config from "../config"; import { Mjolnir } from "../Mjolnir"; /** @@ -43,7 +42,7 @@ export class UnlistedUserRedactionQueue { const permalink = Permalinks.forEvent(roomId, event['event_id']); try { LogService.info("AutomaticRedactionQueue", `Redacting event because the user is listed as bad: ${permalink}`) - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.redactEvent(roomId, event['event_id']); } else { await mjolnir.logMessage(LogLevel.WARN, "AutomaticRedactionQueue", `Tried to redact ${permalink} but Mjolnir is running in no-op mode`); diff --git a/src/report/ReportManager.ts b/src/report/ReportManager.ts index 5c96d98..c75e636 100644 --- a/src/report/ReportManager.ts +++ b/src/report/ReportManager.ts @@ -20,8 +20,6 @@ import { htmlToText } from "html-to-text"; import { htmlEscape } from "../utils"; import { JSDOM } from 'jsdom'; import { EventEmitter } from 'events'; - -import config from "../config"; import { Mjolnir } from "../Mjolnir"; /// Regexp, used to extract the action label from an action reaction @@ -115,7 +113,7 @@ export class ReportManager extends EventEmitter { */ public async handleServerAbuseReport({ roomId, reporterId, event, reason }: { roomId: string, reporterId: string, event: any, reason?: string }) { this.emit("report.new", { roomId: roomId, reporterId: reporterId, event: event, reason: reason }); - if (config.displayReports) { + if (this.mjolnir.config.displayReports) { return this.displayManager.displayReportAndUI({ kind: Kind.SERVER_ABUSE_REPORT, event, reporterId, reason, moderationRoomId: this.mjolnir.managementRoomId }); } } diff --git a/src/utils.ts b/src/utils.ts index d30609f..7d31538 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -28,7 +28,6 @@ import { setRequestFn, } from "matrix-bot-sdk"; import { Mjolnir } from "./Mjolnir"; -import config from "./config"; import { ClientRequest, IncomingMessage } from "http"; import { default as parseDuration } from "parse-duration"; @@ -78,7 +77,7 @@ export async function redactUserMessagesIn(mjolnir: Mjolnir, userIdOrGlob: strin await getMessagesByUserIn(mjolnir.client, userIdOrGlob, targetRoomId, limit, async (eventsToRedact) => { for (const victimEvent of eventsToRedact) { await mjolnir.logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Redacting ${victimEvent['event_id']} in ${targetRoomId}`, targetRoomId); - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.redactEvent(targetRoomId, victimEvent['event_id']); } else { await mjolnir.logMessage(LogLevel.WARN, "utils#redactUserMessagesIn", `Tried to redact ${victimEvent['event_id']} in ${targetRoomId} but Mjolnir is running in no-op mode`, targetRoomId); diff --git a/src/webapis/WebAPIs.ts b/src/webapis/WebAPIs.ts index 863c6ae..e8fae70 100644 --- a/src/webapis/WebAPIs.ts +++ b/src/webapis/WebAPIs.ts @@ -15,13 +15,11 @@ limitations under the License. */ import { Server } from "http"; - import * as express from "express"; import { LogService, MatrixClient } from "matrix-bot-sdk"; - -import config from "../config"; import RuleServer from "../models/RuleServer"; import { ReportManager } from "../report/ReportManager"; +import { IConfig } from "../config"; /** @@ -35,7 +33,7 @@ export class WebAPIs { private webController: express.Express = express(); private httpServer?: Server; - constructor(private reportManager: ReportManager, private readonly ruleServer: RuleServer|null) { + constructor(private reportManager: ReportManager, private readonly config: IConfig, private readonly ruleServer: RuleServer|null) { // Setup JSON parsing. this.webController.use(express.json()); } @@ -44,14 +42,14 @@ export class WebAPIs { * Start accepting requests to the Web API. */ public async start() { - if (!config.web.enabled) { + if (!this.config.web.enabled) { return; } - this.httpServer = this.webController.listen(config.web.port, config.web.address); + this.httpServer = this.webController.listen(this.config.web.port, this.config.web.address); - // Configure /report API. - if (config.web.abuseReporting.enabled) { - console.log(`Configuring ${API_PREFIX}/report/:room_id/:event_id...`); + // configure /report API. + if (this.config.web.abuseReporting.enabled) { + console.log(`configuring ${API_PREFIX}/report/:room_id/:event_id...`); this.webController.options(`${API_PREFIX}/report/:room_id/:event_id`, async (request, response) => { // reply with CORS options response.header("Access-Control-Allow-Origin", "*"); @@ -68,15 +66,15 @@ export class WebAPIs { response.header("Access-Control-Allow-Methods", "POST, OPTIONS"); await this.handleReport({ request, response, roomId: request.params.room_id, eventId: request.params.event_id }) }); - console.log(`Configuring ${API_PREFIX}/report/:room_id/:event_id... DONE`); + console.log(`configuring ${API_PREFIX}/report/:room_id/:event_id... DONE`); } - // Configure ruleServer API. + // configure ruleServer API. // FIXME: Doesn't this need some kind of access control? // See https://github.com/matrix-org/mjolnir/issues/139#issuecomment-1012221479. - if (config.web.ruleServer?.enabled) { + if (this.config.web.ruleServer?.enabled) { const updatesUrl = `${API_PREFIX}/ruleserver/updates`; - LogService.info("WebAPIs", `Configuring ${updatesUrl}...`); + LogService.info("WebAPIs", `configuring ${updatesUrl}...`); if (!this.ruleServer) { throw new Error("The rule server to use has not been configured for the WebAPIs."); } @@ -84,7 +82,7 @@ export class WebAPIs { this.webController.get(updatesUrl, async (request, response) => { await this.handleRuleServerUpdate(ruleServer, { request, response, since: request.query.since as string}); }); - LogService.info("WebAPIs", `Configuring ${updatesUrl}... DONE`); + LogService.info("WebAPIs", `configuring ${updatesUrl}... DONE`); } } @@ -163,7 +161,7 @@ export class WebAPIs { // so we are not extending the abilities of Mjölnir // 3. We are avoiding the use of the Synapse Admin API to ensure that // this feature can work with all homeservers, not just Synapse. - let reporterClient = new MatrixClient(config.rawHomeserverUrl, accessToken); + let reporterClient = new MatrixClient(this.config.rawHomeserverUrl, accessToken); reporterClient.start = () => { throw new Error("We MUST NEVER call start on the reporter client"); }; diff --git a/test/commands/UnbanBanCommandTest.ts b/test/commands/UnbanBanCommandTest.ts index 34d8d90..24b9bc8 100644 --- a/test/commands/UnbanBanCommandTest.ts +++ b/test/commands/UnbanBanCommandTest.ts @@ -18,6 +18,7 @@ import * as expect from "expect"; import { Mjolnir } from "../../src/Mjolnir"; import { DEFAULT_LIST_EVENT_TYPE } from "../../src/commands/SetDefaultBanListCommand"; import { parseArguments } from "../../src/commands/UnbanBanCommand"; +import config from "../../src/config"; import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../../src/models/ListRule"; function createTestMjolnir(defaultShortcode: string|null = null): Mjolnir { @@ -30,7 +31,10 @@ function createTestMjolnir(defaultShortcode: string|null = null): Mjolnir { throw new Error(`Unknown event type ${eventType}, expected ${DEFAULT_LIST_EVENT_TYPE}`); }, }; - return {client}; + return { + client, + config, + }; } function createFakeEvent(command: string): any { diff --git a/test/integration/banListTest.ts b/test/integration/banListTest.ts index eb3984b..ddd94ef 100644 --- a/test/integration/banListTest.ts +++ b/test/integration/banListTest.ts @@ -1,12 +1,11 @@ import { strict as assert } from "assert"; - -import config from "../../src/config"; import { newTestUser } from "./clientHelper"; import { LogService, MatrixClient, Permalinks, UserID } from "matrix-bot-sdk"; import PolicyList, { ChangeType, ListRuleChange } from "../../src/models/PolicyList"; import { ServerAcl } from "../../src/models/ServerAcl"; import { getFirstReaction } from "./commands/commandUtils"; import { getMessagesByUserIn } from "../../src/utils"; +import { Mjolnir } from "../../src/Mjolnir"; import { ALL_RULE_TYPES, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES } from "../../src/models/ListRule"; /** @@ -30,33 +29,33 @@ async function createPolicyRule(client: MatrixClient, policyRoomId: string, poli describe("Test: Updating the PolicyList", function() { it("Calculates what has changed correctly.", async function() { this.timeout(10000); - const mjolnir = config.RUNTIME.client! + const mjolnir: Mjolnir = this.mjolnir! const moderator = await newTestUser({ name: { contains: "moderator" } }); - const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()] }); - const banList = new PolicyList(banListId, banListId, mjolnir); - mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100); + const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] }); + const banList = new PolicyList(banListId, banListId, mjolnir.client); + mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100); assert.equal(banList.allRules.length, 0); // Test adding a new rule - await createPolicyRule(mjolnir, banListId, RULE_USER, '@added:localhost:9999', ''); + await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@added:localhost:9999', ''); let changes: ListRuleChange[] = await banList.updateList(); assert.equal(changes.length, 1, 'There should only be one change'); assert.equal(changes[0].changeType, ChangeType.Added); - assert.equal(changes[0].sender, await mjolnir.getUserId()); + assert.equal(changes[0].sender, await mjolnir.client.getUserId()); assert.equal(banList.userRules.length, 1); assert.equal(banList.allRules.length, 1); // Test modifiying a rule - let originalEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@modified:localhost:9999', ''); + let originalEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@modified:localhost:9999', ''); await banList.updateList(); - let modifyingEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@modified:localhost:9999', 'modified reason'); + let modifyingEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@modified:localhost:9999', 'modified reason'); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Modified); assert.equal(changes[0].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule'); assert.equal(changes[0].event['event_id'], modifyingEventId); - let modifyingAgainEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@modified:localhost:9999', 'modified again'); + let modifyingAgainEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@modified:localhost:9999', 'modified again'); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Modified); @@ -65,10 +64,10 @@ describe("Test: Updating the PolicyList", function() { assert.equal(banList.userRules.length, 2, 'There should be two rules, one for @modified:localhost:9999 and one for @added:localhost:9999'); // Test redacting a rule - const redactThis = await createPolicyRule(mjolnir, banListId, RULE_USER, '@redacted:localhost:9999', ''); + const redactThis = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@redacted:localhost:9999', ''); await banList.updateList(); assert.equal(banList.userRules.filter(r => r.entity === '@redacted:localhost:9999').length, 1); - await mjolnir.redactEvent(banListId, redactThis); + await mjolnir.client.redactEvent(banListId, redactThis); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Removed); @@ -80,10 +79,10 @@ describe("Test: Updating the PolicyList", function() { // Test soft redaction of a rule const softRedactedEntity = '@softredacted:localhost:9999' - await createPolicyRule(mjolnir, banListId, RULE_USER, softRedactedEntity, ''); + await createPolicyRule(mjolnir.client, banListId, RULE_USER, softRedactedEntity, ''); await banList.updateList(); assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 1); - await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {}); + await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {}); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Removed); @@ -93,25 +92,25 @@ describe("Test: Updating the PolicyList", function() { assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 0, 'The rule should have been removed'); // Now test a double soft redaction just to make sure stuff doesn't explode - await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {}); + await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {}); changes = await banList.updateList(); assert.equal(changes.length, 0, "It shouldn't detect a double soft redaction as a change, it should be seen as adding an invalid rule."); assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 0, 'The rule should have been removed'); // Test that different (old) rule types will be modelled as the latest event type. - originalEventId = await createPolicyRule(mjolnir, banListId, 'org.matrix.mjolnir.rule.user', '@old:localhost:9999', ''); + originalEventId = await createPolicyRule(mjolnir.client, banListId, 'org.matrix.mjolnir.rule.user', '@old:localhost:9999', ''); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Added); assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1); - modifyingEventId = await createPolicyRule(mjolnir, banListId, 'm.room.rule.user', '@old:localhost:9999', 'modified reason'); + modifyingEventId = await createPolicyRule(mjolnir.client, banListId, 'm.room.rule.user', '@old:localhost:9999', 'modified reason'); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Modified); assert.equal(changes[0].event['event_id'], modifyingEventId); assert.equal(changes[0].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule'); assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1); - modifyingAgainEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@old:localhost:9999', 'changes again'); + modifyingAgainEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@old:localhost:9999', 'changes again'); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Modified); @@ -121,19 +120,19 @@ describe("Test: Updating the PolicyList", function() { }) it("Will remove rules with old types when they are 'soft redacted' with a different but more recent event type.", async function() { this.timeout(3000); - const mjolnir = config.RUNTIME.client! - const moderator = await newTestUser({ name: { contains: "moderator" } }); - const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()] }); - const banList = new PolicyList(banListId, banListId, mjolnir); - mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100); + const mjolnir: Mjolnir = this.mjolnir! + const moderator = await newTestUser({ name: { contains: "moderator" }} ); + const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] }); + const banList = new PolicyList(banListId, banListId, mjolnir.client); + mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100); const entity = '@old:localhost:9999'; - let originalEventId = await createPolicyRule(mjolnir, banListId, 'm.room.rule.user', entity, ''); + let originalEventId = await createPolicyRule(mjolnir.client, banListId, 'm.room.rule.user', entity, ''); let changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Added); assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'There should be a rule stored that we just added...') - let softRedactingEventId = await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {}); + let softRedactingEventId = await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {}); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Removed); @@ -143,19 +142,19 @@ describe("Test: Updating the PolicyList", function() { }) it("A rule of the most recent type won't be deleted when an old rule is deleted for the same entity.", async function() { this.timeout(3000); - const mjolnir = config.RUNTIME.client! + const mjolnir: Mjolnir = this.mjolnir! const moderator = await newTestUser({ name: { contains: "moderator" } }); - const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()] }); - const banList = new PolicyList(banListId, banListId, mjolnir); - mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100); + const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] }); + const banList = new PolicyList(banListId, banListId, mjolnir.client); + mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100); const entity = '@old:localhost:9999'; - let originalEventId = await createPolicyRule(mjolnir, banListId, 'm.room.rule.user', entity, ''); + let originalEventId = await createPolicyRule(mjolnir.client, banListId, 'm.room.rule.user', entity, ''); let changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Added); assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'There should be a rule stored that we just added...') - let updatedEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, entity, ''); + let updatedEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, entity, ''); changes = await banList.updateList(); // If in the future you change this and it fails, it's really subjective whether this constitutes a modification, since the only thing that has changed // is the rule type. The actual content is identical. @@ -166,13 +165,13 @@ describe("Test: Updating the PolicyList", function() { assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'Only the latest version of the rule gets returned.'); // Now we delete the old version of the rule without consequence. - await mjolnir.sendStateEvent(banListId, 'm.room.rule.user', `rule:${entity}`, {}); + await mjolnir.client.sendStateEvent(banListId, 'm.room.rule.user', `rule:${entity}`, {}); changes = await banList.updateList(); assert.equal(changes.length, 0); assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'The rule should still be active.'); // And we can still delete the new version of the rule. - let softRedactingEventId = await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {}); + let softRedactingEventId = await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {}); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Removed); @@ -180,12 +179,12 @@ describe("Test: Updating the PolicyList", function() { assert.equal(changes[0].previousState['event_id'], updatedEventId, 'There should be a previous state event for a modified rule'); assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 0, 'The rule should no longer be stored.'); }) - it('Test: PolicyList Supports all entity types.', async function() { - const mjolnir = config.RUNTIME.client! - const banListId = await mjolnir.createRoom(); - const banList = new PolicyList(banListId, banListId, mjolnir); + it('Test: PolicyList Supports all entity types.', async function () { + const mjolnir: Mjolnir = this.mjolnir! + const banListId = await mjolnir.client.createRoom(); + const banList = new PolicyList(banListId, banListId, mjolnir.client); for (let i = 0; i < ALL_RULE_TYPES.length; i++) { - await createPolicyRule(mjolnir, banListId, ALL_RULE_TYPES[i], `*${i}*`, ''); + await createPolicyRule(mjolnir.client, banListId, ALL_RULE_TYPES[i], `*${i}*`, ''); } let changes: ListRuleChange[] = await banList.updateList(); assert.equal(changes.length, ALL_RULE_TYPES.length); @@ -193,16 +192,16 @@ describe("Test: Updating the PolicyList", function() { }) }); -describe('Test: We do not respond to recommendations other than m.ban in the banlist', function() { +describe('Test: We do not respond to recommendations other than m.ban in the PolicyList', function() { it('Will not respond to a rule that has a different recommendation to m.ban (or the unstable equivalent).', async function() { - const mjolnir = config.RUNTIME.client! - const banListId = await mjolnir.createRoom(); - const banList = new PolicyList(banListId, banListId, mjolnir); - await createPolicyRule(mjolnir, banListId, RULE_SERVER, 'exmaple.org', '', { recommendation: 'something that is not m.ban' }); + const mjolnir: Mjolnir = this.mjolnir! + const banListId = await mjolnir.client.createRoom(); + const banList = new PolicyList(banListId, banListId, mjolnir.client); + await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, 'exmaple.org', '', { recommendation: 'something that is not m.ban' }); let changes: ListRuleChange[] = await banList.updateList(); assert.equal(changes.length, 1, 'There should only be one change'); assert.equal(changes[0].changeType, ChangeType.Added); - assert.equal(changes[0].sender, await mjolnir.getUserId()); + assert.equal(changes[0].sender, await mjolnir.client.getUserId()); // We really don't want things that aren't m.ban to end up being accessible in these APIs. assert.equal(banList.serverRules.length, 0, `We should have an empty serverRules, got ${JSON.stringify(banList.serverRules)}`); assert.equal(banList.allRules.length, 0, `We should have an empty allRules, got ${JSON.stringify(banList.allRules)}`); @@ -211,13 +210,13 @@ describe('Test: We do not respond to recommendations other than m.ban in the ban describe('Test: We will not be able to ban ourselves via ACL.', function() { it('We do not ban ourselves when we put ourselves into the policy list.', async function() { - const mjolnir = config.RUNTIME.client! - const serverName = new UserID(await mjolnir.getUserId()).domain; - const banListId = await mjolnir.createRoom(); - const banList = new PolicyList(banListId, banListId, mjolnir); - await createPolicyRule(mjolnir, banListId, RULE_SERVER, serverName, ''); - await createPolicyRule(mjolnir, banListId, RULE_SERVER, 'evil.com', ''); - await createPolicyRule(mjolnir, banListId, RULE_SERVER, '*', ''); + const mjolnir: Mjolnir = this.mjolnir + const serverName = new UserID(await mjolnir.client.getUserId()).domain; + const banListId = await mjolnir.client.createRoom(); + const banList = new PolicyList(banListId, banListId, mjolnir.client); + await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, serverName, ''); + await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, 'evil.com', ''); + await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, '*', ''); // We should still intern the matching rules rule. let changes: ListRuleChange[] = await banList.updateList(); assert.equal(banList.serverRules.length, 3); @@ -232,34 +231,34 @@ describe('Test: We will not be able to ban ourselves via ACL.', function() { describe('Test: ACL updates will batch when rules are added in succession.', function() { it('Will batch ACL updates if we spam rules into a PolicyList', async function() { - const mjolnir = config.RUNTIME.client! - const serverName: string = new UserID(await mjolnir.getUserId()).domain + const mjolnir: Mjolnir = this.mjolnir! + const serverName: string = new UserID(await mjolnir.client.getUserId()).domain const moderator = await newTestUser({ name: { contains: "moderator" } }); - moderator.joinRoom(this.mjolnir.managementRoomId); - const mjolnirId = await mjolnir.getUserId(); + moderator.joinRoom(this.mjolnir.client.managementRoomId); + const mjolnirId = await mjolnir.client.getUserId(); // Setup some protected rooms so we can check their ACL state later. const protectedRooms: string[] = []; for (let i = 0; i < 10; i++) { const room = await moderator.createRoom({ invite: [mjolnirId] }); - await mjolnir.joinRoom(room); + await mjolnir.client.joinRoom(room); await moderator.setUserPowerLevel(mjolnirId, room, 100); - await this.mjolnir!.addProtectedRoom(room); + await mjolnir.addProtectedRoom(room); protectedRooms.push(room); } // If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point. - await this.mjolnir!.syncLists(); + await mjolnir.syncLists(); await Promise.all(protectedRooms.map(async room => { // We're going to need timeline pagination I'm afraid. - const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", ""); + const roomAcl = await mjolnir.client.getRoomStateEvent(room, "m.room.server_acl", ""); assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.'); })); // Flood the watched list with banned servers, which should prompt Mjolnir to update server ACL in protected rooms. const banListId = await moderator.createRoom({ invite: [mjolnirId] }); - mjolnir.joinRoom(banListId); - this.mjolnir!.watchList(Permalinks.forRoom(banListId)); + mjolnir.client.joinRoom(banListId); + mjolnir.watchList(Permalinks.forRoom(banListId)); const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*"); const evilServerCount = 200; for (let i = 0; i < evilServerCount; i++) { @@ -271,7 +270,7 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun } // We do this because it should force us to wait until all the ACL events have been applied. // Even if that does mean the last few events will not go through batching... - await this.mjolnir!.syncLists(); + await mjolnir.syncLists(); // At this point we check that the state within Mjolnir is internally consistent, this is just because debugging the following // is a pita. @@ -280,12 +279,12 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun // Check each of the protected rooms for ACL events and make sure they were batched and are correct. await Promise.all(protectedRooms.map(async room => { - const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", ""); + const roomAcl = await mjolnir.client.getRoomStateEvent(room, "m.room.server_acl", ""); if (!acl.matches(roomAcl)) { assert.fail(`Room ${room} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`) } let aclEventCount = 0; - await getMessagesByUserIn(mjolnir, mjolnirId, room, 100, events => { + await getMessagesByUserIn(mjolnir.client, mjolnirId, room, 100, events => { events.forEach(event => event.type === 'm.room.server_acl' ? aclEventCount += 1 : null); }); LogService.debug('PolicyListTest', `aclEventCount: ${aclEventCount}`); @@ -301,29 +300,29 @@ describe('Test: unbaning entities via the PolicyList.', function() { afterEach(function() { this.moderator?.stop(); }); it('Will remove rules that have legacy types', async function() { this.timeout(20000) - const mjolnir = config.RUNTIME.client! - const serverName: string = new UserID(await mjolnir.getUserId()).domain + const mjolnir: Mjolnir = this.mjolnir! + const serverName: string = new UserID(await mjolnir.client.getUserId()).domain const moderator = await newTestUser({ name: { contains: "moderator" } }); this.moderator = moderator; - moderator.joinRoom(this.mjolnir.managementRoomId); - const mjolnirId = await mjolnir.getUserId(); + moderator.joinRoom(mjolnir.managementRoomId); + const mjolnirId = await mjolnir.client.getUserId(); // We'll make 1 protected room to test ACLs in. const protectedRoom = await moderator.createRoom({ invite: [mjolnirId] }); - await mjolnir.joinRoom(protectedRoom); + await mjolnir.client.joinRoom(protectedRoom); await moderator.setUserPowerLevel(mjolnirId, protectedRoom, 100); - await this.mjolnir!.addProtectedRoom(protectedRoom); + await mjolnir.addProtectedRoom(protectedRoom); // If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point. - await this.mjolnir!.syncLists(); - const roomAcl = await mjolnir.getRoomStateEvent(protectedRoom, "m.room.server_acl", ""); + await mjolnir.syncLists(); + const roomAcl = await mjolnir.client.getRoomStateEvent(protectedRoom, "m.room.server_acl", ""); assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.'); // Create some legacy rules on a PolicyList. const banListId = await moderator.createRoom({ invite: [mjolnirId] }); - await moderator.setUserPowerLevel(await mjolnir.getUserId(), banListId, 100); + await moderator.setUserPowerLevel(await mjolnir.client.getUserId(), banListId, 100); await moderator.sendStateEvent(banListId, 'org.matrix.mjolnir.shortcode', '', { shortcode: "unban-test" }); - await mjolnir.joinRoom(banListId); + await mjolnir.client.joinRoom(banListId); this.mjolnir!.watchList(Permalinks.forRoom(banListId)); // we use this to compare changes. const banList = new PolicyList(banListId, banListId, moderator); @@ -342,7 +341,7 @@ describe('Test: unbaning entities via the PolicyList.', function() { // Check that we have setup our test properly and therefore evil.com is banned. const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*").denyServer(olderBadServer).denyServer(newerBadServer); - const protectedAcl = await mjolnir.getRoomStateEvent(protectedRoom, "m.room.server_acl", ""); + const protectedAcl = await mjolnir.client.getRoomStateEvent(protectedRoom, "m.room.server_acl", ""); if (!acl.matches(protectedAcl)) { assert.fail(`Room ${protectedRoom} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`); } @@ -364,7 +363,7 @@ describe('Test: unbaning entities via the PolicyList.', function() { // Confirm that the server is unbanned. await banList.updateList(); assert.equal(banList.allRules.length, 0); - const aclAfter = await mjolnir.getRoomStateEvent(protectedRoom, "m.room.server_acl", ""); + const aclAfter = await mjolnir.client.getRoomStateEvent(protectedRoom, "m.room.server_acl", ""); assert.equal(aclAfter.deny.length, 0, 'Should be no servers denied anymore'); }) }) @@ -372,35 +371,35 @@ describe('Test: unbaning entities via the PolicyList.', function() { describe('Test: should apply bans to the most recently active rooms first', function() { it('Applies bans to the most recently active rooms first', async function() { this.timeout(180000) - const mjolnir = config.RUNTIME.client! - const serverName: string = new UserID(await mjolnir.getUserId()).domain + const mjolnir: Mjolnir = this.mjolnir! + const serverName: string = new UserID(await mjolnir.client.getUserId()).domain const moderator = await newTestUser({ name: { contains: "moderator" } }); - moderator.joinRoom(this.mjolnir.managementRoomId); - const mjolnirId = await mjolnir.getUserId(); + moderator.joinRoom(mjolnir.managementRoomId); + const mjolnirId = await mjolnir.client.getUserId(); // Setup some protected rooms so we can check their ACL state later. const protectedRooms: string[] = []; for (let i = 0; i < 10; i++) { const room = await moderator.createRoom({ invite: [mjolnirId] }); - await mjolnir.joinRoom(room); + await mjolnir.client.joinRoom(room); await moderator.setUserPowerLevel(mjolnirId, room, 100); - await this.mjolnir!.addProtectedRoom(room); + await mjolnir.addProtectedRoom(room); protectedRooms.push(room); } // If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point. - await this.mjolnir!.syncLists(); + await mjolnir.syncLists(); await Promise.all(protectedRooms.map(async room => { - const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", "").catch(e => e.statusCode === 404 ? { deny: [] } : Promise.reject(e)); + const roomAcl = await mjolnir.client.getRoomStateEvent(room, "m.room.server_acl", "").catch(e => e.statusCode === 404 ? { deny: [] } : Promise.reject(e)); assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.'); })); // Flood the watched list with banned servers, which should prompt Mjolnir to update server ACL in protected rooms. const banListId = await moderator.createRoom({ invite: [mjolnirId] }); - mjolnir.joinRoom(banListId); - this.mjolnir!.watchList(Permalinks.forRoom(banListId)); + mjolnir.client.joinRoom(banListId); + mjolnir.watchList(Permalinks.forRoom(banListId)); - await this.mjolnir!.syncLists(); + await mjolnir.syncLists(); // shuffle protected rooms https://stackoverflow.com/a/12646864, we do this so we can create activity "randomly" in them. for (let i = protectedRooms.length - 1; i > 0; i--) { @@ -409,13 +408,13 @@ describe('Test: should apply bans to the most recently active rooms first', func } // create some activity in the same order. for (const roomId of protectedRooms.slice().reverse()) { - await mjolnir.sendMessage(roomId, { body: `activity`, msgtype: 'm.text' }); + await mjolnir.client.sendMessage(roomId, { body: `activity`, msgtype: 'm.text' }); await new Promise(resolve => setTimeout(resolve, 100)); } // check the rooms are in the expected order for (let i = 0; i < protectedRooms.length; i++) { - assert.equal(this.mjolnir!.protectedRoomsByActivity()[i], protectedRooms[i]); + assert.equal(mjolnir.protectedRoomsByActivity()[i], protectedRooms[i]); } const badServer = `evil.com`; @@ -424,10 +423,10 @@ describe('Test: should apply bans to the most recently active rooms first', func await createPolicyRule(moderator, banListId, RULE_SERVER, badServer, `Rule ${badServer}`); // Wait until all the ACL events have been applied. - await this.mjolnir!.syncLists(); + await mjolnir.syncLists(); for (let i = 0; i < protectedRooms.length; i++) { - assert.equal(this.mjolnir!.protectedRoomsByActivity()[i], protectedRooms.at(-i - 1)); + assert.equal(mjolnir.protectedRoomsByActivity()[i], protectedRooms.at(-i - 1)); } // Check that the most recently active rooms got the ACL update first. @@ -435,7 +434,7 @@ describe('Test: should apply bans to the most recently active rooms first', func for (const roomId of protectedRooms) { let roomAclEvent: null | any; // Can't be the best way to get the whole event, but ok. - await getMessagesByUserIn(mjolnir, mjolnirId, roomId, 1, events => roomAclEvent = events[0]); + await getMessagesByUserIn(mjolnir.client, mjolnirId, roomId, 1, events => roomAclEvent = events[0]); const roomAcl = roomAclEvent!.content; if (!acl.matches(roomAcl)) { assert.fail(`Room ${roomId} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`) diff --git a/test/integration/clientHelper.ts b/test/integration/clientHelper.ts index 7b38900..4356c9b 100644 --- a/test/integration/clientHelper.ts +++ b/test/integration/clientHelper.ts @@ -1,6 +1,6 @@ import { HmacSHA1 } from "crypto-js"; import { getRequestFn, LogService, MatrixClient, MemoryStorageProvider, PantalaimonClient } from "matrix-bot-sdk"; -import config from "../../src/config"; +import config from '../../src/config'; const REGISTRATION_ATTEMPTS = 10; const REGISTRATION_RETRY_BASE_DELAY_MS = 100; diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index a81d6f6..f18717f 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -14,7 +14,7 @@ export const mochaHooks = { // Sometimes it takes a little longer to register users. this.timeout(10000) this.managementRoomAlias = config.managementRoom; - this.mjolnir = await makeMjolnir(); + this.mjolnir = await makeMjolnir(config); config.RUNTIME.client = this.mjolnir.client; await Promise.all([ this.mjolnir.client.setAccountData('org.matrix.mjolnir.protected_rooms', { rooms: [] }), diff --git a/test/integration/manualLaunchScript.ts b/test/integration/manualLaunchScript.ts index aff4440..26062c2 100644 --- a/test/integration/manualLaunchScript.ts +++ b/test/integration/manualLaunchScript.ts @@ -3,8 +3,9 @@ */ import { makeMjolnir } from "./mjolnirSetupUtils"; +import config from '../../src/config'; (async () => { - let mjolnir = await makeMjolnir(); + let mjolnir = await makeMjolnir(config); await mjolnir.start(); })(); diff --git a/test/integration/mjolnirSetupUtils.ts b/test/integration/mjolnirSetupUtils.ts index 2a4939b..b372f4b 100644 --- a/test/integration/mjolnirSetupUtils.ts +++ b/test/integration/mjolnirSetupUtils.ts @@ -22,9 +22,9 @@ import { RichConsoleLogger } from "matrix-bot-sdk"; import { Mjolnir} from '../../src/Mjolnir'; -import config from "../../src/config"; import { overrideRatelimitForUser, registerUser } from "./clientHelper"; import { patchMatrixClient } from "../../src/utils"; +import { IConfig } from "../../src/config"; /** * Ensures that a room exists with the alias, if it does not exist we create it. @@ -48,7 +48,7 @@ export async function ensureAliasedRoomExists(client: MatrixClient, alias: strin } } -async function configureMjolnir() { +async function configureMjolnir(config: IConfig) { try { await registerUser(config.pantalaimon.username, config.pantalaimon.username, config.pantalaimon.password, true) } catch (e) { @@ -72,8 +72,8 @@ let globalMjolnir: Mjolnir | null; /** * Return a test instance of Mjolnir. */ -export async function makeMjolnir(): Promise { - await configureMjolnir(); +export async function makeMjolnir(config: IConfig): Promise { + await configureMjolnir(config); LogService.setLogger(new RichConsoleLogger()); LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG)); LogService.info("test/mjolnirSetupUtils", "Starting bot..."); @@ -82,7 +82,7 @@ export async function makeMjolnir(): Promise { await overrideRatelimitForUser(await client.getUserId()); patchMatrixClient(); await ensureAliasedRoomExists(client, config.managementRoom); - let mj = await Mjolnir.setupMjolnirFromConfig(client); + let mj = await Mjolnir.setupMjolnirFromConfig(client, config); globalClient = client; globalMjolnir = mj; return mj;