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.
This commit is contained in:
Gnuxie
2026-03-30 14:25:39 +01:00
committed by GitHub
parent 5870c02933
commit 7bb9d51980
13 changed files with 88 additions and 20 deletions
+6
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
"@the-draupnir-project/matrix-protection-suite": minor
---
Fix method names around room v12 privileged creators to no longer have typos
+16
View File
@@ -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);
}
@@ -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({
@@ -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<PowerLevelsEvent>(
"m.room.power_levels",
""
);
const createEvent =
this.stateIssuer.currentRevision.getStateEvent<RoomCreateEvent>(
"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;
}
}
@@ -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 (
<fragment>
<details>
@@ -26,6 +26,7 @@ void (async () => {
config.roomStateBackingStore.enabled ?? false,
}),
allowSafeMode: true,
deleteManagementRoomAliasOnStart: false,
});
await draupnirClient()?.start();
await toggle.encryptionInitialized();
@@ -143,10 +143,12 @@ export async function makeBotModeToggle(
stores,
eraseAccountData,
allowSafeMode,
deleteManagementRoomAliasOnStart: deleteAlias,
}: {
stores: TopLevelStores;
eraseAccountData?: boolean;
allowSafeMode?: boolean;
deleteManagementRoomAliasOnStart?: boolean;
} = { stores: { dispose() {} } }
): Promise<DraupnirBotModeToggle> {
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,
@@ -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
@@ -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"] = {
@@ -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: [],
@@ -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];
}
@@ -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[] = [];