From 7bb9d51980ffbbcaabb12d3742e4151f7f8b3353 Mon Sep 17 00:00:00 2001 From: Gnuxie <50846879+Gnuxie@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:25:39 +0100 Subject: [PATCH] Crash if Draupnir doesn't have the power level for `state_default` in the management room. (#1063) Looks like this: ``` TypeError: Could not create Draupnir caused by: @mjolnir:localhost:9999 doesn't have the power level required to send state events in the management room. Please make the Draupnir user an administrator at ResultError.toExpectError (/home/user/experiments/Draupnir/node_modules/@gnuxie/typescript-result/src/Result.ts:200:12) at ExpectError (/home/user/experiments/Draupnir/node_modules/@gnuxie/typescript-result/src/Result.ts:128:24) at Object.expect (/home/user/experiments/Draupnir/node_modules/@gnuxie/typescript-result/src/Result.ts:137:10) at makeBotModeToggle (/home/user/experiments/Draupnir/apps/draupnir/test/integration/mjolnirSetupUtils.ts:194:5) at processTicksAndRejections (node:internal/process/task_queues:104:5) at async /home/user/experiments/Draupnir/apps/draupnir/test/integration/manualLaunchScript.ts:23:18 ``` Fixes https://github.com/the-draupnir-project/Draupnir/issues/1025 Closes https://github.com/the-draupnir-project/planning/issues/108 * Fix test:manual deleting the prior management room The old behaviour of leaving any stale management room active was intentional, to be able to inspect failed tests and restart the bot quickly to check changes. * Check Draupnir can send state to the management room at startup. Fixes https://github.com/the-draupnir-project/Draupnir/issues/1025 Part of https://github.com/the-draupnir-project/planning/issues/108 * Fix small bug in PowerLevelsMirror. It was checking the wrong entries for regular events. Discovered as part of https://github.com/the-draupnir-project/planning/issues/108 * Fix typos in privileged creator APIs Fixed during https://github.com/the-draupnir-project/planning/issues/108 * Notify management room of lack of state_default permission. --- .changeset/fast-years-battle.md | 6 ++++ .changeset/gentle-rockets-punch.md | 5 +++ apps/draupnir/src/Draupnir.ts | 16 +++++++++ .../appservice/AppServiceDraupnirManager.ts | 2 +- .../managementroom/ManagementRoomDetail.ts | 34 +++++++++++++++++++ .../WatchReplacementPolicyRooms.tsx | 2 +- .../test/integration/manualLaunchScript.ts | 1 + .../test/integration/mjolnirSetupUtils.ts | 12 ++++--- .../RoomStateManagerFactory.ts | 2 +- .../src/PolicyList/PolicyListManager.ts | 2 +- .../src/Client/PowerLevelsMirror.ts | 10 +++--- .../src/MatrixTypes/CreateRoom.ts | 14 ++++---- .../src/Protection/ProtectedRoomsSet.ts | 2 +- 13 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 .changeset/fast-years-battle.md create mode 100644 .changeset/gentle-rockets-punch.md diff --git a/.changeset/fast-years-battle.md b/.changeset/fast-years-battle.md new file mode 100644 index 00000000..1ca6aaa9 --- /dev/null +++ b/.changeset/fast-years-battle.md @@ -0,0 +1,6 @@ +--- +"draupnir": minor +--- + +Draupnir will crash at startup if it does not have the ability to send state +events to the mangement room. diff --git a/.changeset/gentle-rockets-punch.md b/.changeset/gentle-rockets-punch.md new file mode 100644 index 00000000..225119c8 --- /dev/null +++ b/.changeset/gentle-rockets-punch.md @@ -0,0 +1,5 @@ +--- +"@the-draupnir-project/matrix-protection-suite": minor +--- + +Fix method names around room v12 privileged creators to no longer have typos diff --git a/apps/draupnir/src/Draupnir.ts b/apps/draupnir/src/Draupnir.ts index a9fc30cc..7bfbd529 100644 --- a/apps/draupnir/src/Draupnir.ts +++ b/apps/draupnir/src/Draupnir.ts @@ -81,6 +81,7 @@ import { sendMatrixEventsFromDeadDocument, } from "@the-draupnir-project/mps-interface-adaptor"; import { TimelineRedactionQueue } from "./queues/TimelineRedactionQueue"; +import { ResultError } from "@gnuxie/typescript-result"; const log = new Logger("Draupnir"); @@ -325,6 +326,21 @@ export class Draupnir implements Client, MatrixAdaptorContext { if (isError(managementRoomProtectResult)) { return managementRoomProtectResult; } + // Check that Draupnir has the ability to send state events to the management + // room. Which is important because Draupnir users state events to persist + // protection settings, including the draupnir news feed position. + if (!managementRoomDetail.isDraupnirUserPowered(clientUserID)) { + const errorText = `${clientUserID} doesn't have the power level required to send state events in the management room. Please make the Draupnir user an administrator.`; + await Task( + clientPlatform + .toRoomMessageSender() + .sendMessage(managementRoomDetail.managementRoomID, { + body: errorText, + msgtype: "m.notice", + }) + ); + return ResultError.Result(errorText); + } return Ok(draupnir); } diff --git a/apps/draupnir/src/appservice/AppServiceDraupnirManager.ts b/apps/draupnir/src/appservice/AppServiceDraupnirManager.ts index dd86434d..9883a966 100644 --- a/apps/draupnir/src/appservice/AppServiceDraupnirManager.ts +++ b/apps/draupnir/src/appservice/AppServiceDraupnirManager.ts @@ -458,7 +458,7 @@ export async function makeManagementRoom( ); } const isRoomVersionWithPrivilidgedCreators = - RoomVersionMirror.isVersionWithPrivilidgedCreators( + RoomVersionMirror.isVersionWithPrivilegedCreators( capabilities.ok.capabilities["m.room_versions"].default ); return await roomCreator.createRoom({ diff --git a/apps/draupnir/src/managementroom/ManagementRoomDetail.ts b/apps/draupnir/src/managementroom/ManagementRoomDetail.ts index c56c73f0..e78acd6c 100644 --- a/apps/draupnir/src/managementroom/ManagementRoomDetail.ts +++ b/apps/draupnir/src/managementroom/ManagementRoomDetail.ts @@ -10,13 +10,19 @@ import { import { JoinRulesEvent, Membership, + PowerLevelPermission, + PowerLevelsEvent, + PowerLevelsMirror, + RoomCreateEvent, RoomMembershipRevisionIssuer, RoomStateRevisionIssuer, + RoomVersionMirror, } from "matrix-protection-suite"; export interface ManagementRoomDetail { isRoomPublic(): boolean; isModerator(userID: StringUserID): boolean; + isDraupnirUserPowered(draupnirUserID: StringUserID): boolean; managementRoom: MatrixRoomID; managementRoomID: StringRoomID; } @@ -52,4 +58,32 @@ export class StandardManagementRoomDetail implements ManagementRoomDetail { public get managementRoomID(): StringRoomID { return this.managementRoom.toRoomIDOrAlias(); } + + public isDraupnirUserPowered(draupnirUserID: StringUserID): boolean { + const powerLevelEvent = + this.stateIssuer.currentRevision.getStateEvent( + "m.room.power_levels", + "" + ); + + const createEvent = + this.stateIssuer.currentRevision.getStateEvent( + "m.room.create", + "" + ); + if (powerLevelEvent === undefined || createEvent === undefined) { + throw new TypeError("Unable to fetch management room state"); + } + if ( + PowerLevelsMirror.isUserAbleToUse( + draupnirUserID, + PowerLevelPermission.StateDefault, + powerLevelEvent.content + ) || + RoomVersionMirror.isUserAPrivilegedCreator(draupnirUserID, createEvent) + ) { + return true; + } + return false; + } } diff --git a/apps/draupnir/src/protections/ProtectedRooms/WatchReplacementPolicyRooms.tsx b/apps/draupnir/src/protections/ProtectedRooms/WatchReplacementPolicyRooms.tsx index 00fb4c47..04653486 100644 --- a/apps/draupnir/src/protections/ProtectedRooms/WatchReplacementPolicyRooms.tsx +++ b/apps/draupnir/src/protections/ProtectedRooms/WatchReplacementPolicyRooms.tsx @@ -89,7 +89,7 @@ function renderPrivilegedUsers(revision: RoomStateRevision): DocumentNode { if (createEvent === undefined) { throw new TypeError("Mate can't find create event in the room"); } - const privilegedCreators = RoomVersionMirror.priviligedCreators(createEvent); + const privilegedCreators = RoomVersionMirror.privilegedCreators(createEvent); return (
diff --git a/apps/draupnir/test/integration/manualLaunchScript.ts b/apps/draupnir/test/integration/manualLaunchScript.ts index 14f17130..05cc0ee2 100644 --- a/apps/draupnir/test/integration/manualLaunchScript.ts +++ b/apps/draupnir/test/integration/manualLaunchScript.ts @@ -26,6 +26,7 @@ void (async () => { config.roomStateBackingStore.enabled ?? false, }), allowSafeMode: true, + deleteManagementRoomAliasOnStart: false, }); await draupnirClient()?.start(); await toggle.encryptionInitialized(); diff --git a/apps/draupnir/test/integration/mjolnirSetupUtils.ts b/apps/draupnir/test/integration/mjolnirSetupUtils.ts index aa98198d..429f6d42 100644 --- a/apps/draupnir/test/integration/mjolnirSetupUtils.ts +++ b/apps/draupnir/test/integration/mjolnirSetupUtils.ts @@ -143,10 +143,12 @@ export async function makeBotModeToggle( stores, eraseAccountData, allowSafeMode, + deleteManagementRoomAliasOnStart: deleteAlias, }: { stores: TopLevelStores; eraseAccountData?: boolean; allowSafeMode?: boolean; + deleteManagementRoomAliasOnStart?: boolean; } = { stores: { dispose() {} } } ): Promise { await configureMjolnir(config); @@ -173,10 +175,12 @@ export async function makeBotModeToggle( config.homeserverUrl, await client.getUserId() ); - - await client - .deleteRoomAlias(config.managementRoom) - .catch((_: unknown) => undefined); + // defaults to true + if (deleteAlias === undefined || deleteAlias) { + await client + .deleteRoomAlias(config.managementRoom) + .catch((_: unknown) => undefined); + } await ensureAliasedRoomExists(client, config.managementRoom); const toggle = await DraupnirBotModeToggle.create( client, diff --git a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/RoomStateManagerFactory.ts b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/RoomStateManagerFactory.ts index 8742eaf8..ac20f8df 100644 --- a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/RoomStateManagerFactory.ts +++ b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/RoomStateManagerFactory.ts @@ -259,7 +259,7 @@ export class RoomStateManagerFactory { if (createEvent === undefined) { return false; } - return RoomVersionMirror.isUserAPrivilidgedCreator(editor, createEvent); + return RoomVersionMirror.isUserAPrivilegedCreator(editor, createEvent); }) .map((issuer) => issuer.room.toRoomIDOrAlias()); const editableRoomIDs = this.policyRoomIssuers diff --git a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/PolicyList/PolicyListManager.ts b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/PolicyList/PolicyListManager.ts index 4fa2fc80..1c418284 100644 --- a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/PolicyList/PolicyListManager.ts +++ b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/PolicyList/PolicyListManager.ts @@ -111,7 +111,7 @@ export class BotSDKPolicyRoomManager implements PolicyRoomManager { ); } const isRoomVersionWithPrivilidgedCreators = - RoomVersionMirror.isVersionWithPrivilidgedCreators( + RoomVersionMirror.isVersionWithPrivilegedCreators( clientCapabilities.ok.capabilities["m.room_versions"].default ); const powerLevels: RoomCreateOptions["power_level_content_override"] = { diff --git a/packages/matrix-protection-suite/src/Client/PowerLevelsMirror.ts b/packages/matrix-protection-suite/src/Client/PowerLevelsMirror.ts index 40fdf2c8..587e80a2 100644 --- a/packages/matrix-protection-suite/src/Client/PowerLevelsMirror.ts +++ b/packages/matrix-protection-suite/src/Client/PowerLevelsMirror.ts @@ -11,6 +11,8 @@ export enum PowerLevelPermission { Invite = "invite", Kick = "kick", Redact = "redact", + EventsDefault = "events_default", + StateDefault = "state_default", } export type MissingPermissionsChange = { @@ -56,9 +58,9 @@ export const PowerLevelsMirror = Object.freeze({ content?: PowerLevelsEventContent ): boolean { const userLevel = this.getUserPowerLevel(who, content); - const defaultPowerLevel = + const defaultPermissionLevel = permission === PowerLevelPermission.Invite ? 0 : 50; - const permissionLevel = content?.[permission] ?? defaultPowerLevel; + const permissionLevel = content?.[permission] ?? defaultPermissionLevel; return userLevel >= permissionLevel; }, isUserAbleToSendEvent( @@ -68,7 +70,7 @@ export const PowerLevelsMirror = Object.freeze({ ): boolean { return ( this.getUserPowerLevel(who, content) >= - this.getStatePowerLevel(eventType, content) + this.getEventPowerLevel(eventType, content) ); }, missingPermissions( @@ -166,7 +168,7 @@ export const PowerLevelsMirror = Object.freeze({ isNewlyAddedRoom?: boolean; } ): MissingPermissionsChange { - if (RoomVersionMirror.isUserAPrivilidgedCreator(userID, createEvent)) { + if (RoomVersionMirror.isUserAPrivilegedCreator(userID, createEvent)) { return { missingStatePermissions: [], missingPermissions: [], diff --git a/packages/matrix-protection-suite/src/MatrixTypes/CreateRoom.ts b/packages/matrix-protection-suite/src/MatrixTypes/CreateRoom.ts index 678953fe..63776f69 100644 --- a/packages/matrix-protection-suite/src/MatrixTypes/CreateRoom.ts +++ b/packages/matrix-protection-suite/src/MatrixTypes/CreateRoom.ts @@ -172,11 +172,11 @@ export const RoomCreateEvent = Type.Intersect([ }), ]); -// FIXME: SHouldn't the prividliged creators function return a result error? +// FIXME: SHouldn't the privileged creators function return a result error? // i think so, but it just depends how the permission calculation system // uses it and whether it supports feeding errors back. export const RoomVersionMirror = Object.freeze({ - isVersionWithPrivilidgedCreators(versionSpecifier: string): boolean { + isVersionWithPrivilegedCreators(versionSpecifier: string): boolean { const integerResult = (() => { try { return Ok(parseInt(versionSpecifier, 10)); @@ -191,11 +191,11 @@ export const RoomVersionMirror = Object.freeze({ return false; // unknown room version. } if (integerResult.ok >= 12) { - return true; // versions below 12 and abovehave privilidged creators. + return true; // versions 12 and above have privileged creators. } return false; }, - isUserAPrivilidgedCreator( + isUserAPrivilegedCreator( userID: StringUserID, creationEvent: RoomCreateEvent ): boolean { @@ -203,7 +203,7 @@ export const RoomVersionMirror = Object.freeze({ return false; } if ( - !this.isVersionWithPrivilidgedCreators(creationEvent.content.room_version) + !this.isVersionWithPrivilegedCreators(creationEvent.content.room_version) ) { return false; } @@ -215,10 +215,10 @@ export const RoomVersionMirror = Object.freeze({ } return false; }, - priviligedCreators(creationEvent: RoomCreateEvent): StringUserID[] { + privilegedCreators(creationEvent: RoomCreateEvent): StringUserID[] { if ( creationEvent.content.room_version === undefined || - !this.isVersionWithPrivilidgedCreators(creationEvent.content.room_version) + !this.isVersionWithPrivilegedCreators(creationEvent.content.room_version) ) { return [creationEvent.sender]; } diff --git a/packages/matrix-protection-suite/src/Protection/ProtectedRoomsSet.ts b/packages/matrix-protection-suite/src/Protection/ProtectedRoomsSet.ts index 606ba267..5c00a13d 100644 --- a/packages/matrix-protection-suite/src/Protection/ProtectedRoomsSet.ts +++ b/packages/matrix-protection-suite/src/Protection/ProtectedRoomsSet.ts @@ -228,7 +228,7 @@ export class StandardProtectedRoomsSet implements ProtectedRoomsSet { previousPowerLevels: PowerLevelsEventContent | undefined ): void { // prividliged creators never change and always have permission. - if (RoomVersionMirror.isUserAPrivilidgedCreator(this.userID, createEvent)) { + if (RoomVersionMirror.isUserAPrivilegedCreator(this.userID, createEvent)) { return; } const missingPermissionsInfo: ProtectionPermissionsChange[] = [];