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?
This commit is contained in:
Gnuxie 2022-08-09 11:29:27 +01:00 committed by GitHub
parent 121d4cf98f
commit 21aabc879a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 179 additions and 188 deletions

View File

@ -32,14 +32,12 @@ import { applyServerAcls } from "./actions/ApplyAcl";
import { RoomUpdateError } from "./models/RoomUpdateError"; import { RoomUpdateError } from "./models/RoomUpdateError";
import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler"; import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler";
import { applyUserBans } from "./actions/ApplyBan"; import { applyUserBans } from "./actions/ApplyBan";
import config from "./config";
import ErrorCache, { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "./ErrorCache"; import ErrorCache, { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "./ErrorCache";
import { Protection } from "./protections/IProtection"; import { Protection } from "./protections/IProtection";
import { PROTECTIONS } from "./protections/protections"; import { PROTECTIONS } from "./protections/protections";
import { ConsequenceType, Consequence } from "./protections/consequence"; import { ConsequenceType, Consequence } from "./protections/consequence";
import { ProtectionSettingValidationError } from "./protections/ProtectionSettings"; import { ProtectionSettingValidationError } from "./protections/ProtectionSettings";
import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue"; import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue";
import { Healthz } from "./health/healthz";
import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue"; import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue";
import { htmlEscape } from "./utils"; import { htmlEscape } from "./utils";
import { ReportManager } from "./report/ReportManager"; import { ReportManager } from "./report/ReportManager";
@ -50,6 +48,7 @@ import RuleServer from "./models/RuleServer";
import { RoomMemberManager } from "./RoomMembers"; import { RoomMemberManager } from "./RoomMembers";
import { ProtectedRoomActivityTracker } from "./queues/ProtectedRoomActivityTracker"; import { ProtectedRoomActivityTracker } from "./queues/ProtectedRoomActivityTracker";
import { ThrottlingQueue } from "./queues/ThrottlingQueue"; import { ThrottlingQueue } from "./queues/ThrottlingQueue";
import { IConfig } from "./config";
import PolicyList, { ListRuleChange } from "./models/PolicyList"; import PolicyList, { ListRuleChange } from "./models/PolicyList";
const levelToFn = { const levelToFn = {
@ -162,7 +161,7 @@ export class Mjolnir {
* @param {MatrixClient} client The client for Mjolnir to use. * @param {MatrixClient} client The client for Mjolnir to use.
* @returns A new Mjolnir instance that can be started without further setup. * @returns A new Mjolnir instance that can be started without further setup.
*/ */
static async setupMjolnirFromConfig(client: MatrixClient): Promise<Mjolnir> { static async setupMjolnirFromConfig(client: MatrixClient, config: IConfig): Promise<Mjolnir> {
const policyLists: PolicyList[] = []; const policyLists: PolicyList[] = [];
const protectedRooms: { [roomId: string]: string } = {}; const protectedRooms: { [roomId: string]: string } = {};
const joinedRooms = await client.getJoinedRooms(); const joinedRooms = await client.getJoinedRooms();
@ -188,7 +187,7 @@ export class Mjolnir {
} }
const ruleServer = config.web.ruleServer ? new RuleServer() : null; 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."); await mjolnir.logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status.");
Mjolnir.addJoinOnInviteListener(mjolnir, client, config); Mjolnir.addJoinOnInviteListener(mjolnir, client, config);
return mjolnir; return mjolnir;
@ -197,6 +196,7 @@ export class Mjolnir {
constructor( constructor(
public readonly client: MatrixClient, public readonly client: MatrixClient,
public readonly managementRoomId: string, public readonly managementRoomId: string,
public readonly config: IConfig,
/* /*
* All the rooms that Mjolnir is protecting and their permalinks. * 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). * 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); 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())); this.automaticRedactionReasons.push(new MatrixGlob(reason.toLowerCase()));
} }
@ -276,7 +276,7 @@ export class Mjolnir {
console.log("Creating Web APIs"); console.log("Creating Web APIs");
const reportManager = new ReportManager(this); const reportManager = new ReportManager(this);
reportManager.on("report.new", this.handleReport.bind(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) { if (config.pollReports) {
this.reportPoller = new ReportPoller(this, reportManager); this.reportPoller = new ReportPoller(this, reportManager);
} }
@ -356,20 +356,19 @@ export class Mjolnir {
await this.buildWatchedPolicyLists(); await this.buildWatchedPolicyLists();
this.applyUnprotectedRooms(); this.applyUnprotectedRooms();
if (config.verifyPermissionsOnStartup) { if (this.config.verifyPermissionsOnStartup) {
await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Checking permissions..."); await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Checking permissions...");
await this.verifyPermissions(config.verboseLogging); await this.verifyPermissions(this.config.verboseLogging);
} }
this.currentState = STATE_SYNCING; this.currentState = STATE_SYNCING;
if (config.syncOnStartup) { if (this.config.syncOnStartup) {
await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Syncing lists..."); await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Syncing lists...");
await this.syncLists(config.verboseLogging); await this.syncLists(this.config.verboseLogging);
await this.registerProtections(); await this.registerProtections();
} }
this.currentState = STATE_RUNNING; this.currentState = STATE_RUNNING;
Healthz.isHealthy = true;
await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Startup complete. Now monitoring rooms."); await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Startup complete. Now monitoring rooms.");
} catch (err) { } catch (err) {
try { try {
@ -399,14 +398,13 @@ export class Mjolnir {
if (!additionalRoomIds) additionalRoomIds = []; if (!additionalRoomIds) additionalRoomIds = [];
if (!Array.isArray(additionalRoomIds)) 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; let clientMessage = message;
if (level === LogLevel.WARN) clientMessage = `⚠ | ${message}`; if (level === LogLevel.WARN) clientMessage = `⚠ | ${message}`;
if (level === LogLevel.ERROR) clientMessage = `‼ | ${message}`; if (level === LogLevel.ERROR) clientMessage = `‼ | ${message}`;
const client = config.RUNTIME.client; const client = this.client;
const managementRoomId = await client.resolveRoom(config.managementRoom); const roomIds = [this.managementRoomId, ...additionalRoomIds];
const roomIds = [managementRoomId, ...additionalRoomIds];
let evContent: TextualMessageEventContent = { let evContent: TextualMessageEventContent = {
body: message, body: message,
@ -418,7 +416,7 @@ export class Mjolnir {
evContent = await replaceRoomIdsWithPills(this, clientMessage, new Set(roomIds), "m.notice"); 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); levelToFn[level.toString()](module, message);
@ -443,7 +441,7 @@ export class Mjolnir {
const rooms = (additionalProtectedRooms?.rooms ?? []); const rooms = (additionalProtectedRooms?.rooms ?? []);
rooms.push(roomId); rooms.push(roomId);
await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, { rooms: rooms }); 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) { public async removeProtectedRoom(roomId: string) {
@ -465,7 +463,7 @@ export class Mjolnir {
} }
private async resyncJoinedRooms(withSync = true) { 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 joinedRoomIds = (await this.client.getJoinedRooms()).filter(r => r !== this.managementRoomId);
const oldRoomIdsSet = new Set(this.protectedJoinedRoomIds); const oldRoomIdsSet = new Set(this.protectedJoinedRoomIds);
@ -491,7 +489,7 @@ export class Mjolnir {
this.applyUnprotectedRooms(); this.applyUnprotectedRooms();
if (withSync) { 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) { 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 if (this.explicitlyProtectedRoomIds.includes(roomId)) return; // explicitly protected
const createEvent = new CreateEvent(await this.client.getRoomStateEvent(roomId, "m.room.create", "")); const createEvent = new CreateEvent(await this.client.getRoomStateEvent(roomId, "m.room.create", ""));

View File

@ -18,7 +18,6 @@ import PolicyList from "../models/PolicyList";
import { ServerAcl } from "../models/ServerAcl"; import { ServerAcl } from "../models/ServerAcl";
import { RoomUpdateError } from "../models/RoomUpdateError"; import { RoomUpdateError } from "../models/RoomUpdateError";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import config from "../config";
import { LogLevel, UserID } from "matrix-bot-sdk"; import { LogLevel, UserID } from "matrix-bot-sdk";
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache"; 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.`); 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 // 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(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 // We specifically use sendNotice to avoid having to escape HTML
await mjolnir.logMessage(LogLevel.DEBUG, "ApplyAcl", `Applying ACL in ${roomId}`, roomId); 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); await mjolnir.client.sendStateEvent(roomId, "m.room.server_acl", "", finalAcl);
} else { } else {
await mjolnir.logMessage(LogLevel.WARN, "ApplyAcl", `Tried to apply ACL in ${roomId} but Mjolnir is running in no-op mode`, roomId); await mjolnir.logMessage(LogLevel.WARN, "ApplyAcl", `Tried to apply ACL in ${roomId} but Mjolnir is running in no-op mode`, roomId);

View File

@ -17,7 +17,6 @@ limitations under the License.
import PolicyList from "../models/PolicyList"; import PolicyList from "../models/PolicyList";
import { RoomUpdateError } from "../models/RoomUpdateError"; import { RoomUpdateError } from "../models/RoomUpdateError";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import config from "../config";
import { LogLevel } from "matrix-bot-sdk"; import { LogLevel } from "matrix-bot-sdk";
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache"; 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 }[]; let members: { userId: string, membership: string }[];
if (config.fasterMembershipChecks) { if (mjolnir.config.fasterMembershipChecks) {
const memberIds = await mjolnir.client.getJoinedRoomMembers(roomId); const memberIds = await mjolnir.client.getJoinedRoomMembers(roomId);
members = memberIds.map(u => { members = memberIds.map(u => {
return { userId: u, membership: "join" }; 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 // 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); 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); await mjolnir.client.banUser(member.userId, roomId, userRule.reason);
if (mjolnir.automaticRedactGlobs.find(g => g.test(userRule.reason.toLowerCase()))) { if (mjolnir.automaticRedactGlobs.find(g => g.test(userRule.reason.toLowerCase()))) {
mjolnir.queueRedactUserMessagesIn(member.userId, roomId); mjolnir.queueRedactUserMessagesIn(member.userId, roomId);

View File

@ -16,7 +16,6 @@ limitations under the License.
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { LogLevel, MatrixGlob, RichReply } from "matrix-bot-sdk"; import { LogLevel, MatrixGlob, RichReply } from "matrix-bot-sdk";
import config from "../config";
// !mjolnir kick <user|filter> [room] [reason] // !mjolnir kick <user|filter> [room] [reason]
export async function execKickCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { 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(); 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"; let replyMessage = "Wildcard bans require an addition `--force` argument to confirm";
const reply = RichReply.createFor(roomId, event, replyMessage, replyMessage); const reply = RichReply.createFor(roomId, event, replyMessage, replyMessage);
reply["msgtype"] = "m.notice"; reply["msgtype"] = "m.notice";
@ -60,7 +59,7 @@ export async function execKickCommand(roomId: string, event: any, mjolnir: Mjoln
if (kickRule.test(victim)) { if (kickRule.test(victim)) {
await mjolnir.logMessage(LogLevel.DEBUG, "KickCommand", `Removing ${victim} in ${protectedRoomId}`, protectedRoomId); await mjolnir.logMessage(LogLevel.DEBUG, "KickCommand", `Removing ${victim} in ${protectedRoomId}`, protectedRoomId);
if (!config.noop) { if (!mjolnir.config.noop) {
try { try {
await mjolnir.taskQueue.push(async () => { await mjolnir.taskQueue.push(async () => {
return mjolnir.client.kickUser(victim, protectedRoomId, reason); return mjolnir.client.kickUser(victim, protectedRoomId, reason);

View File

@ -14,14 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import config from "../config";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { RichReply } from "matrix-bot-sdk"; import { RichReply } from "matrix-bot-sdk";
// !mjolnir make admin <room> [<user ID>] // !mjolnir make admin <room> [<user ID>]
export async function execMakeRoomAdminCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { export async function execMakeRoomAdminCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
const isAdmin = await mjolnir.isSynapseAdmin(); 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 message = "Either the command is disabled or I am not running as homeserver administrator.";
const reply = RichReply.createFor(roomId, event, message, message); const reply = RichReply.createFor(roomId, event, message, message);
reply['msgtype'] = "m.notice"; reply['msgtype'] = "m.notice";

View File

@ -18,7 +18,6 @@ import { Mjolnir } from "../Mjolnir";
import PolicyList from "../models/PolicyList"; import PolicyList from "../models/PolicyList";
import { extractRequestError, LogLevel, LogService, MatrixGlob, RichReply } from "matrix-bot-sdk"; 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 { Recommendation, RULE_ROOM, RULE_SERVER, RULE_USER, USER_RULE_TYPES } from "../models/ListRule";
import config from "../config";
import { DEFAULT_LIST_EVENT_TYPE } from "./SetDefaultBanListCommand"; import { DEFAULT_LIST_EVENT_TYPE } from "./SetDefaultBanListCommand";
interface Arguments { 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 (!ruleType) replyMessage = "Please specify the type as either 'user', 'room', or 'server'";
else if (!entity) replyMessage = "No entity found"; 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"; 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)) { if (rule.test(victim)) {
await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Unbanning ${victim} in ${protectedRoomId}`, protectedRoomId); await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Unbanning ${victim} in ${protectedRoomId}`, protectedRoomId);
if (!config.noop) { if (!mjolnir.config.noop) {
await mjolnir.client.unbanUser(victim, protectedRoomId); await mjolnir.client.unbanUser(victim, protectedRoomId);
} else { } else {
await mjolnir.logMessage(LogLevel.WARN, "UnbanBanCommand", `Attempted to unban ${victim} in ${protectedRoomId} but Mjolnir is running in no-op mode`, protectedRoomId); 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) { if (unbannedSomeone) {
await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Syncing lists to ensure no users were accidentally unbanned`); 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);
} }
} }

View File

@ -25,7 +25,7 @@ import { MatrixClient } from "matrix-bot-sdk";
// The object is magically generated by external lib `config` // The object is magically generated by external lib `config`
// from the file specified by `NODE_ENV`, e.g. production.yaml // from the file specified by `NODE_ENV`, e.g. production.yaml
// or harness.yaml. // or harness.yaml.
interface IConfig { export interface IConfig {
homeserverUrl: string; homeserverUrl: string;
rawHomeserverUrl: string; rawHomeserverUrl: string;
accessToken: string; accessToken: string;

View File

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import config from "../config";
import * as http from "http"; import * as http from "http";
import { LogService } from "matrix-bot-sdk"; 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 { export class Healthz {
private static healthCode: number; private static healthCode: number;

View File

@ -56,13 +56,14 @@ if (config.health.healthz.enabled) {
patchMatrixClient(); patchMatrixClient();
config.RUNTIME.client = client; config.RUNTIME.client = client;
bot = await Mjolnir.setupMjolnirFromConfig(client); bot = await Mjolnir.setupMjolnirFromConfig(client, config);
} catch (err) { } catch (err) {
console.error(`Failed to setup mjolnir from the config ${config.dataPath}: ${err}`); console.error(`Failed to setup mjolnir from the config ${config.dataPath}: ${err}`);
throw err; throw err;
} }
try { try {
await bot.start(); await bot.start();
Healthz.isHealthy = true;
} catch (err) { } catch (err) {
console.error(`Mjolnir failed to start: ${err}`); console.error(`Mjolnir failed to start: ${err}`);
throw err; throw err;

View File

@ -18,7 +18,6 @@ import { Protection } from "./IProtection";
import { NumberProtectionSetting } from "./ProtectionSettings"; import { NumberProtectionSetting } from "./ProtectionSettings";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { LogLevel, LogService } from "matrix-bot-sdk"; 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 // if this is exceeded, we'll ban the user for spam and redact their messages
export const DEFAULT_MAX_PER_MINUTE = 10; export const DEFAULT_MAX_PER_MINUTE = 10;
@ -64,7 +63,7 @@ export class BasicFlooding extends Protection {
if (messageCount >= this.settings.maxPerMinute.value) { 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); 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"); await mjolnir.client.banUser(event['sender'], roomId, "spam");
} else { } else {
await mjolnir.logMessage(LogLevel.WARN, "BasicFlooding", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); 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 this.recentlyBanned.push(event['sender']); // flag to reduce spam
// Redact all the things the user said too // Redact all the things the user said too
if (!config.noop) { if (!mjolnir.config.noop) {
for (const eventId of forUser.map(e => e.eventId)) { for (const eventId of forUser.map(e => e.eventId)) {
await mjolnir.client.redactEvent(roomId, eventId, "spam"); await mjolnir.client.redactEvent(roomId, eventId, "spam");
} }

View File

@ -17,7 +17,6 @@ limitations under the License.
import { Protection } from "./IProtection"; import { Protection } from "./IProtection";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { LogLevel, LogService } from "matrix-bot-sdk"; import { LogLevel, LogService } from "matrix-bot-sdk";
import config from "../config";
import { isTrueJoinEvent } from "../utils"; import { isTrueJoinEvent } from "../utils";
export class FirstMessageIsImage extends Protection { 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('<img'); const isMedia = msgtype === 'm.image' || msgtype === 'm.video' || formattedBody.toLowerCase().includes('<img');
if (isMedia && this.justJoined[roomId].includes(event['sender'])) { if (isMedia && this.justJoined[roomId].includes(event['sender'])) {
await mjolnir.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Banning ${event['sender']} for posting an image as the first thing after joining in ${roomId}.`); await mjolnir.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Banning ${event['sender']} for posting an image as the first thing after joining in ${roomId}.`);
if (!config.noop) { if (!mjolnir.config.noop) {
await mjolnir.client.banUser(event['sender'], roomId, "spam"); await mjolnir.client.banUser(event['sender'], roomId, "spam");
} else { } else {
await mjolnir.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); await mjolnir.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);
@ -69,7 +68,7 @@ export class FirstMessageIsImage extends Protection {
this.recentlyBanned.push(event['sender']); // flag to reduce spam this.recentlyBanned.push(event['sender']); // flag to reduce spam
// Redact the event // Redact the event
if (!config.noop) { if (!mjolnir.config.noop) {
await mjolnir.client.redactEvent(roomId, event['event_id'], "spam"); await mjolnir.client.redactEvent(roomId, event['event_id'], "spam");
} else { } else {
await mjolnir.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); await mjolnir.logMessage(LogLevel.WARN, "FirstMessageIsImage", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);

View File

@ -18,7 +18,6 @@ import {Protection} from "./IProtection";
import {Mjolnir} from "../Mjolnir"; import {Mjolnir} from "../Mjolnir";
import {NumberProtectionSetting} from "./ProtectionSettings"; import {NumberProtectionSetting} from "./ProtectionSettings";
import {LogLevel} from "matrix-bot-sdk"; import {LogLevel} from "matrix-bot-sdk";
import config from "../config";
const DEFAULT_MAX_PER_TIMESCALE = 50; const DEFAULT_MAX_PER_TIMESCALE = 50;
const DEFAULT_TIMESCALE_MINUTES = 60; const DEFAULT_TIMESCALE_MINUTES = 60;
@ -89,7 +88,7 @@ export class JoinWaveShortCircuit extends Protection {
if (++this.joinBuckets[roomId].numberOfJoins >= this.settings.maxPer.value) { if (++this.joinBuckets[roomId].numberOfJoins >= 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); 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"}) await mjolnir.client.sendStateEvent(roomId, "m.room.join_rules", "", {"join_rule": "invite"})
} else { } else {
await mjolnir.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Tried to set ${roomId} to invite-only, but Mjolnir is running in no-op mode`, roomId); await mjolnir.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Tried to set ${roomId} to invite-only, but Mjolnir is running in no-op mode`, roomId);

View File

@ -17,7 +17,6 @@ limitations under the License.
import { Protection } from "./IProtection"; import { Protection } from "./IProtection";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk"; import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk";
import config from "../config";
export class MessageIsMedia extends Protection { export class MessageIsMedia extends Protection {
@ -43,7 +42,7 @@ export class MessageIsMedia extends Protection {
if (isMedia) { 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])}`); 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 // Redact the event
if (!config.noop) { if (!mjolnir.config.noop) {
await mjolnir.client.redactEvent(roomId, event['event_id'], "Images/videos are not permitted here"); await mjolnir.client.redactEvent(roomId, event['event_id'], "Images/videos are not permitted here");
} else { } else {
await mjolnir.logMessage(LogLevel.WARN, "MessageIsMedia", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); await mjolnir.logMessage(LogLevel.WARN, "MessageIsMedia", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);

View File

@ -17,7 +17,6 @@ limitations under the License.
import { Protection } from "./IProtection"; import { Protection } from "./IProtection";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk"; import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk";
import config from "../config";
export class MessageIsVoice extends Protection { export class MessageIsVoice extends Protection {
@ -40,7 +39,7 @@ export class MessageIsVoice extends Protection {
if (event['content']['org.matrix.msc3245.voice'] === undefined) return; 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])}`); 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 // Redact the event
if (!config.noop) { if (!mjolnir.config.noop) {
await mjolnir.client.redactEvent(roomId, event['event_id'], "Voice messages are not permitted here"); await mjolnir.client.redactEvent(roomId, event['event_id'], "Voice messages are not permitted here");
} else { } else {
await mjolnir.logMessage(LogLevel.WARN, "MessageIsVoice", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); await mjolnir.logMessage(LogLevel.WARN, "MessageIsVoice", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);

View File

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import config from "../config";
import { Protection } from "./IProtection"; import { Protection } from "./IProtection";
import { MXIDListProtectionSetting, NumberProtectionSetting } from "./ProtectionSettings"; import { MXIDListProtectionSetting, NumberProtectionSetting } from "./ProtectionSettings";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
@ -83,7 +82,7 @@ export class TrustedReporters extends Protection {
if (met.length > 0) { if (met.length > 0) {
await mjolnir.client.sendMessage(config.managementRoom, { await mjolnir.client.sendMessage(mjolnir.config.managementRoom, {
msgtype: "m.notice", msgtype: "m.notice",
body: `message ${event.id} reported by ${[...reporters].join(', ')}. ` body: `message ${event.id} reported by ${[...reporters].join(', ')}. `
+ `actions: ${met.join(', ')}` + `actions: ${met.join(', ')}`

View File

@ -17,7 +17,6 @@ limitations under the License.
import { Protection } from "./IProtection"; import { Protection } from "./IProtection";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { LogLevel, LogService } from "matrix-bot-sdk"; import { LogLevel, LogService } from "matrix-bot-sdk";
import config from "../config";
import { isTrueJoinEvent } from "../utils"; import { isTrueJoinEvent } from "../utils";
export class WordList extends Protection { export class WordList extends Protection {
@ -25,15 +24,10 @@ export class WordList extends Protection {
settings = {}; settings = {};
private justJoined: { [roomId: string]: { [username: string]: Date} } = {}; private justJoined: { [roomId: string]: { [username: string]: Date} } = {};
private badWords: RegExp; private badWords?: RegExp;
constructor() { constructor() {
super(); 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 { public get name(): string {
@ -47,7 +41,7 @@ export class WordList extends Protection {
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> { public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
const content = event['content'] || {}; const content = event['content'] || {};
const minsBeforeTrusting = config.protections.wordlist.minutesBeforeTrusting; const minsBeforeTrusting = mjolnir.config.protections.wordlist.minutesBeforeTrusting;
if (minsBeforeTrusting > 0) { if (minsBeforeTrusting > 0) {
if (!this.justJoined[roomId]) this.justJoined[roomId] = {}; if (!this.justJoined[roomId]) this.justJoined[roomId] = {};
@ -89,19 +83,29 @@ export class WordList extends Protection {
return 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 // 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}.`); 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"); await mjolnir.client.banUser(event['sender'], roomId, "Word list violation");
} else { } else {
await mjolnir.logMessage(LogLevel.WARN, "WordList", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); 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 // Redact the event
if (!config.noop) { if (!mjolnir.config.noop) {
await mjolnir.client.redactEvent(roomId, event['event_id'], "spam"); await mjolnir.client.redactEvent(roomId, event['event_id'], "spam");
} else { } else {
await mjolnir.logMessage(LogLevel.WARN, "WordList", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); await mjolnir.logMessage(LogLevel.WARN, "WordList", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId);

View File

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { extractRequestError, LogLevel, LogService, Permalinks } from "matrix-bot-sdk"; import { extractRequestError, LogLevel, LogService, Permalinks } from "matrix-bot-sdk";
import config from "../config";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
/** /**
@ -43,7 +42,7 @@ export class UnlistedUserRedactionQueue {
const permalink = Permalinks.forEvent(roomId, event['event_id']); const permalink = Permalinks.forEvent(roomId, event['event_id']);
try { try {
LogService.info("AutomaticRedactionQueue", `Redacting event because the user is listed as bad: ${permalink}`) 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']); await mjolnir.client.redactEvent(roomId, event['event_id']);
} else { } else {
await mjolnir.logMessage(LogLevel.WARN, "AutomaticRedactionQueue", `Tried to redact ${permalink} but Mjolnir is running in no-op mode`); await mjolnir.logMessage(LogLevel.WARN, "AutomaticRedactionQueue", `Tried to redact ${permalink} but Mjolnir is running in no-op mode`);

View File

@ -20,8 +20,6 @@ import { htmlToText } from "html-to-text";
import { htmlEscape } from "../utils"; import { htmlEscape } from "../utils";
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import config from "../config";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
/// Regexp, used to extract the action label from an action reaction /// 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 }) { 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 }); 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 }); return this.displayManager.displayReportAndUI({ kind: Kind.SERVER_ABUSE_REPORT, event, reporterId, reason, moderationRoomId: this.mjolnir.managementRoomId });
} }
} }

View File

@ -28,7 +28,6 @@ import {
setRequestFn, setRequestFn,
} from "matrix-bot-sdk"; } from "matrix-bot-sdk";
import { Mjolnir } from "./Mjolnir"; import { Mjolnir } from "./Mjolnir";
import config from "./config";
import { ClientRequest, IncomingMessage } from "http"; import { ClientRequest, IncomingMessage } from "http";
import { default as parseDuration } from "parse-duration"; 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) => { await getMessagesByUserIn(mjolnir.client, userIdOrGlob, targetRoomId, limit, async (eventsToRedact) => {
for (const victimEvent of eventsToRedact) { for (const victimEvent of eventsToRedact) {
await mjolnir.logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Redacting ${victimEvent['event_id']} in ${targetRoomId}`, targetRoomId); 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']); await mjolnir.client.redactEvent(targetRoomId, victimEvent['event_id']);
} else { } 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); await mjolnir.logMessage(LogLevel.WARN, "utils#redactUserMessagesIn", `Tried to redact ${victimEvent['event_id']} in ${targetRoomId} but Mjolnir is running in no-op mode`, targetRoomId);

View File

@ -15,13 +15,11 @@ limitations under the License.
*/ */
import { Server } from "http"; import { Server } from "http";
import * as express from "express"; import * as express from "express";
import { LogService, MatrixClient } from "matrix-bot-sdk"; import { LogService, MatrixClient } from "matrix-bot-sdk";
import config from "../config";
import RuleServer from "../models/RuleServer"; import RuleServer from "../models/RuleServer";
import { ReportManager } from "../report/ReportManager"; import { ReportManager } from "../report/ReportManager";
import { IConfig } from "../config";
/** /**
@ -35,7 +33,7 @@ export class WebAPIs {
private webController: express.Express = express(); private webController: express.Express = express();
private httpServer?: Server; 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. // Setup JSON parsing.
this.webController.use(express.json()); this.webController.use(express.json());
} }
@ -44,14 +42,14 @@ export class WebAPIs {
* Start accepting requests to the Web API. * Start accepting requests to the Web API.
*/ */
public async start() { public async start() {
if (!config.web.enabled) { if (!this.config.web.enabled) {
return; 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. // configure /report API.
if (config.web.abuseReporting.enabled) { if (this.config.web.abuseReporting.enabled) {
console.log(`Configuring ${API_PREFIX}/report/:room_id/:event_id...`); console.log(`configuring ${API_PREFIX}/report/:room_id/:event_id...`);
this.webController.options(`${API_PREFIX}/report/:room_id/:event_id`, async (request, response) => { this.webController.options(`${API_PREFIX}/report/:room_id/:event_id`, async (request, response) => {
// reply with CORS options // reply with CORS options
response.header("Access-Control-Allow-Origin", "*"); response.header("Access-Control-Allow-Origin", "*");
@ -68,15 +66,15 @@ export class WebAPIs {
response.header("Access-Control-Allow-Methods", "POST, OPTIONS"); response.header("Access-Control-Allow-Methods", "POST, OPTIONS");
await this.handleReport({ request, response, roomId: request.params.room_id, eventId: request.params.event_id }) 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? // FIXME: Doesn't this need some kind of access control?
// See https://github.com/matrix-org/mjolnir/issues/139#issuecomment-1012221479. // 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`; const updatesUrl = `${API_PREFIX}/ruleserver/updates`;
LogService.info("WebAPIs", `Configuring ${updatesUrl}...`); LogService.info("WebAPIs", `configuring ${updatesUrl}...`);
if (!this.ruleServer) { if (!this.ruleServer) {
throw new Error("The rule server to use has not been configured for the WebAPIs."); 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) => { this.webController.get(updatesUrl, async (request, response) => {
await this.handleRuleServerUpdate(ruleServer, { request, response, since: request.query.since as string}); 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 // so we are not extending the abilities of Mjölnir
// 3. We are avoiding the use of the Synapse Admin API to ensure that // 3. We are avoiding the use of the Synapse Admin API to ensure that
// this feature can work with all homeservers, not just Synapse. // 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 = () => { reporterClient.start = () => {
throw new Error("We MUST NEVER call start on the reporter client"); throw new Error("We MUST NEVER call start on the reporter client");
}; };

View File

@ -18,6 +18,7 @@ import * as expect from "expect";
import { Mjolnir } from "../../src/Mjolnir"; import { Mjolnir } from "../../src/Mjolnir";
import { DEFAULT_LIST_EVENT_TYPE } from "../../src/commands/SetDefaultBanListCommand"; import { DEFAULT_LIST_EVENT_TYPE } from "../../src/commands/SetDefaultBanListCommand";
import { parseArguments } from "../../src/commands/UnbanBanCommand"; import { parseArguments } from "../../src/commands/UnbanBanCommand";
import config from "../../src/config";
import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../../src/models/ListRule"; import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../../src/models/ListRule";
function createTestMjolnir(defaultShortcode: string|null = null): Mjolnir { 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}`); throw new Error(`Unknown event type ${eventType}, expected ${DEFAULT_LIST_EVENT_TYPE}`);
}, },
}; };
return <Mjolnir>{client}; return <Mjolnir>{
client,
config,
};
} }
function createFakeEvent(command: string): any { function createFakeEvent(command: string): any {

View File

@ -1,12 +1,11 @@
import { strict as assert } from "assert"; import { strict as assert } from "assert";
import config from "../../src/config";
import { newTestUser } from "./clientHelper"; import { newTestUser } from "./clientHelper";
import { LogService, MatrixClient, Permalinks, UserID } from "matrix-bot-sdk"; import { LogService, MatrixClient, Permalinks, UserID } from "matrix-bot-sdk";
import PolicyList, { ChangeType, ListRuleChange } from "../../src/models/PolicyList"; import PolicyList, { ChangeType, ListRuleChange } from "../../src/models/PolicyList";
import { ServerAcl } from "../../src/models/ServerAcl"; import { ServerAcl } from "../../src/models/ServerAcl";
import { getFirstReaction } from "./commands/commandUtils"; import { getFirstReaction } from "./commands/commandUtils";
import { getMessagesByUserIn } from "../../src/utils"; 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"; 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() { describe("Test: Updating the PolicyList", function() {
it("Calculates what has changed correctly.", async function() { it("Calculates what has changed correctly.", async function() {
this.timeout(10000); this.timeout(10000);
const mjolnir = config.RUNTIME.client! const mjolnir: Mjolnir = this.mjolnir!
const moderator = await newTestUser({ name: { contains: "moderator" } }); const moderator = await newTestUser({ name: { contains: "moderator" } });
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()] }); const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] });
const banList = new PolicyList(banListId, banListId, mjolnir); const banList = new PolicyList(banListId, banListId, mjolnir.client);
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100); mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
assert.equal(banList.allRules.length, 0); assert.equal(banList.allRules.length, 0);
// Test adding a new rule // 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(); let changes: ListRuleChange[] = await banList.updateList();
assert.equal(changes.length, 1, 'There should only be one change'); assert.equal(changes.length, 1, 'There should only be one change');
assert.equal(changes[0].changeType, ChangeType.Added); 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.userRules.length, 1);
assert.equal(banList.allRules.length, 1); assert.equal(banList.allRules.length, 1);
// Test modifiying a rule // 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(); 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(); changes = await banList.updateList();
assert.equal(changes.length, 1); assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Modified); 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].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule');
assert.equal(changes[0].event['event_id'], modifyingEventId); 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(); changes = await banList.updateList();
assert.equal(changes.length, 1); assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Modified); 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'); 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 // 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(); await banList.updateList();
assert.equal(banList.userRules.filter(r => r.entity === '@redacted:localhost:9999').length, 1); 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(); changes = await banList.updateList();
assert.equal(changes.length, 1); assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Removed); assert.equal(changes[0].changeType, ChangeType.Removed);
@ -80,10 +79,10 @@ describe("Test: Updating the PolicyList", function() {
// Test soft redaction of a rule // Test soft redaction of a rule
const softRedactedEntity = '@softredacted:localhost:9999' const softRedactedEntity = '@softredacted:localhost:9999'
await createPolicyRule(mjolnir, banListId, RULE_USER, softRedactedEntity, ''); await createPolicyRule(mjolnir.client, banListId, RULE_USER, softRedactedEntity, '');
await banList.updateList(); await banList.updateList();
assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 1); 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(); changes = await banList.updateList();
assert.equal(changes.length, 1); assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Removed); 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'); 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 // 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(); 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(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'); 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. // 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(); changes = await banList.updateList();
assert.equal(changes.length, 1); assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Added); assert.equal(changes[0].changeType, ChangeType.Added);
assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1); 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(); changes = await banList.updateList();
assert.equal(changes.length, 1); assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Modified); assert.equal(changes[0].changeType, ChangeType.Modified);
assert.equal(changes[0].event['event_id'], modifyingEventId); 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(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); 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(); changes = await banList.updateList();
assert.equal(changes.length, 1); assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Modified); 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() { 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); this.timeout(3000);
const mjolnir = config.RUNTIME.client! const mjolnir: Mjolnir = this.mjolnir!
const moderator = await newTestUser({ name: { contains: "moderator" } }); const moderator = await newTestUser({ name: { contains: "moderator" }} );
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()] }); const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] });
const banList = new PolicyList(banListId, banListId, mjolnir); const banList = new PolicyList(banListId, banListId, mjolnir.client);
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100); mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
const entity = '@old:localhost:9999'; 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(); let changes = await banList.updateList();
assert.equal(changes.length, 1); assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Added); 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...') 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(); changes = await banList.updateList();
assert.equal(changes.length, 1); assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Removed); 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() { 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); this.timeout(3000);
const mjolnir = config.RUNTIME.client! const mjolnir: Mjolnir = this.mjolnir!
const moderator = await newTestUser({ name: { contains: "moderator" } }); const moderator = await newTestUser({ name: { contains: "moderator" } });
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()] }); const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] });
const banList = new PolicyList(banListId, banListId, mjolnir); const banList = new PolicyList(banListId, banListId, mjolnir.client);
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100); mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
const entity = '@old:localhost:9999'; 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(); let changes = await banList.updateList();
assert.equal(changes.length, 1); assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Added); 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...') 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(); 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 // 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. // 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.'); 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. // 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(); changes = await banList.updateList();
assert.equal(changes.length, 0); assert.equal(changes.length, 0);
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'The rule should still be active.'); 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. // 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(); changes = await banList.updateList();
assert.equal(changes.length, 1); assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Removed); 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(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.'); 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() { it('Test: PolicyList Supports all entity types.', async function () {
const mjolnir = config.RUNTIME.client! const mjolnir: Mjolnir = this.mjolnir!
const banListId = await mjolnir.createRoom(); const banListId = await mjolnir.client.createRoom();
const banList = new PolicyList(banListId, banListId, mjolnir); const banList = new PolicyList(banListId, banListId, mjolnir.client);
for (let i = 0; i < ALL_RULE_TYPES.length; i++) { 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(); let changes: ListRuleChange[] = await banList.updateList();
assert.equal(changes.length, ALL_RULE_TYPES.length); 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() { 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 mjolnir: Mjolnir = this.mjolnir!
const banListId = await mjolnir.createRoom(); const banListId = await mjolnir.client.createRoom();
const banList = new PolicyList(banListId, banListId, mjolnir); const banList = new PolicyList(banListId, banListId, mjolnir.client);
await createPolicyRule(mjolnir, banListId, RULE_SERVER, 'exmaple.org', '', { recommendation: 'something that is not m.ban' }); await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, 'exmaple.org', '', { recommendation: 'something that is not m.ban' });
let changes: ListRuleChange[] = await banList.updateList(); let changes: ListRuleChange[] = await banList.updateList();
assert.equal(changes.length, 1, 'There should only be one change'); assert.equal(changes.length, 1, 'There should only be one change');
assert.equal(changes[0].changeType, ChangeType.Added); 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. // 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.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)}`); 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() { 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() { it('We do not ban ourselves when we put ourselves into the policy list.', async function() {
const mjolnir = config.RUNTIME.client! const mjolnir: Mjolnir = this.mjolnir
const serverName = new UserID(await mjolnir.getUserId()).domain; const serverName = new UserID(await mjolnir.client.getUserId()).domain;
const banListId = await mjolnir.createRoom(); const banListId = await mjolnir.client.createRoom();
const banList = new PolicyList(banListId, banListId, mjolnir); const banList = new PolicyList(banListId, banListId, mjolnir.client);
await createPolicyRule(mjolnir, banListId, RULE_SERVER, serverName, ''); await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, serverName, '');
await createPolicyRule(mjolnir, banListId, RULE_SERVER, 'evil.com', ''); await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, 'evil.com', '');
await createPolicyRule(mjolnir, banListId, RULE_SERVER, '*', ''); await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, '*', '');
// We should still intern the matching rules rule. // We should still intern the matching rules rule.
let changes: ListRuleChange[] = await banList.updateList(); let changes: ListRuleChange[] = await banList.updateList();
assert.equal(banList.serverRules.length, 3); 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() { 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() { it('Will batch ACL updates if we spam rules into a PolicyList', async function() {
const mjolnir = config.RUNTIME.client! const mjolnir: Mjolnir = this.mjolnir!
const serverName: string = new UserID(await mjolnir.getUserId()).domain const serverName: string = new UserID(await mjolnir.client.getUserId()).domain
const moderator = await newTestUser({ name: { contains: "moderator" } }); const moderator = await newTestUser({ name: { contains: "moderator" } });
moderator.joinRoom(this.mjolnir.managementRoomId); moderator.joinRoom(this.mjolnir.client.managementRoomId);
const mjolnirId = await mjolnir.getUserId(); const mjolnirId = await mjolnir.client.getUserId();
// Setup some protected rooms so we can check their ACL state later. // Setup some protected rooms so we can check their ACL state later.
const protectedRooms: string[] = []; const protectedRooms: string[] = [];
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const room = await moderator.createRoom({ invite: [mjolnirId] }); const room = await moderator.createRoom({ invite: [mjolnirId] });
await mjolnir.joinRoom(room); await mjolnir.client.joinRoom(room);
await moderator.setUserPowerLevel(mjolnirId, room, 100); await moderator.setUserPowerLevel(mjolnirId, room, 100);
await this.mjolnir!.addProtectedRoom(room); await mjolnir.addProtectedRoom(room);
protectedRooms.push(room); protectedRooms.push(room);
} }
// If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point. // 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 => { await Promise.all(protectedRooms.map(async room => {
// We're going to need timeline pagination I'm afraid. // 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.'); 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. // 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] }); const banListId = await moderator.createRoom({ invite: [mjolnirId] });
mjolnir.joinRoom(banListId); mjolnir.client.joinRoom(banListId);
this.mjolnir!.watchList(Permalinks.forRoom(banListId)); mjolnir.watchList(Permalinks.forRoom(banListId));
const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*"); const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*");
const evilServerCount = 200; const evilServerCount = 200;
for (let i = 0; i < evilServerCount; i++) { 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. // 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... // 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 // At this point we check that the state within Mjolnir is internally consistent, this is just because debugging the following
// is a pita. // 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. // 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 => { 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)) { if (!acl.matches(roomAcl)) {
assert.fail(`Room ${room} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`) assert.fail(`Room ${room} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`)
} }
let aclEventCount = 0; 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); events.forEach(event => event.type === 'm.room.server_acl' ? aclEventCount += 1 : null);
}); });
LogService.debug('PolicyListTest', `aclEventCount: ${aclEventCount}`); LogService.debug('PolicyListTest', `aclEventCount: ${aclEventCount}`);
@ -301,29 +300,29 @@ describe('Test: unbaning entities via the PolicyList.', function() {
afterEach(function() { this.moderator?.stop(); }); afterEach(function() { this.moderator?.stop(); });
it('Will remove rules that have legacy types', async function() { it('Will remove rules that have legacy types', async function() {
this.timeout(20000) this.timeout(20000)
const mjolnir = config.RUNTIME.client! const mjolnir: Mjolnir = this.mjolnir!
const serverName: string = new UserID(await mjolnir.getUserId()).domain const serverName: string = new UserID(await mjolnir.client.getUserId()).domain
const moderator = await newTestUser({ name: { contains: "moderator" } }); const moderator = await newTestUser({ name: { contains: "moderator" } });
this.moderator = moderator; this.moderator = moderator;
moderator.joinRoom(this.mjolnir.managementRoomId); moderator.joinRoom(mjolnir.managementRoomId);
const mjolnirId = await mjolnir.getUserId(); const mjolnirId = await mjolnir.client.getUserId();
// We'll make 1 protected room to test ACLs in. // We'll make 1 protected room to test ACLs in.
const protectedRoom = await moderator.createRoom({ invite: [mjolnirId] }); const protectedRoom = await moderator.createRoom({ invite: [mjolnirId] });
await mjolnir.joinRoom(protectedRoom); await mjolnir.client.joinRoom(protectedRoom);
await moderator.setUserPowerLevel(mjolnirId, protectedRoom, 100); 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. // 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();
const roomAcl = await mjolnir.getRoomStateEvent(protectedRoom, "m.room.server_acl", ""); 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.'); assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.');
// Create some legacy rules on a PolicyList. // Create some legacy rules on a PolicyList.
const banListId = await moderator.createRoom({ invite: [mjolnirId] }); 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 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)); this.mjolnir!.watchList(Permalinks.forRoom(banListId));
// we use this to compare changes. // we use this to compare changes.
const banList = new PolicyList(banListId, banListId, moderator); 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. // 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 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)) { if (!acl.matches(protectedAcl)) {
assert.fail(`Room ${protectedRoom} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`); 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. // Confirm that the server is unbanned.
await banList.updateList(); await banList.updateList();
assert.equal(banList.allRules.length, 0); 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'); 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() { 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() { it('Applies bans to the most recently active rooms first', async function() {
this.timeout(180000) this.timeout(180000)
const mjolnir = config.RUNTIME.client! const mjolnir: Mjolnir = this.mjolnir!
const serverName: string = new UserID(await mjolnir.getUserId()).domain const serverName: string = new UserID(await mjolnir.client.getUserId()).domain
const moderator = await newTestUser({ name: { contains: "moderator" } }); const moderator = await newTestUser({ name: { contains: "moderator" } });
moderator.joinRoom(this.mjolnir.managementRoomId); moderator.joinRoom(mjolnir.managementRoomId);
const mjolnirId = await mjolnir.getUserId(); const mjolnirId = await mjolnir.client.getUserId();
// Setup some protected rooms so we can check their ACL state later. // Setup some protected rooms so we can check their ACL state later.
const protectedRooms: string[] = []; const protectedRooms: string[] = [];
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const room = await moderator.createRoom({ invite: [mjolnirId] }); const room = await moderator.createRoom({ invite: [mjolnirId] });
await mjolnir.joinRoom(room); await mjolnir.client.joinRoom(room);
await moderator.setUserPowerLevel(mjolnirId, room, 100); await moderator.setUserPowerLevel(mjolnirId, room, 100);
await this.mjolnir!.addProtectedRoom(room); await mjolnir.addProtectedRoom(room);
protectedRooms.push(room); protectedRooms.push(room);
} }
// If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point. // 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 => { 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.'); 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. // 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] }); const banListId = await moderator.createRoom({ invite: [mjolnirId] });
mjolnir.joinRoom(banListId); mjolnir.client.joinRoom(banListId);
this.mjolnir!.watchList(Permalinks.forRoom(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. // 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--) { 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. // create some activity in the same order.
for (const roomId of protectedRooms.slice().reverse()) { 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)); await new Promise(resolve => setTimeout(resolve, 100));
} }
// check the rooms are in the expected order // check the rooms are in the expected order
for (let i = 0; i < protectedRooms.length; i++) { 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`; 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}`); await createPolicyRule(moderator, banListId, RULE_SERVER, badServer, `Rule ${badServer}`);
// Wait until all the ACL events have been applied. // Wait until all the ACL events have been applied.
await this.mjolnir!.syncLists(); await mjolnir.syncLists();
for (let i = 0; i < protectedRooms.length; i++) { 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. // 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) { for (const roomId of protectedRooms) {
let roomAclEvent: null | any; let roomAclEvent: null | any;
// Can't be the best way to get the whole event, but ok. // 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; const roomAcl = roomAclEvent!.content;
if (!acl.matches(roomAcl)) { if (!acl.matches(roomAcl)) {
assert.fail(`Room ${roomId} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`) assert.fail(`Room ${roomId} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`)

View File

@ -1,6 +1,6 @@
import { HmacSHA1 } from "crypto-js"; import { HmacSHA1 } from "crypto-js";
import { getRequestFn, LogService, MatrixClient, MemoryStorageProvider, PantalaimonClient } from "matrix-bot-sdk"; 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_ATTEMPTS = 10;
const REGISTRATION_RETRY_BASE_DELAY_MS = 100; const REGISTRATION_RETRY_BASE_DELAY_MS = 100;

View File

@ -14,7 +14,7 @@ export const mochaHooks = {
// Sometimes it takes a little longer to register users. // Sometimes it takes a little longer to register users.
this.timeout(10000) this.timeout(10000)
this.managementRoomAlias = config.managementRoom; this.managementRoomAlias = config.managementRoom;
this.mjolnir = await makeMjolnir(); this.mjolnir = await makeMjolnir(config);
config.RUNTIME.client = this.mjolnir.client; config.RUNTIME.client = this.mjolnir.client;
await Promise.all([ await Promise.all([
this.mjolnir.client.setAccountData('org.matrix.mjolnir.protected_rooms', { rooms: [] }), this.mjolnir.client.setAccountData('org.matrix.mjolnir.protected_rooms', { rooms: [] }),

View File

@ -3,8 +3,9 @@
*/ */
import { makeMjolnir } from "./mjolnirSetupUtils"; import { makeMjolnir } from "./mjolnirSetupUtils";
import config from '../../src/config';
(async () => { (async () => {
let mjolnir = await makeMjolnir(); let mjolnir = await makeMjolnir(config);
await mjolnir.start(); await mjolnir.start();
})(); })();

View File

@ -22,9 +22,9 @@ import {
RichConsoleLogger RichConsoleLogger
} from "matrix-bot-sdk"; } from "matrix-bot-sdk";
import { Mjolnir} from '../../src/Mjolnir'; import { Mjolnir} from '../../src/Mjolnir';
import config from "../../src/config";
import { overrideRatelimitForUser, registerUser } from "./clientHelper"; import { overrideRatelimitForUser, registerUser } from "./clientHelper";
import { patchMatrixClient } from "../../src/utils"; 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. * 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 { try {
await registerUser(config.pantalaimon.username, config.pantalaimon.username, config.pantalaimon.password, true) await registerUser(config.pantalaimon.username, config.pantalaimon.username, config.pantalaimon.password, true)
} catch (e) { } catch (e) {
@ -72,8 +72,8 @@ let globalMjolnir: Mjolnir | null;
/** /**
* Return a test instance of Mjolnir. * Return a test instance of Mjolnir.
*/ */
export async function makeMjolnir(): Promise<Mjolnir> { export async function makeMjolnir(config: IConfig): Promise<Mjolnir> {
await configureMjolnir(); await configureMjolnir(config);
LogService.setLogger(new RichConsoleLogger()); LogService.setLogger(new RichConsoleLogger());
LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG)); LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG));
LogService.info("test/mjolnirSetupUtils", "Starting bot..."); LogService.info("test/mjolnirSetupUtils", "Starting bot...");
@ -82,7 +82,7 @@ export async function makeMjolnir(): Promise<Mjolnir> {
await overrideRatelimitForUser(await client.getUserId()); await overrideRatelimitForUser(await client.getUserId());
patchMatrixClient(); patchMatrixClient();
await ensureAliasedRoomExists(client, config.managementRoom); await ensureAliasedRoomExists(client, config.managementRoom);
let mj = await Mjolnir.setupMjolnirFromConfig(client); let mj = await Mjolnir.setupMjolnirFromConfig(client, config);
globalClient = client; globalClient = client;
globalMjolnir = mj; globalMjolnir = mj;
return mj; return mj;