diff --git a/src/LogProxy.ts b/src/LogProxy.ts index 47699e2..417ce8e 100644 --- a/src/LogProxy.ts +++ b/src/LogProxy.ts @@ -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); diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index 6ceef83..04d8def 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -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.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 = `All permissions look OK.`; 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 = `Done updating rooms - no errors`; 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 = `Done updating rooms - no errors`; 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; } diff --git a/src/actions/ApplyAcl.ts b/src/actions/ApplyAcl.ts index 14d397a..e8b40a3 100644 --- a/src/actions/ApplyAcl.ts +++ b/src/actions/ApplyAcl.ts @@ -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[] = []; diff --git a/src/commands/ImportCommand.ts b/src/commands/ImportCommand.ts index e68ab3d..077eb3f 100644 --- a/src/commands/ImportCommand.ts +++ b/src/commands/ImportCommand.ts @@ -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 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'] || ''; - 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 = ""; - 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 = { diff --git a/src/report/ReportManager.ts b/src/report/ReportManager.ts index 7b7b561..0b7c66e 100644 --- a/src/report/ReportManager.ts +++ b/src/report/ReportManager.ts @@ -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 || "", "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 { - 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 { - 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, diff --git a/test/integration/abuseReportTest.ts b/test/integration/abuseReportTest.ts index e79d332..b5b23c0 100644 --- a/test/integration/abuseReportTest.ts +++ b/test/integration/abuseReportTest.ts @@ -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"); diff --git a/test/integration/commands/redactCommandTest.ts b/test/integration/commands/redactCommandTest.ts index 5cb6320..153abab 100644 --- a/test/integration/commands/redactCommandTest.ts +++ b/test/integration/commands/redactCommandTest.ts @@ -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(); diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index 9e78cb2..5fe3347 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -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); } ] }; diff --git a/test/integration/helloTest.ts b/test/integration/helloTest.ts index 91c083d..f82018a 100644 --- a/test/integration/helloTest.ts +++ b/test/integration/helloTest.ts @@ -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 }) })