From 1abc80ecb149a30dec011737ff9510ecdced4820 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 20 Mar 2026 19:49:49 -0600 Subject: [PATCH 1/3] Set and sync policy server configuration in protected rooms Desired behaviour: * When syncing policy list changes, ensure the policy server is set in the rooms (if applicable). * Allow the policy server to be set in the config to avoid unsetting it on startup in already-protected rooms. * Allow the policy server to be changed with commands. This enables operators to pick a different policy server than the config at runtime. * Don't migrate to stable event types if only the unstable event type is sent. It's assumed that an external tool will take care of migrating. * Works with `protectAllJoinedRooms` to set the policy server state event on sync. This does *not* ensure that the policy server is actually joined to each of the rooms. A later PR/feature will make that work. --- config/default.yaml | 5 ++ src/Mjolnir.ts | 31 +++++++++ src/PolicyServer.ts | 58 ++++++++++++++++ src/ProtectedRoomsSet.ts | 91 +++++++++++++++++++++++++- src/commands/CommandHandler.ts | 6 +- src/commands/SetPolicyServerCommand.ts | 44 +++++++++++++ src/commands/StatusCommand.ts | 3 + src/config.ts | 2 + 8 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 src/PolicyServer.ts create mode 100644 src/commands/SetPolicyServerCommand.ts diff --git a/config/default.yaml b/config/default.yaml index 6073d9d1..5049f1d3 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -134,6 +134,11 @@ protectedRooms: # Explicitly add these rooms as a protected room list if you want them protected. protectAllJoinedRooms: false +# Uncomment and populate this to set the default policy server to use. This can be +# overridden with the `!mjolnir policy_server` command. For an example policy server, +# see https://github.com/matrix-org/policyserv +#defaultPolicyServer: "beta2.matrix.org" + # Increase this delay to have Mjölnir wait longer between two consecutive backgrounded # operations. The total duration of operations will be longer, but the homeserver won't # be affected as much. Conversely, decrease this delay to have Mjölnir chain operations diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index d94bdf1a..da8e5686 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -45,6 +45,7 @@ import { OpenMetrics } from "./webapis/OpenMetrics"; import { LRUCache } from "lru-cache"; import { ModCache } from "./ModCache"; import { MASClient } from "./MASClient"; +import { PolicyServer } from "./PolicyServer"; export const STATE_NOT_STARTED = "not_started"; export const STATE_CHECKING_PERMISSIONS = "checking_permissions"; @@ -57,6 +58,11 @@ export const STATE_RUNNING = "running"; */ export const REPORT_POLL_EVENT_TYPE = "org.matrix.mjolnir.report_poll"; +/** + * The account data type which stores the policy server name (if set). + */ +const POLICY_SERVER_CONFIG_ACCOUNT_DATA_TYPE = "org.matrix.mjolnir.policy_server_config"; + export class Mjolnir { private displayName: string; private localpart: string; @@ -374,6 +380,22 @@ export class Mjolnir { console.log("Starting web server"); await this.webapis.start(); + // Get policy server configuration + try { + const policyServerData = await this.client.getAccountData<{ name: string | undefined }>(POLICY_SERVER_CONFIG_ACCOUNT_DATA_TYPE); + await this.protectedRoomsTracker.setPolicyServer(policyServerData.name ? new PolicyServer(policyServerData.name) : undefined, true); + } catch (e) { + if (e.body?.errcode !== "M_NOT_FOUND") { + throw e; + } + + // else account data wasn't found - use default from config + await this.protectedRoomsTracker.setPolicyServer(this.config.defaultPolicyServer ? new PolicyServer(this.config.defaultPolicyServer) : undefined, true); + } + LogService.info("Mjolnir", `Policy server name set to: ${this.protectedRoomsTracker.policyServer?.name}`); + // We log the key primarily to seed the cache before doing work with it. + LogService.info("Mjolnir", `Policy server public key is: ${await this.protectedRoomsTracker.policyServer?.getEd25519Key()}`); + if (this.reportPoller) { let reportPollSetting: { from: number } = { from: 0 }; try { @@ -471,6 +493,15 @@ export class Mjolnir { return this.protectedRoomsConfig.getExplicitlyProtectedRooms(); } + /** + * Sets the policy server to use in all protected rooms. This will cause it to be applied immediately. + * @param server The policy server to use. + */ + public async setPolicyServer(server: PolicyServer | undefined): Promise { + await this.client.setAccountData(POLICY_SERVER_CONFIG_ACCOUNT_DATA_TYPE, { name: server?.name }); + return this.protectedRoomsTracker.setPolicyServer(server); + } + /** * Explicitly protect this room, adding it to the account data. * Should NOT be used to protect a room to implement e.g. `config.protectAllJoinedRooms`, diff --git a/src/PolicyServer.ts b/src/PolicyServer.ts new file mode 100644 index 00000000..4091a72a --- /dev/null +++ b/src/PolicyServer.ts @@ -0,0 +1,58 @@ +/* +Copyright 2026 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { LogService } from "@vector-im/matrix-bot-sdk"; + +export class PolicyServer { + private ed25519Key: string | undefined; + private lastCheck: Date; + + constructor(public readonly name: string) { + this.lastCheck = new Date(0); + } + + public async getEd25519Key(): Promise { + const keyStillFresh = (this.lastCheck.getTime() + 1000 * 60 * 60 * 24) > Date.now(); // valid for 24 hours + if (this.ed25519Key && keyStillFresh) { + return this.ed25519Key; + } + + const errorStillFresh = (this.lastCheck.getTime() + 1000 * 60 * 60) > Date.now(); // errors are valid for 1 hour + if (!this.ed25519Key && errorStillFresh) { + return undefined; + } + + this.lastCheck = new Date(); + + // As per spec/MSC4284 + const response = await fetch(`https://${this.name}/.well-known/matrix/policy_server`); + if (!response.ok) { + LogService.warn("PolicyServer", `Failed to fetch ed25519 key for ${this.name}: ${response.statusText}`); + this.ed25519Key = undefined; + return undefined; + } + + const keyInfo = await response.json(); + if (typeof keyInfo !== "object" || typeof keyInfo.public_keys !== "object" || typeof keyInfo.public_keys.ed25519 !== "string") { + LogService.warn("PolicyServer", `Failed to parse ed25519 key for ${this.name}: invalid response or no key`); + this.ed25519Key = undefined; + return undefined; + } + + this.ed25519Key = keyInfo.public_keys.ed25519; + return this.ed25519Key; + } +} \ No newline at end of file diff --git a/src/ProtectedRoomsSet.ts b/src/ProtectedRoomsSet.ts index 1a8c6a7e..09fd1dcc 100644 --- a/src/ProtectedRoomsSet.ts +++ b/src/ProtectedRoomsSet.ts @@ -28,6 +28,7 @@ import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQu import { ProtectedRoomActivityTracker } from "./queues/ProtectedRoomActivityTracker"; import { getMXCsInMessage, htmlEscape } from "./utils"; import { ModCache } from "./ModCache"; +import { PolicyServer } from "./PolicyServer"; const KEEP_MEDIA_EVENTS_FOR_MS = 4 * 24 * 60 * 60 * 1000; @@ -104,6 +105,8 @@ export class ProtectedRoomsSet { /** The last revision we used to sync protected rooms. */ Revision >(); + private enabledPolicyServer: PolicyServer | undefined; + /** * whether the mjolnir instance is server admin */ @@ -139,6 +142,21 @@ export class ProtectedRoomsSet { this.listUpdateListener = this.syncWithUpdatedPolicyList.bind(this); } + public async setPolicyServer(server: PolicyServer | undefined, skipSync = false): Promise { + if (server?.name !== this.enabledPolicyServer?.name) { + this.enabledPolicyServer = server; + } + + if (!skipSync) { + const errors = await this.applyPolicyServerConfig(this.protectedRoomsByActivity()); + await this.printActionResult(errors, "Errors updating policy server config:"); + } + } + + public get policyServer(): PolicyServer | undefined { + return this.enabledPolicyServer; + } + /** * Queue a user's messages in a room for redaction once we have stopped synchronizing bans * over the protected rooms. @@ -261,14 +279,16 @@ export class ProtectedRoomsSet { */ private async syncRoomsWithPolicies() { let hadErrors = false; - const [aclErrors, banErrors] = await Promise.all([ + const [aclErrors, banErrors, psErrors] = await Promise.all([ this.applyServerAcls(this.policyLists, this.protectedRoomsByActivity()), this.applyUserBans(this.protectedRoomsByActivity()), + this.applyPolicyServerConfig(this.protectedRoomsByActivity()), ]); const redactionErrors = await this.processRedactionQueue(); hadErrors = hadErrors || (await this.printActionResult(aclErrors, "Errors updating server ACLs:")); hadErrors = hadErrors || (await this.printActionResult(banErrors, "Errors updating member bans:")); hadErrors = hadErrors || (await this.printActionResult(redactionErrors, "Error updating redactions:")); + hadErrors = hadErrors || (await this.printActionResult(psErrors, "Error updating policy server config:")); if (!hadErrors) { const html = `Done updating rooms - no errors`; @@ -340,6 +360,75 @@ export class ProtectedRoomsSet { await this.printBanlistChanges(changes, policyList); } + private async applyPolicyServerConfig(roomIds: string[]): Promise { + const errors: RoomUpdateError[] = []; + for (const roomId of roomIds) { + await this.managementRoomOutput.logMessage( + LogLevel.DEBUG, + "ApplyPolicyServerConfig", + `Checking policy server config in ${roomId}`, + roomId, + ); + + try { + let currentPolicyServerName: string | undefined | null = null; // string=name, undefined=explict not set, null=unknown + try { + const content = await this.client.getRoomStateEventContent(roomId, "m.room.policy", ""); + currentPolicyServerName = content["via"] as string | undefined; + } catch (e) { + // ignore error and fall back to unstable type + try { + const content = await this.client.getRoomStateEventContent(roomId, "org.matrix.msc4284.policy", ""); + currentPolicyServerName = content["via"] as string | undefined; + } catch (e) { + // ignore - assume no policy server config + } + } + + // Because we use null to represent unknown, this won't trigger when we're unable to get the current state. + if (currentPolicyServerName === this.enabledPolicyServer?.name) { + await this.managementRoomOutput.logMessage( + LogLevel.DEBUG, + "ApplyPolicyServerConfig", + `Skipping policy server config in ${roomId} because the server name already matches`, + roomId, + ); + continue; + } + + await this.managementRoomOutput.logMessage( + LogLevel.DEBUG, + "ApplyPolicyServerConfig", + `Updating policy server config in ${roomId}`, + roomId, + ); + if (this.enabledPolicyServer) { + // We expect the homeserver to deduplicate the state event setting for us. + const ed25519Key = await this.enabledPolicyServer.getEd25519Key(); + await this.client.sendStateEvent(roomId, "m.room.policy", "", { + via: this.enabledPolicyServer.name, + public_keys: { + ed25519: ed25519Key, + }, + }); + // We also set the unstable, though this is less important as time goes on + await this.client.sendStateEvent(roomId, "org.matrix.msc4284.policy", "", { + via: this.enabledPolicyServer.name, + public_key: ed25519Key, + }); + } else { + // "Remove" the policy server by unsetting the state events + await this.client.sendStateEvent(roomId, "m.room.policy", "", {}); + await this.client.sendStateEvent(roomId, "org.matrix.msc4284.policy", "", {}); + } + } catch (e) { + errors.push({ roomId, errorMessage: e.message, errorKind: ERROR_KIND_FATAL }); + } + } + + return errors; + } + /** * Applies the server ACLs represented by the ban lists to the provided rooms, returning the * room IDs that could not be updated and their error. diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index 866e93ec..8dc01fe9 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -54,6 +54,7 @@ import { execIgnoreCommand, execListIgnoredCommand } from "./IgnoreCommand"; import { execLockCommand } from "./LockCommand"; import { execUnlockCommand } from "./UnlockCommand"; import { execQuarantineMediaCommand } from "./QuarantineMediaCommand"; +import {execSetPolicyServerCommand} from "./SetPolicyServerCommand"; export const COMMAND_PREFIX = "!mjolnir"; @@ -155,6 +156,8 @@ export async function handleCommand(roomId: string, event: { content: { body: st return await execLockCommand(roomId, event, mjolnir, parts); } else if (parts[1] === "unlock") { return await execUnlockCommand(roomId, event, mjolnir, parts); + } else if (parts[1] === "policy_server") { + return await execSetPolicyServerCommand(roomId, event, mjolnir, parts); } else if (parts[1] === "help") { // Help menu const protectionMenu = @@ -198,7 +201,8 @@ export async function handleCommand(roomId: string, event: { content: { body: st "!mjolnir default - Sets the default list for commands\n" + "!mjolnir rules - Lists the rules currently in use by Mjolnir\n" + "!mjolnir rules matching - Lists the rules in use that will match this entity e.g. `!rules matching @foo:example.com` will show all the user and server rules, including globs, that match this user\n" + - "!mjolnir sync - Force updates of all lists and re-apply rules\n"; + "!mjolnir sync - Force updates of all lists and re-apply rules\n" + + "!mjolnir policy_server - Sets the policy server name in protected rooms, or removes it if 'unset' is given\n"; const roomsMenu = "" + diff --git a/src/commands/SetPolicyServerCommand.ts b/src/commands/SetPolicyServerCommand.ts new file mode 100644 index 00000000..b6436549 --- /dev/null +++ b/src/commands/SetPolicyServerCommand.ts @@ -0,0 +1,44 @@ +/* +Copyright 2026 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Mjolnir } from "../Mjolnir"; +import { RichReply } from "@vector-im/matrix-bot-sdk"; +import { PolicyServer } from "../PolicyServer"; + +// !mjolnir policy_server +export async function execSetPolicyServerCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { + if (parts.length !== 3) { + await mjolnir.client.replyNotice(roomId, event, "Usage: !mjolnir policy_server "); + return; + } + + const name = parts[2].toLowerCase(); + const server = name === "unset" ? undefined : new PolicyServer(name); + + if (server) { + const key = await server.getEd25519Key(); + if (!key) { + const replyText = "Could not find a valid key for the policy server."; + const reply = RichReply.createFor(roomId, event, replyText, replyText); + reply["msgtype"] = "m.notice"; + await mjolnir.client.sendMessage(roomId, reply); + return; + } + } + + await mjolnir.setPolicyServer(server); + await mjolnir.client.unstableApis.addReactionToEvent(roomId, event["event_id"], "✅"); +} diff --git a/src/commands/StatusCommand.ts b/src/commands/StatusCommand.ts index 49edf979..0a1728bc 100644 --- a/src/commands/StatusCommand.ts +++ b/src/commands/StatusCommand.ts @@ -72,6 +72,9 @@ async function showMjolnirStatus(roomId: string, event: any, mjolnir: Mjolnir) { html += `Protected rooms: ${mjolnir.protectedRoomsTracker.getProtectedRooms().length}
`; text += `Protected rooms: ${mjolnir.protectedRoomsTracker.getProtectedRooms().length}\n`; + html += `Policy server name: ${mjolnir.protectedRoomsTracker.policyServer?.name}
`; + text += `Policy server name: ${mjolnir.protectedRoomsTracker.policyServer?.name}\n`; + // Append list information const renderPolicyLists = (header: string, lists: PolicyList[]) => { html += `${header}:
    `; diff --git a/src/config.ts b/src/config.ts index 2280fd71..8123317e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -106,6 +106,7 @@ export interface IConfig { fasterMembershipChecks: boolean; automaticallyRedactForReasons: string[]; // case-insensitive globs protectAllJoinedRooms: boolean; + defaultPolicyServer: string; /** * Backgrounded tasks: number of milliseconds to wait between the completion * of one background task and the start of the next one. @@ -236,6 +237,7 @@ const defaultConfig: IConfig = { fasterMembershipChecks: false, automaticallyRedactForReasons: ["spam", "advertising"], protectAllJoinedRooms: false, + defaultPolicyServer: "", backgroundDelayMS: 500, pollReports: false, displayReports: true, From b8077b723574b98a3bc274b3137b44f6ce04c765 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 21 Mar 2026 16:15:28 -0600 Subject: [PATCH 2/3] Add some tests This can't test all of the behaviour because the test harness is relatively strict. Ideally, we'd be able to specify distinct config for each test, but we can't. --- src/PolicyServer.ts | 24 +++++++- test/integration/policyServerTest.ts | 91 ++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 test/integration/policyServerTest.ts diff --git a/src/PolicyServer.ts b/src/PolicyServer.ts index 4091a72a..bb53c7f0 100644 --- a/src/PolicyServer.ts +++ b/src/PolicyServer.ts @@ -19,11 +19,25 @@ import { LogService } from "@vector-im/matrix-bot-sdk"; export class PolicyServer { private ed25519Key: string | undefined; private lastCheck: Date; + private serverNameOverride: string | undefined; + + constructor(private serverName: string) { + // Check for HTTP URIs in the server name, just in case we're running a test + if (this.serverName.startsWith("http://")) { + const uri = new URL(this.serverName); + this.serverNameOverride = uri.hostname; + } - constructor(public readonly name: string) { this.lastCheck = new Date(0); } + public get name(): string { + if (this.serverNameOverride) { + return this.serverNameOverride; + } + return this.serverName; + } + public async getEd25519Key(): Promise { const keyStillFresh = (this.lastCheck.getTime() + 1000 * 60 * 60 * 24) > Date.now(); // valid for 24 hours if (this.ed25519Key && keyStillFresh) { @@ -38,7 +52,13 @@ export class PolicyServer { this.lastCheck = new Date(); // As per spec/MSC4284 - const response = await fetch(`https://${this.name}/.well-known/matrix/policy_server`); + // We allow HTTP URIs in the server name for testing purposes + let schemeAndHostname = `https://${this.name}`; // will be the hostname if an HTTP link, per constructor + if (this.serverName.startsWith("http://")) { // this is the unnormalized name + LogService.warn("PolicyServer", "Using non-HTTP URI for policy server: " + this.serverName); + schemeAndHostname = this.serverName; + } + const response = await fetch(`${schemeAndHostname}/.well-known/matrix/policy_server`); if (!response.ok) { LogService.warn("PolicyServer", `Failed to fetch ed25519 key for ${this.name}: ${response.statusText}`); this.ed25519Key = undefined; diff --git a/test/integration/policyServerTest.ts b/test/integration/policyServerTest.ts new file mode 100644 index 00000000..5ca04d0c --- /dev/null +++ b/test/integration/policyServerTest.ts @@ -0,0 +1,91 @@ +/* +Copyright 2026 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {Mjolnir} from "../../src/Mjolnir"; +import * as http from "node:http"; +import {AddressInfo} from "node:net"; +import { strict as assert } from "assert"; + +describe("Test: Policy Servers", function () { + const ed25519Key = "this would be a real unpadded base64 key in production"; + let mjolnir: Mjolnir; + let lookInRoomId: string; + let policyServerUrl: string; + let policyServer: http.Server; + + function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + beforeEach(async function () { + mjolnir = this.config.RUNTIME.client!; + policyServer = http.createServer((req, res) => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ + "public_keys": { + "ed25519": ed25519Key, + }, + })); + }); + // grab any port by not specifying one to listen on + policyServer.listen(() => { + policyServerUrl = `http://localhost:${(policyServer.address()! as AddressInfo).port}` + }); + + // Create a room we can inspect + lookInRoomId = await mjolnir.client.createRoom(); + await mjolnir.client.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text", + body: `!mjolnir rooms add ${lookInRoomId}`, + }); + }); + + afterEach(async function () { + policyServer.close(); + }); + + it("should set the policy server information on demand", async function () { + this.timeout(15000); + + // Verify the room does *not* have a policy server set + try { + await mjolnir.client.getRoomStateEventContent(lookInRoomId, "m.room.policy", ""); + assert.fail("Room should not have a policy server set"); + } catch (e) { + assert.equal(e.statusCode, 404); + } + + // Set the policy server, wait a bit, then check for it + await mjolnir.client.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text", + body: `!mjolnir policy_server ${policyServerUrl}`, + }); + await delay(1500); + const policyServerContent = await mjolnir.client.getRoomStateEventContent(lookInRoomId, "m.room.policy", ""); + assert.equal(policyServerContent.url, policyServerUrl); + assert.equal((policyServerContent.public_keys! as Record).ed25519, ed25519Key); + + // Now unset it, wait a bit more, then check for lack of server again + await mjolnir.client.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text", + body: `!mjolnir policy_server unset`, + }); + await delay(1500); + const policyServerContent = await mjolnir.client.getRoomStateEventContent(lookInRoomId, "m.room.policy", ""); + assert.equal(policyServerContent.url, undefined); + assert.equal(policyServerContent.public_keys, undefined); + }); +}); \ No newline at end of file From 49b7bb663d957618b1a3bce95f87d9eee6f8cea0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 21 Mar 2026 16:17:46 -0600 Subject: [PATCH 3/3] Fix linting & JS syntax --- src/Mjolnir.ts | 19 +++++++++++++++---- src/PolicyServer.ts | 15 ++++++++++----- src/ProtectedRoomsSet.ts | 6 +++++- src/commands/CommandHandler.ts | 2 +- test/integration/policyServerTest.ts | 24 +++++++++++++----------- 5 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index da8e5686..ad174ce9 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -382,19 +382,30 @@ export class Mjolnir { // Get policy server configuration try { - const policyServerData = await this.client.getAccountData<{ name: string | undefined }>(POLICY_SERVER_CONFIG_ACCOUNT_DATA_TYPE); - await this.protectedRoomsTracker.setPolicyServer(policyServerData.name ? new PolicyServer(policyServerData.name) : undefined, true); + const policyServerData = await this.client.getAccountData<{ name: string | undefined }>( + POLICY_SERVER_CONFIG_ACCOUNT_DATA_TYPE, + ); + await this.protectedRoomsTracker.setPolicyServer( + policyServerData.name ? new PolicyServer(policyServerData.name) : undefined, + true, + ); } catch (e) { if (e.body?.errcode !== "M_NOT_FOUND") { throw e; } // else account data wasn't found - use default from config - await this.protectedRoomsTracker.setPolicyServer(this.config.defaultPolicyServer ? new PolicyServer(this.config.defaultPolicyServer) : undefined, true); + await this.protectedRoomsTracker.setPolicyServer( + this.config.defaultPolicyServer ? new PolicyServer(this.config.defaultPolicyServer) : undefined, + true, + ); } LogService.info("Mjolnir", `Policy server name set to: ${this.protectedRoomsTracker.policyServer?.name}`); // We log the key primarily to seed the cache before doing work with it. - LogService.info("Mjolnir", `Policy server public key is: ${await this.protectedRoomsTracker.policyServer?.getEd25519Key()}`); + LogService.info( + "Mjolnir", + `Policy server public key is: ${await this.protectedRoomsTracker.policyServer?.getEd25519Key()}`, + ); if (this.reportPoller) { let reportPollSetting: { from: number } = { from: 0 }; diff --git a/src/PolicyServer.ts b/src/PolicyServer.ts index bb53c7f0..1f50aa5f 100644 --- a/src/PolicyServer.ts +++ b/src/PolicyServer.ts @@ -39,12 +39,12 @@ export class PolicyServer { } public async getEd25519Key(): Promise { - const keyStillFresh = (this.lastCheck.getTime() + 1000 * 60 * 60 * 24) > Date.now(); // valid for 24 hours + const keyStillFresh = this.lastCheck.getTime() + 1000 * 60 * 60 * 24 > Date.now(); // valid for 24 hours if (this.ed25519Key && keyStillFresh) { return this.ed25519Key; } - const errorStillFresh = (this.lastCheck.getTime() + 1000 * 60 * 60) > Date.now(); // errors are valid for 1 hour + const errorStillFresh = this.lastCheck.getTime() + 1000 * 60 * 60 > Date.now(); // errors are valid for 1 hour if (!this.ed25519Key && errorStillFresh) { return undefined; } @@ -54,7 +54,8 @@ export class PolicyServer { // As per spec/MSC4284 // We allow HTTP URIs in the server name for testing purposes let schemeAndHostname = `https://${this.name}`; // will be the hostname if an HTTP link, per constructor - if (this.serverName.startsWith("http://")) { // this is the unnormalized name + if (this.serverName.startsWith("http://")) { + // this is the unnormalized name LogService.warn("PolicyServer", "Using non-HTTP URI for policy server: " + this.serverName); schemeAndHostname = this.serverName; } @@ -66,7 +67,11 @@ export class PolicyServer { } const keyInfo = await response.json(); - if (typeof keyInfo !== "object" || typeof keyInfo.public_keys !== "object" || typeof keyInfo.public_keys.ed25519 !== "string") { + if ( + typeof keyInfo !== "object" || + typeof keyInfo.public_keys !== "object" || + typeof keyInfo.public_keys.ed25519 !== "string" + ) { LogService.warn("PolicyServer", `Failed to parse ed25519 key for ${this.name}: invalid response or no key`); this.ed25519Key = undefined; return undefined; @@ -75,4 +80,4 @@ export class PolicyServer { this.ed25519Key = keyInfo.public_keys.ed25519; return this.ed25519Key; } -} \ No newline at end of file +} diff --git a/src/ProtectedRoomsSet.ts b/src/ProtectedRoomsSet.ts index 09fd1dcc..a4ddde29 100644 --- a/src/ProtectedRoomsSet.ts +++ b/src/ProtectedRoomsSet.ts @@ -378,7 +378,11 @@ export class ProtectedRoomsSet { } catch (e) { // ignore error and fall back to unstable type try { - const content = await this.client.getRoomStateEventContent(roomId, "org.matrix.msc4284.policy", ""); + const content = await this.client.getRoomStateEventContent( + roomId, + "org.matrix.msc4284.policy", + "", + ); currentPolicyServerName = content["via"] as string | undefined; } catch (e) { // ignore - assume no policy server config diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index 8dc01fe9..5a98ec23 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -54,7 +54,7 @@ import { execIgnoreCommand, execListIgnoredCommand } from "./IgnoreCommand"; import { execLockCommand } from "./LockCommand"; import { execUnlockCommand } from "./UnlockCommand"; import { execQuarantineMediaCommand } from "./QuarantineMediaCommand"; -import {execSetPolicyServerCommand} from "./SetPolicyServerCommand"; +import { execSetPolicyServerCommand } from "./SetPolicyServerCommand"; export const COMMAND_PREFIX = "!mjolnir"; diff --git a/test/integration/policyServerTest.ts b/test/integration/policyServerTest.ts index 5ca04d0c..4ac4abd4 100644 --- a/test/integration/policyServerTest.ts +++ b/test/integration/policyServerTest.ts @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Mjolnir} from "../../src/Mjolnir"; +import { Mjolnir } from "../../src/Mjolnir"; import * as http from "node:http"; -import {AddressInfo} from "node:net"; +import { AddressInfo } from "node:net"; import { strict as assert } from "assert"; describe("Test: Policy Servers", function () { @@ -34,15 +34,17 @@ describe("Test: Policy Servers", function () { mjolnir = this.config.RUNTIME.client!; policyServer = http.createServer((req, res) => { res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ - "public_keys": { - "ed25519": ed25519Key, - }, - })); + res.end( + JSON.stringify({ + public_keys: { + ed25519: ed25519Key, + }, + }), + ); }); // grab any port by not specifying one to listen on policyServer.listen(() => { - policyServerUrl = `http://localhost:${(policyServer.address()! as AddressInfo).port}` + policyServerUrl = `http://localhost:${(policyServer.address()! as AddressInfo).port}`; }); // Create a room we can inspect @@ -74,7 +76,7 @@ describe("Test: Policy Servers", function () { body: `!mjolnir policy_server ${policyServerUrl}`, }); await delay(1500); - const policyServerContent = await mjolnir.client.getRoomStateEventContent(lookInRoomId, "m.room.policy", ""); + let policyServerContent = await mjolnir.client.getRoomStateEventContent(lookInRoomId, "m.room.policy", ""); assert.equal(policyServerContent.url, policyServerUrl); assert.equal((policyServerContent.public_keys! as Record).ed25519, ed25519Key); @@ -84,8 +86,8 @@ describe("Test: Policy Servers", function () { body: `!mjolnir policy_server unset`, }); await delay(1500); - const policyServerContent = await mjolnir.client.getRoomStateEventContent(lookInRoomId, "m.room.policy", ""); + policyServerContent = await mjolnir.client.getRoomStateEventContent(lookInRoomId, "m.room.policy", ""); assert.equal(policyServerContent.url, undefined); assert.equal(policyServerContent.public_keys, undefined); }); -}); \ No newline at end of file +});