never mutate config.managementRoom in-place (#184)

This commit is contained in:
Jess Porter 2022-01-17 16:24:12 +00:00 committed by GitHub
parent 4490f9ba82
commit 941d10b015
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 59 additions and 66 deletions

View File

@ -35,8 +35,9 @@ export async function logMessage(level: LogLevel, module: string, message: strin
if (level === LogLevel.WARN) clientMessage = `⚠ | ${message}`;
if (level === LogLevel.ERROR) clientMessage = `‼ | ${message}`;
const roomIds = [config.managementRoom, ...additionalRoomIds];
const client = config.RUNTIME.client;
const managementRoomId = await client.resolveRoom(config.managementRoom);
const roomIds = [managementRoomId, ...additionalRoomIds];
let evContent: TextualMessageEventContent = {
body: message,
@ -48,7 +49,7 @@ export async function logMessage(level: LogLevel, module: string, message: strin
evContent = await replaceRoomIdsWithPills(client, clientMessage, roomIds, "m.notice");
}
await client.sendMessage(config.managementRoom, evContent);
await client.sendMessage(managementRoomId, evContent);
}
levelToFn[level.toString()](module, message);

View File

@ -82,14 +82,14 @@ export class Mjolnir {
* @param {boolean} options.autojoinOnlyIfManager Whether to only accept an invitation by a user present in the `managementRoom`.
* @param {string} options.acceptInvitesFromGroup A group of users to accept invites from, ignores invites form users not in this group.
*/
private static addJoinOnInviteListener(client: MatrixClient, options) {
private static addJoinOnInviteListener(mjolnir: Mjolnir, client: MatrixClient, options) {
client.on("room.invite", async (roomId: string, inviteEvent: any) => {
const membershipEvent = new MembershipEvent(inviteEvent);
const reportInvite = async () => {
if (!options.recordIgnoredInvites) return; // Nothing to do
await client.sendMessage(options.managementRoom, {
await client.sendMessage(mjolnir.managementRoomId, {
msgtype: "m.text",
body: `${membershipEvent.sender} has invited me to ${roomId} but the config prevents me from accepting the invitation. `
+ `If you would like this room protected, use "!mjolnir rooms add ${roomId}" so I can accept the invite.`,
@ -101,7 +101,7 @@ export class Mjolnir {
};
if (options.autojoinOnlyIfManager) {
const managers = await client.getJoinedRoomMembers(options.managementRoom);
const managers = await client.getJoinedRoomMembers(mjolnir.managementRoomId);
if (!managers.includes(membershipEvent.sender)) return reportInvite(); // ignore invite
} else {
const groupMembers = await client.unstableApis.getGroupUsers(options.acceptInvitesFromGroup);
@ -119,8 +119,6 @@ export class Mjolnir {
* @returns A new Mjolnir instance that can be started without further setup.
*/
static async setupMjolnirFromConfig(client: MatrixClient): Promise<Mjolnir> {
Mjolnir.addJoinOnInviteListener(client, config);
const banLists: BanList[] = [];
const protectedRooms: { [roomId: string]: string } = {};
const joinedRooms = await client.getJoinedRooms();
@ -142,17 +140,18 @@ export class Mjolnir {
LogService.info("index", "Resolving management room...");
const managementRoomId = await client.resolveRoom(config.managementRoom);
if (!joinedRooms.includes(managementRoomId)) {
config.managementRoom = await client.joinRoom(config.managementRoom);
} else {
config.managementRoom = managementRoomId;
await client.joinRoom(config.managementRoom);
}
await logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status.");
return new Mjolnir(client, protectedRooms, banLists);
const mjolnir = new Mjolnir(client, managementRoomId, protectedRooms, banLists);
Mjolnir.addJoinOnInviteListener(mjolnir, client, config);
return mjolnir;
}
constructor(
public readonly client: MatrixClient,
public readonly managementRoomId: string,
public readonly protectedRooms: { [roomId: string]: string },
private banLists: BanList[],
) {
@ -167,7 +166,7 @@ export class Mjolnir {
client.on("room.event", this.handleEvent.bind(this));
client.on("room.message", async (roomId, event) => {
if (roomId !== config.managementRoom) return;
if (roomId !== this.managementRoomId) return;
if (!event['content']) return;
const content = event['content'];
@ -356,7 +355,7 @@ export class Mjolnir {
private async resyncJoinedRooms(withSync = true) {
if (!config.protectAllJoinedRooms) return;
const joinedRoomIds = (await this.client.getJoinedRooms()).filter(r => r !== config.managementRoom);
const joinedRoomIds = (await this.client.getJoinedRooms()).filter(r => r !== this.managementRoomId);
for (const roomId of this.protectedJoinedRoomIds) {
delete this.protectedRooms[roomId];
}
@ -526,7 +525,7 @@ export class Mjolnir {
if (!hadErrors && verbose) {
const html = `<font color="#00cc00">All permissions look OK.</font>`;
const text = "All permissions look OK.";
await this.client.sendMessage(config.managementRoom, {
await this.client.sendMessage(this.managementRoomId, {
msgtype: "m.notice",
body: text,
format: "org.matrix.custom.html",
@ -629,7 +628,7 @@ export class Mjolnir {
if (!hadErrors && verbose) {
const html = `<font color="#00cc00">Done updating rooms - no errors</font>`;
const text = "Done updating rooms - no errors";
await this.client.sendMessage(config.managementRoom, {
await this.client.sendMessage(this.managementRoomId, {
msgtype: "m.notice",
body: text,
format: "org.matrix.custom.html",
@ -661,7 +660,7 @@ export class Mjolnir {
if (!hadErrors) {
const html = `<font color="#00cc00"><b>Done updating rooms - no errors</b></font>`;
const text = "Done updating rooms - no errors";
await this.client.sendMessage(config.managementRoom, {
await this.client.sendMessage(this.managementRoomId, {
msgtype: "m.notice",
body: text,
format: "org.matrix.custom.html",
@ -672,7 +671,7 @@ export class Mjolnir {
private async handleEvent(roomId: string, event: any) {
// Check for UISI errors
if (roomId === config.managementRoom) {
if (roomId === this.managementRoomId) {
if (event['type'] === 'm.room.message' && event['content'] && event['content']['body']) {
if (event['content']['body'] === "** Unable to decrypt: The sender's device has not sent us the keys for this message. **") {
// UISI
@ -703,7 +702,7 @@ export class Mjolnir {
LogService.error("Mjolnir", "Error handling protection: " + protection.name);
LogService.error("Mjolnir", "Failed event: " + eventPermalink);
LogService.error("Mjolnir", extractRequestError(e));
await this.client.sendNotice(config.managementRoom, "There was an error processing an event through a protection - see log for details. Event: " + eventPermalink);
await this.client.sendNotice(this.managementRoomId, "There was an error processing an event through a protection - see log for details. Event: " + eventPermalink);
}
}
@ -776,7 +775,7 @@ export class Mjolnir {
format: "org.matrix.custom.html",
formatted_body: html,
};
await this.client.sendMessage(config.managementRoom, message);
await this.client.sendMessage(this.managementRoomId, message);
return true;
}
@ -814,7 +813,7 @@ export class Mjolnir {
format: "org.matrix.custom.html",
formatted_body: html,
};
await this.client.sendMessage(config.managementRoom, message);
await this.client.sendMessage(this.managementRoomId, message);
return true;
}

View File

@ -43,7 +43,7 @@ export async function applyServerAcls(lists: BanList[], roomIds: string[], mjoln
if (config.verboseLogging) {
// We specifically use sendNotice to avoid having to escape HTML
await mjolnir.client.sendNotice(config.managementRoom, `Constructed server ACL:\n${JSON.stringify(finalAcl, null, 2)}`);
await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Constructed server ACL:\n${JSON.stringify(finalAcl, null, 2)}`);
}
const errors: RoomUpdateError[] = [];

View File

@ -18,7 +18,6 @@ import { Mjolnir } from "../Mjolnir";
import { RichReply } from "matrix-bot-sdk";
import { RECOMMENDATION_BAN, recommendationToStable } from "../models/ListRule";
import { RULE_SERVER, RULE_USER, ruleTypeToStable } from "../models/BanList";
import config from "../config";
// !mjolnir import <room ID> <shortcode>
export async function execImportCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) {
@ -44,7 +43,7 @@ export async function execImportCommand(roomId: string, event: any, mjolnir: Mjo
if (content['membership'] === 'ban') {
const reason = content['reason'] || '<no reason>';
await mjolnir.client.sendNotice(config.managementRoom, `Adding user ${stateEvent['state_key']} to ban list`);
await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Adding user ${stateEvent['state_key']} to ban list`);
const recommendation = recommendationToStable(RECOMMENDATION_BAN);
const ruleContent = {
@ -65,7 +64,7 @@ export async function execImportCommand(roomId: string, event: any, mjolnir: Mjo
for (const server of content['deny']) {
const reason = "<no reason>";
await mjolnir.client.sendNotice(config.managementRoom, `Adding server ${server} to ban list`);
await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Adding server ${server} to ban list`);
const recommendation = recommendationToStable(RECOMMENDATION_BAN);
const ruleContent = {

View File

@ -20,7 +20,6 @@ import { htmlToText } from "html-to-text";
import * as htmlEscape from "escape-html";
import { JSDOM } from 'jsdom';
import config from "../config";
import { Mjolnir } from "../Mjolnir";
/// Regexp, used to extract the action label from an action reaction
@ -113,7 +112,7 @@ export class ReportManager {
* @param reason A reason provided by the reporter.
*/
public async handleServerAbuseReport({ reporterId, event, reason }: { roomId: string, eventId: string, reporterId: string, event: any, reason?: string }) {
return this.displayManager.displayReportAndUI({ kind: Kind.SERVER_ABUSE_REPORT, event, reporterId, reason, moderationRoomId: config.managementRoom });
return this.displayManager.displayReportAndUI({ kind: Kind.SERVER_ABUSE_REPORT, event, reporterId, reason, moderationRoomId: this.mjolnir.managementRoomId });
}
/**
@ -128,7 +127,7 @@ export class ReportManager {
return;
}
if (roomId !== config.managementRoom) {
if (roomId !== this.mjolnir.managementRoomId) {
// Let's not accept commands in rooms other than the management room.
return;
}
@ -201,7 +200,7 @@ export class ReportManager {
})
} else {
LogService.info("ReportManager::handleReaction", "User", event["sender"], "cancelled action", matches[1]);
this.mjolnir.client.redactEvent(config.managementRoom, relation.event_id, "Action cancelled");
this.mjolnir.client.redactEvent(this.mjolnir.managementRoomId, relation.event_id, "Action cancelled");
}
return;
@ -235,15 +234,15 @@ export class ReportManager {
};
confirmation[ABUSE_ACTION_CONFIRMATION_KEY] = confirmationReport;
let requestConfirmationEventId = await this.mjolnir.client.sendMessage(config.managementRoom, confirmation);
await this.mjolnir.client.sendEvent(config.managementRoom, "m.reaction", {
let requestConfirmationEventId = await this.mjolnir.client.sendMessage(this.mjolnir.managementRoomId, confirmation);
await this.mjolnir.client.sendEvent(this.mjolnir.managementRoomId, "m.reaction", {
"m.relates_to": {
"rel_type": "m.annotation",
"event_id": requestConfirmationEventId,
"key": `🆗 ${action.emoji} ${await action.title(this, initialNoticeReport)} [${action.label}][${CONFIRM}]`
}
});
await this.mjolnir.client.sendEvent(config.managementRoom, "m.reaction", {
await this.mjolnir.client.sendEvent(this.mjolnir.managementRoomId, "m.reaction", {
"m.relates_to": {
"rel_type": "m.annotation",
"event_id": requestConfirmationEventId,
@ -287,7 +286,7 @@ export class ReportManager {
let response;
try {
// Check security.
if (moderationRoomId === config.managementRoom) {
if (moderationRoomId === this.mjolnir.managementRoomId) {
// Always accept actions executed from the management room.
} else {
throw new Error("Security error: Cannot execute this action.");
@ -297,14 +296,14 @@ export class ReportManager {
error = ex;
}
if (error) {
this.mjolnir.client.sendEvent(config.managementRoom, "m.reaction", {
this.mjolnir.client.sendEvent(this.mjolnir.managementRoomId, "m.reaction", {
"m.relates_to": {
"rel_type": "m.annotation",
"event_id": failureEventId,
"key": `${action.emoji}`
}
});
this.mjolnir.client.sendEvent(config.managementRoom, "m.notice", {
this.mjolnir.client.sendEvent(this.mjolnir.managementRoomId, "m.notice", {
"body": error.message || "<unknown error>",
"m.relationship": {
"rel_type": "m.reference",
@ -312,7 +311,7 @@ export class ReportManager {
}
})
} else {
this.mjolnir.client.sendEvent(config.managementRoom, "m.reaction", {
this.mjolnir.client.sendEvent(this.mjolnir.managementRoomId, "m.reaction", {
"m.relates_to": {
"rel_type": "m.annotation",
"event_id": successEventId,
@ -320,10 +319,10 @@ export class ReportManager {
}
});
if (onSuccessRemoveEventId) {
this.mjolnir.client.redactEvent(config.managementRoom, onSuccessRemoveEventId, "Action complete");
this.mjolnir.client.redactEvent(this.mjolnir.managementRoomId, onSuccessRemoveEventId, "Action complete");
}
if (response) {
this.mjolnir.client.sendMessage(config.managementRoom, {
this.mjolnir.client.sendMessage(this.mjolnir.managementRoomId, {
msgtype: "m.notice",
"formatted_body": response,
format: "org.matrix.custom.html",
@ -460,7 +459,7 @@ class IgnoreBadReport implements IUIAction {
return "Ignore bad report";
}
public async execute(manager: ReportManager, report: IReportWithAction): Promise<string | undefined> {
await manager.mjolnir.client.sendEvent(config.managementRoom, "m.room.message",
await manager.mjolnir.client.sendEvent(manager.mjolnir.managementRoomId, "m.room.message",
{
msgtype: "m.notice",
body: "Report classified as invalid",
@ -622,7 +621,7 @@ class EscalateToServerModerationRoom implements IUIAction {
public emoji = "⏫";
public needsConfirmation = true;
public async canExecute(manager: ReportManager, report: IReport, moderationRoomId: string): Promise<boolean> {
if (moderationRoomId === config.managementRoom) {
if (moderationRoomId === manager.mjolnir.managementRoomId) {
// We're already at the top of the chain.
return false;
}
@ -651,7 +650,7 @@ class EscalateToServerModerationRoom implements IUIAction {
// - `moderationRoomId`: statically known good;
// - `reporterId`: we trust `report`, could be forged by a moderator, low impact;
// - `event`: checked just before.
await displayManager.displayReportAndUI({ kind: Kind.ESCALATED_REPORT, reporterId: report.reporter_id, moderationRoomId: config.managementRoom, event });
await displayManager.displayReportAndUI({ kind: Kind.ESCALATED_REPORT, reporterId: report.reporter_id, moderationRoomId: manager.mjolnir.managementRoomId, event });
return;
}
}
@ -867,7 +866,7 @@ class DisplayManager {
};
notice[ABUSE_REPORT_KEY] = report;
let noticeEventId = await this.owner.mjolnir.client.sendMessage(config.managementRoom, notice);
let noticeEventId = await this.owner.mjolnir.client.sendMessage(this.owner.mjolnir.managementRoomId, notice);
if (kind !== Kind.ERROR) {
// Now let's display buttons.
for (let [label, action] of ACTIONS) {
@ -875,7 +874,7 @@ class DisplayManager {
if (!await action.canExecute(this.owner, report, moderationRoomId)) {
continue;
}
await this.owner.mjolnir.client.sendEvent(config.managementRoom, "m.reaction", {
await this.owner.mjolnir.client.sendEvent(this.owner.mjolnir.managementRoomId, "m.reaction", {
"m.relates_to": {
"rel_type": "m.annotation",
"event_id": noticeEventId,

View File

@ -1,7 +1,7 @@
import { strict as assert } from "assert";
import config from "../../src/config";
import { matrixClient, mjolnir } from "./mjolnirSetupUtils";
import { matrixClient } from "./mjolnirSetupUtils";
import { newTestUser } from "./clientHelper";
import { ReportManager, ABUSE_ACTION_CONFIRMATION_KEY, ABUSE_REPORT_KEY } from "../../src/report/ReportManager";
@ -26,7 +26,7 @@ describe("Test: Reporting abuse", async () => {
// Listen for any notices that show up.
let notices = [];
matrixClient().on("room.event", (roomId, event) => {
if (roomId = config.managementRoom) {
if (roomId = this.mjolnir.managementRoomId) {
notices.push(event);
}
});
@ -221,15 +221,15 @@ describe("Test: Reporting abuse", async () => {
// Listen for any notices that show up.
let notices = [];
matrixClient().on("room.event", (roomId, event) => {
if (roomId = config.managementRoom) {
if (roomId = this.mjolnir.managementRoomId) {
notices.push(event);
}
});
// Create a moderator.
let moderatorUser = await newTestUser(false, "reacting-abuse-moderator-user");
matrixClient().inviteUser(await moderatorUser.getUserId(), config.managementRoom);
await moderatorUser.joinRoom(config.managementRoom);
matrixClient().inviteUser(await moderatorUser.getUserId(), this.mjolnir.managementRoomId);
await moderatorUser.joinRoom(this.mjolnir.managementRoomId);
// Create a few users and a room.
let goodUser = await newTestUser(false, "reacting-abuse-good-user");
@ -312,7 +312,7 @@ describe("Test: Reporting abuse", async () => {
for (let button of buttons) {
if (button["content"]["m.relates_to"]["key"].includes("[redact-message]")) {
redactButtonId = button["event_id"];
await moderatorUser.sendEvent(config.managementRoom, "m.reaction", button["content"]);
await moderatorUser.sendEvent(this.mjolnir.managementRoomId, "m.reaction", button["content"]);
break;
}
}
@ -339,7 +339,7 @@ describe("Test: Reporting abuse", async () => {
// It's the confirm button, click it!
confirmEventId = event["event_id"];
await moderatorUser.sendEvent(config.managementRoom, "m.reaction", event["content"]);
await moderatorUser.sendEvent(this.mjolnir.managementRoomId, "m.reaction", event["content"]);
break;
}
assert.ok(confirmEventId, "We should have found the confirm button");

View File

@ -20,7 +20,7 @@ import { onReactionTo } from "./commandUtils";
let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]});
await moderator.setUserPowerLevel(mjolnirUserId, targetRoom, 100);
await badUser.joinRoom(targetRoom);
moderator.sendMessage(config.managementRoom, {msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}`});
moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}`});
LogService.debug("redactionTest", `targetRoom: ${targetRoom}, managementRoom: ${config.managementRoom}`);
// Sandwich irrelevant messages in bad messages.
@ -34,8 +34,8 @@ import { onReactionTo } from "./commandUtils";
try {
moderator.start();
await onReactionTo(moderator, config.managementRoom, '✅', async () => {
return await moderator.sendMessage(config.managementRoom, { msgtype: 'm.text', body: `!mjolnir redact ${badUserId} ${targetRoom}` });
await onReactionTo(moderator, this.mjolnir.managementRoomId, '✅', async () => {
return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir redact ${badUserId} ${targetRoom}` });
});
} finally {
moderator.stop();
@ -66,7 +66,7 @@ import { onReactionTo } from "./commandUtils";
let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]});
await moderator.setUserPowerLevel(mjolnirUserId, targetRoom, 100);
await badUser.joinRoom(targetRoom);
await moderator.sendMessage(config.managementRoom, {msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}`});
await moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}`});
targetRooms.push(targetRoom);
// Sandwich irrelevant messages in bad messages.
@ -81,8 +81,8 @@ import { onReactionTo } from "./commandUtils";
try {
moderator.start();
await onReactionTo(moderator, config.managementRoom, '✅', async () => {
return await moderator.sendMessage(config.managementRoom, { msgtype: 'm.text', body: `!mjolnir redact ${badUserId}` });
await onReactionTo(moderator, this.mjolnir.managementRoomId, '✅', async () => {
return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir redact ${badUserId}` });
});
} finally {
moderator.stop();
@ -112,13 +112,13 @@ import { onReactionTo } from "./commandUtils";
let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]});
await moderator.setUserPowerLevel(mjolnirUserId, targetRoom, 100);
await badUser.joinRoom(targetRoom);
moderator.sendMessage(config.managementRoom, {msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}`});
moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}`});
let eventToRedact = await badUser.sendMessage(targetRoom, {msgtype: 'm.text', body: "Very Bad Stuff"});
try {
moderator.start();
await onReactionTo(moderator, config.managementRoom, '✅', async () => {
return await moderator.sendMessage(config.managementRoom, {msgtype: 'm.text', body: `!mjolnir redact https://matrix.to/#/${encodeURIComponent(targetRoom)}/${encodeURIComponent(eventToRedact)}`});
await onReactionTo(moderator, this.mjolnir.managementRoomId, '✅', async () => {
return await moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text', body: `!mjolnir redact https://matrix.to/#/${encodeURIComponent(targetRoom)}/${encodeURIComponent(eventToRedact)}`});
});
} finally {
moderator.stop();

View File

@ -20,12 +20,8 @@ export const mochaHooks = {
afterEach: [
async function() {
await this.mjolnir.stop();
// Mjolnir resolves config.managementRoom and overwrites it, so we undo this here
// after stopping Mjolnir for the next time we setup a Mjolnir and a management room.
let managementRoomId = config.managementRoom;
config.managementRoom = this.managementRoomAlias;
// remove alias from management room and leave it.
await teardownManagementRoom(this.mjolnir.client, managementRoomId, this.managementRoomAlias);
await teardownManagementRoom(this.mjolnir.client, this.mjolnir.managementRoomId, config.managementRoom);
}
]
};

View File

@ -12,19 +12,18 @@ describe("Test: !help command", function() {
})
it('Mjolnir responded to !mjolnir help', async function() {
this.timeout(30000);
console.log(`management room ${config.managementRoom}`);
// send a messgage
await client.joinRoom(config.managementRoom);
// listener for getting the event reply
let reply = new Promise((resolve, reject) => {
client.on('room.message', noticeListener(config.managementRoom, (event) => {
client.on('room.message', noticeListener(this.mjolnir.managementRoomId, (event) => {
if (event.content.body.includes("Print status information")) {
resolve(event);
}
}))});
// check we get one back
console.log(config);
await client.sendMessage(config.managementRoom, {msgtype: "m.text", body: "!mjolnir help"})
await client.sendMessage(this.mjolnir.managementRoomId, {msgtype: "m.text", body: "!mjolnir help"})
await reply
})
})