mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-10-22 17:11:17 +00:00
Zero Touch Provisioning Support
This commit is contained in:
16 files changed
+450
-78
No files matched your search
@@ -0,0 +1,21 @@
|
||||
---
|
||||
"draupnir": minor
|
||||
---
|
||||
|
||||
Add managed management/admin room support to bot mode / appservice mode.
|
||||
|
||||
\- Add managed management / admin room support
|
||||
|
||||
- Add new config options for zero touch provisioning of managed rooms:
|
||||
|
||||
\- managedManagementRoom and initialManager for bot mode
|
||||
|
||||
\- managedAdminRoom and initialManager for appservice mode
|
||||
|
||||
\- If managed modes are enabled while unmanaged rooms are defined the bot will
|
||||
crash.
|
||||
|
||||
\- Fix TrustedReporters being broken by managed management rooms due to explicit
|
||||
reliance on a management room in the config.
|
||||
|
||||
\- Add integration testing for Zero Touch Provisioning workflows.
|
||||
@@ -48,7 +48,10 @@ import { ResultError } from "@gnuxie/typescript-result";
|
||||
import { SafeModeCause, SafeModeReason } from "./safemode/SafeModeCause";
|
||||
import { SafeModeBootOption } from "./safemode/BootOption";
|
||||
import { TopLevelStores } from "./backingstore/DraupnirStores";
|
||||
import { MANAGEMENT_ROOM_ACCOUNT_DATA_EVENT_TYPE } from "./managedRoomAccountData";
|
||||
import { makeManagementRoom } from "./appservice/AppServiceDraupnirManager";
|
||||
import { patchMatrixClient } from "./utils";
|
||||
import { resolveManagedRoom } from "./managedRoomBootstrap";
|
||||
|
||||
const log = new Logger("DraupnirBotMode");
|
||||
|
||||
@@ -116,17 +119,6 @@ export class DraupnirBotModeToggle implements BotModeTogle {
|
||||
if (!isStringUserID(clientUserID)) {
|
||||
throw new TypeError(`${clientUserID} is not a valid mxid`);
|
||||
}
|
||||
if (
|
||||
!isStringRoomAlias(config.managementRoom) &&
|
||||
!isStringRoomID(config.managementRoom)
|
||||
) {
|
||||
throw new TypeError(
|
||||
`${config.managementRoom} is not a valid room id or alias`
|
||||
);
|
||||
}
|
||||
const configManagementRoomReference = MatrixRoomReference.fromRoomIDOrAlias(
|
||||
config.managementRoom
|
||||
);
|
||||
const clientsInRoomMap = new StandardClientsInRoomMap();
|
||||
const clientCapabilityFactory = new ClientCapabilityFactory(
|
||||
clientsInRoomMap,
|
||||
@@ -142,14 +134,37 @@ export class DraupnirBotModeToggle implements BotModeTogle {
|
||||
clientUserID,
|
||||
client
|
||||
);
|
||||
const managementRoom = (
|
||||
await clientPlatform
|
||||
.toRoomResolver()
|
||||
.resolveRoom(configManagementRoomReference)
|
||||
).expect("Unable to resolve Draupnir's management room");
|
||||
(await clientPlatform.toRoomJoiner().joinRoom(managementRoom)).expect(
|
||||
"Unable to join Draupnir's management room"
|
||||
);
|
||||
const managementRoom = await resolveManagedRoom({
|
||||
managedRoomEnabled: config.managedManagementRoom,
|
||||
configuredRoom: config.managementRoom,
|
||||
initialManager: config.initialManager,
|
||||
clientUserID,
|
||||
client,
|
||||
clientPlatform,
|
||||
accountDataEventType: MANAGEMENT_ROOM_ACCOUNT_DATA_EVENT_TYPE,
|
||||
roomKindDescription: "management room",
|
||||
parseConfiguredRoom(configuredRoom: string): MatrixRoomReference {
|
||||
if (
|
||||
!isStringRoomAlias(configuredRoom) &&
|
||||
!isStringRoomID(configuredRoom)
|
||||
) {
|
||||
throw new TypeError(
|
||||
`${configuredRoom} is not a valid room id or alias`
|
||||
);
|
||||
}
|
||||
return MatrixRoomReference.fromRoomIDOrAlias(configuredRoom);
|
||||
},
|
||||
async createManagedRoom(initialManager, resolvedClientUserID) {
|
||||
return (
|
||||
await makeManagementRoom(
|
||||
clientPlatform.toRoomCreator(),
|
||||
clientPlatform.toClientCapabilitiesNegotiation(),
|
||||
initialManager,
|
||||
resolvedClientUserID
|
||||
)
|
||||
).expect("Unable to create managed management room");
|
||||
},
|
||||
});
|
||||
const clientProvider = async (userID: StringUserID) => {
|
||||
if (userID !== clientUserID) {
|
||||
throw new TypeError(`Bot mode shouldn't be requesting any other mxids`);
|
||||
|
||||
@@ -55,6 +55,9 @@ import { SqliteRoomStateBackingStore } from "../backingstore/better-sqlite3/Sqli
|
||||
import { TopLevelStores } from "../backingstore/DraupnirStores";
|
||||
import { patchMatrixClient } from "../utils";
|
||||
import { Result } from "@gnuxie/typescript-result";
|
||||
import { ADMIN_ROOM_ACCOUNT_DATA_EVENT_TYPE } from "../managedRoomAccountData";
|
||||
import { makeManagementRoom } from "./AppServiceDraupnirManager";
|
||||
import { resolveManagedRoom } from "../managedRoomBootstrap";
|
||||
|
||||
const log = new Logger("AppService");
|
||||
/**
|
||||
@@ -198,27 +201,51 @@ export class MjolnirAppService {
|
||||
joinedRoomsSafe(bridge.getBot().getClient())
|
||||
)
|
||||
).expect("Unable to initialize client rooms for the appservice bot user");
|
||||
const botRoomJoiner = clientCapabilityFactory
|
||||
.makeClientPlatform(botUserID, bridge.getBot().getClient())
|
||||
.toRoomJoiner();
|
||||
const adminRoom = (() => {
|
||||
if (isStringRoomID(config.adminRoom)) {
|
||||
return MatrixRoomReference.fromRoomID(config.adminRoom);
|
||||
} else if (isStringRoomAlias(config.adminRoom)) {
|
||||
return MatrixRoomReference.fromRoomIDOrAlias(config.adminRoom);
|
||||
} else {
|
||||
const parseResult = MatrixRoomReference.fromPermalink(config.adminRoom);
|
||||
const botClient = bridge.getBot().getClient();
|
||||
const botClientPlatform = clientCapabilityFactory.makeClientPlatform(
|
||||
botUserID,
|
||||
botClient
|
||||
);
|
||||
const botRoomJoiner = botClientPlatform.toRoomJoiner();
|
||||
const accessControlRoom = await resolveManagedRoom({
|
||||
managedRoomEnabled: config.managedAdminRoom,
|
||||
configuredRoom: config.adminRoom,
|
||||
initialManager: config.initialManager,
|
||||
clientUserID: botUserID,
|
||||
client: botClient,
|
||||
clientPlatform: botClientPlatform,
|
||||
accountDataEventType: ADMIN_ROOM_ACCOUNT_DATA_EVENT_TYPE,
|
||||
roomKindDescription: "admin room",
|
||||
parseConfiguredRoom(configuredRoom: string): MatrixRoomReference {
|
||||
if (isStringRoomID(configuredRoom)) {
|
||||
return MatrixRoomReference.fromRoomID(configuredRoom);
|
||||
}
|
||||
if (isStringRoomAlias(configuredRoom)) {
|
||||
return MatrixRoomReference.fromRoomIDOrAlias(configuredRoom);
|
||||
}
|
||||
const parseResult = MatrixRoomReference.fromPermalink(configuredRoom);
|
||||
if (isError(parseResult)) {
|
||||
throw new TypeError(
|
||||
`${config.adminRoom} needs to be a room id, alias or permalink`
|
||||
`${configuredRoom} needs to be a room id, alias or permalink`
|
||||
);
|
||||
}
|
||||
return parseResult.ok;
|
||||
}
|
||||
})();
|
||||
const accessControlRoom = (
|
||||
await botRoomJoiner.resolveRoom(adminRoom)
|
||||
).expect("Unable to resolve the admin room");
|
||||
},
|
||||
async createManagedRoom(initialManager, resolvedClientUserID) {
|
||||
return (
|
||||
await makeManagementRoom(
|
||||
botClientPlatform.toRoomCreator(),
|
||||
botClientPlatform.toClientCapabilitiesNegotiation(),
|
||||
initialManager,
|
||||
resolvedClientUserID,
|
||||
{
|
||||
invitees: [initialManager],
|
||||
roomName: `${initialManager}'s Draupnir Appservice Admin room`,
|
||||
}
|
||||
)
|
||||
).expect("Unable to create managed admin room");
|
||||
},
|
||||
});
|
||||
const appserviceBotPolicyRoomManager =
|
||||
await roomStateManagerFactory.getPolicyRoomManager(botUserID);
|
||||
const accessControl = (
|
||||
|
||||
@@ -52,7 +52,7 @@ import {
|
||||
MatrixRoomID,
|
||||
} from "@the-draupnir-project/matrix-basic-types";
|
||||
import { TopLevelStores } from "../backingstore/DraupnirStores";
|
||||
import { Result } from "@gnuxie/typescript-result";
|
||||
import { Result, ResultError } from "@gnuxie/typescript-result";
|
||||
|
||||
const log = new Logger("AppServiceDraupnirManager");
|
||||
|
||||
@@ -468,7 +468,11 @@ export async function makeManagementRoom(
|
||||
roomCreator: RoomCreator,
|
||||
clientCapabilitiesNegotiation: ClientCapabilitiesNegotiation,
|
||||
requestingUserID: StringUserID,
|
||||
draupnirUserID: StringUserID
|
||||
draupnirUserID: StringUserID,
|
||||
options?: {
|
||||
invitees?: StringUserID[];
|
||||
roomName?: string;
|
||||
}
|
||||
): Promise<Result<MatrixRoomID>> {
|
||||
const capabilities =
|
||||
await clientCapabilitiesNegotiation.getClientCapabilities();
|
||||
@@ -477,15 +481,20 @@ export async function makeManagementRoom(
|
||||
"Failed to fetch room versions from client capabilities"
|
||||
);
|
||||
}
|
||||
const isRoomVersionWithPriviligedCreators =
|
||||
RoomVersionMirror.isVersionWithPrivilegedCreators(
|
||||
capabilities.ok.capabilities["m.room_versions"].default
|
||||
const defaultRoomVersion =
|
||||
capabilities.ok.capabilities["m.room_versions"].default;
|
||||
if (typeof defaultRoomVersion !== "string") {
|
||||
return ResultError.Result(
|
||||
"Client capabilities did not contain a valid default room version"
|
||||
);
|
||||
}
|
||||
const isRoomVersionWithPrivilegedCreators =
|
||||
RoomVersionMirror.isVersionWithPrivilegedCreators(defaultRoomVersion);
|
||||
return await roomCreator.createRoom({
|
||||
preset: "private_chat",
|
||||
invite: [requestingUserID],
|
||||
name: `${requestingUserID}'s Draupnir`,
|
||||
power_level_content_override: isRoomVersionWithPriviligedCreators
|
||||
invite: options?.invitees ?? [requestingUserID],
|
||||
name: options?.roomName ?? `${requestingUserID}'s Draupnir`,
|
||||
power_level_content_override: isRoomVersionWithPrivilegedCreators
|
||||
? {
|
||||
users: {
|
||||
[requestingUserID]: 150,
|
||||
|
||||
@@ -13,6 +13,12 @@ db:
|
||||
# See docs/access_control.md
|
||||
adminRoom: "#draupnir-admin:localhost:9999"
|
||||
|
||||
# Enable automatic admin-room recovery/creation on first boot.
|
||||
managedAdminRoom: false
|
||||
|
||||
# The initial manager to invite if the admin room has to be created.
|
||||
# initialManager: "@admin:example.org"
|
||||
|
||||
# The directory the bot should store various bits of information in
|
||||
dataPath: "/data/storage"
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ db:
|
||||
|
||||
adminRoom: "#draupnir-admin:localhost:9999"
|
||||
|
||||
managedAdminRoom: false
|
||||
|
||||
# initialManager: "@admin:example.org"
|
||||
|
||||
# The directory the bot should store various bits of information in
|
||||
dataPath: "./test/harness/mjolnir-data/"
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import * as fs from "fs";
|
||||
import { load } from "js-yaml";
|
||||
import { Value } from "@sinclair/typebox/value";
|
||||
import { EDStatic } from "matrix-protection-suite";
|
||||
import { StringUserIDSchema } from "matrix-protection-suite";
|
||||
|
||||
export function read(configPath: string): AppserviceConfig {
|
||||
const content = fs.readFileSync(configPath, "utf8");
|
||||
@@ -81,10 +82,16 @@ export const AppserviceConfig = Type.Object({
|
||||
},
|
||||
{ description: "Details for the database backend" }
|
||||
),
|
||||
adminRoom: Type.String({
|
||||
description:
|
||||
"The admin room for the appservice bot. Not called managementRoom like draupnir on purpose, so they're not mixed in code somehow.",
|
||||
managedAdminRoom: Type.Boolean({
|
||||
default: false,
|
||||
}),
|
||||
initialManager: Type.Optional(StringUserIDSchema),
|
||||
adminRoom: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"The admin room for the appservice bot. Not called managementRoom like draupnir on purpose, so they're not mixed in code somehow.",
|
||||
})
|
||||
),
|
||||
roomStateBackingStore: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
// Copyright 2022 Gnuxie <Gnuxie@protonmail.com>
|
||||
// Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
@@ -18,6 +19,7 @@ import Config from "config";
|
||||
import path from "path";
|
||||
import { SafeModeBootOption } from "./safemode/BootOption";
|
||||
import { Logger, setGlobalLoggerProvider } from "matrix-protection-suite";
|
||||
import { StringUserID } from "@the-draupnir-project/matrix-basic-types";
|
||||
|
||||
LogService.setLogger(new RichConsoleLogger());
|
||||
setGlobalLoggerProvider(new RichConsoleLogger());
|
||||
@@ -84,7 +86,9 @@ export interface IConfig {
|
||||
/** Draupnir will accept invites from members of this space if `autojoinOnlyIfManager` is false. */
|
||||
acceptInvitesFromSpace: string | undefined;
|
||||
recordIgnoredInvites: boolean;
|
||||
managementRoom: string;
|
||||
managedManagementRoom: boolean;
|
||||
initialManager: StringUserID | undefined;
|
||||
managementRoom: string | undefined;
|
||||
logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
|
||||
logMutedModules: string[];
|
||||
verifyPermissionsOnStartup: boolean;
|
||||
@@ -205,6 +209,8 @@ const defaultConfig: IConfig = {
|
||||
acceptInvitesFromSpace: "!noop:example.org",
|
||||
autojoinOnlyIfManager: true,
|
||||
recordIgnoredInvites: false,
|
||||
managedManagementRoom: false,
|
||||
initialManager: undefined,
|
||||
managementRoom: "!noop:example.org",
|
||||
logLevel: "INFO",
|
||||
logMutedModules: ["MatrixHttpClient", "MatrixClientLite"],
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { EDStatic, StringRoomIDSchema } from "matrix-protection-suite";
|
||||
import {
|
||||
BotSDKMatrixAccountData,
|
||||
MatrixSendClient,
|
||||
} from "matrix-protection-suite-for-matrix-bot-sdk";
|
||||
|
||||
export const MANAGEMENT_ROOM_ACCOUNT_DATA_EVENT_TYPE =
|
||||
"space.draupnir.management.room";
|
||||
export const ADMIN_ROOM_ACCOUNT_DATA_EVENT_TYPE = "space.draupnir.admin.room";
|
||||
|
||||
export const ManagedRoomAccountDataRecordSchema = Type.Object({
|
||||
room_id: StringRoomIDSchema,
|
||||
});
|
||||
|
||||
export type ManagedRoomAccountDataRecord = EDStatic<
|
||||
typeof ManagedRoomAccountDataRecordSchema
|
||||
>;
|
||||
|
||||
export function makeManagedRoomAccountDataStore(
|
||||
client: MatrixSendClient,
|
||||
eventType: string
|
||||
): BotSDKMatrixAccountData<ManagedRoomAccountDataRecord> {
|
||||
return new BotSDKMatrixAccountData(
|
||||
eventType,
|
||||
ManagedRoomAccountDataRecordSchema,
|
||||
client
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
import { ClientPlatform } from "matrix-protection-suite";
|
||||
import { MatrixSendClient } from "matrix-protection-suite-for-matrix-bot-sdk";
|
||||
import {
|
||||
isStringUserID,
|
||||
MatrixRoomID,
|
||||
MatrixRoomReference,
|
||||
StringUserID,
|
||||
} from "@the-draupnir-project/matrix-basic-types";
|
||||
import { makeManagedRoomAccountDataStore } from "./managedRoomAccountData";
|
||||
|
||||
export type ManagedRoomBootstrapOptions = {
|
||||
managedRoomEnabled: boolean;
|
||||
configuredRoom: string | undefined;
|
||||
initialManager: StringUserID | undefined;
|
||||
clientUserID: StringUserID;
|
||||
client: MatrixSendClient;
|
||||
clientPlatform: ClientPlatform;
|
||||
accountDataEventType: string;
|
||||
roomKindDescription: string;
|
||||
parseConfiguredRoom(configuredRoom: string): MatrixRoomReference;
|
||||
createManagedRoom(
|
||||
initialManager: StringUserID,
|
||||
clientUserID: StringUserID
|
||||
): Promise<MatrixRoomID>;
|
||||
};
|
||||
|
||||
export async function resolveManagedRoom(
|
||||
options: ManagedRoomBootstrapOptions
|
||||
): Promise<MatrixRoomID> {
|
||||
if (options.managedRoomEnabled && options.configuredRoom !== undefined) {
|
||||
throw new TypeError(
|
||||
`managed ${options.roomKindDescription} mode cannot be used with an explicit ${options.roomKindDescription}; remove the configured room or disable managed mode`
|
||||
);
|
||||
}
|
||||
|
||||
const roomAccountData = makeManagedRoomAccountDataStore(
|
||||
options.client,
|
||||
options.accountDataEventType
|
||||
);
|
||||
const roomResolver = options.clientPlatform.toRoomResolver();
|
||||
const roomJoiner = options.clientPlatform.toRoomJoiner();
|
||||
|
||||
if (options.managedRoomEnabled) {
|
||||
const storedRoom = (await roomAccountData.requestAccountData()).expect(
|
||||
`Unable to read managed ${options.roomKindDescription} account data`
|
||||
);
|
||||
if (storedRoom !== undefined) {
|
||||
const roomReference = MatrixRoomReference.fromRoomID(storedRoom.room_id);
|
||||
const room = (await roomResolver.resolveRoom(roomReference)).expect(
|
||||
`Unable to resolve managed Draupnir ${options.roomKindDescription}`
|
||||
);
|
||||
(await roomJoiner.joinRoom(room)).expect(
|
||||
`Unable to join managed Draupnir ${options.roomKindDescription}`
|
||||
);
|
||||
return room;
|
||||
}
|
||||
|
||||
if (options.initialManager === undefined) {
|
||||
throw new TypeError(
|
||||
`managed ${options.roomKindDescription} mode is enabled but initialManager is not configured`
|
||||
);
|
||||
}
|
||||
if (!isStringUserID(options.initialManager)) {
|
||||
throw new TypeError(
|
||||
`${options.initialManager} is not a valid initial manager mxid`
|
||||
);
|
||||
}
|
||||
|
||||
const createdRoom = await options.createManagedRoom(
|
||||
options.initialManager,
|
||||
options.clientUserID
|
||||
);
|
||||
(
|
||||
await roomAccountData.storeAccountData({
|
||||
room_id: createdRoom.toRoomIDOrAlias(),
|
||||
})
|
||||
).expect(`Unable to persist managed ${options.roomKindDescription}`);
|
||||
return createdRoom;
|
||||
}
|
||||
|
||||
if (options.configuredRoom === undefined) {
|
||||
throw new TypeError(
|
||||
`${options.roomKindDescription} is required when managed ${options.roomKindDescription} mode is disabled`
|
||||
);
|
||||
}
|
||||
|
||||
const configuredRoomReference = options.parseConfiguredRoom(
|
||||
options.configuredRoom
|
||||
);
|
||||
const configuredRoom = (
|
||||
await roomResolver.resolveRoom(configuredRoomReference)
|
||||
).expect(`Unable to resolve Draupnir's ${options.roomKindDescription}`);
|
||||
(await roomJoiner.joinRoom(configuredRoom)).expect(
|
||||
`Unable to join Draupnir's ${options.roomKindDescription}`
|
||||
);
|
||||
return configuredRoom;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
// Copyright 2022 Gnuxie <Gnuxie@protonmail.com>
|
||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
@@ -194,7 +195,7 @@ export class TrustedReporters
|
||||
|
||||
if (met.length > 0) {
|
||||
await this.draupnir.client.sendMessage(
|
||||
this.draupnir.config.managementRoom,
|
||||
this.draupnir.managementRoomID,
|
||||
{
|
||||
msgtype: "m.notice",
|
||||
body:
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
/// <reference types="mocha" />
|
||||
|
||||
import { StringUserID } from "@the-draupnir-project/matrix-basic-types";
|
||||
import { readTestConfig, setupHarnessWithConfig } from "../utils/harness";
|
||||
import { newTestUser } from "../../integration/clientHelper";
|
||||
import { MjolnirAppService } from "../../../src/appservice/AppService";
|
||||
|
||||
describe("Managed room bootstrap startup integration", function (this: Mocha.Suite) {
|
||||
it("appservice mode reaches started stage with managed admin room enabled", async function (this: Mocha.Context) {
|
||||
this.timeout(120000);
|
||||
const config = readTestConfig();
|
||||
const initialManager = await newTestUser(config.homeserver.url, {
|
||||
name: { contains: "managed-admin" },
|
||||
});
|
||||
config.managedAdminRoom = true;
|
||||
config.adminRoom = undefined;
|
||||
config.initialManager = (await initialManager.getUserId()) as StringUserID;
|
||||
|
||||
let appservice: MjolnirAppService | undefined;
|
||||
try {
|
||||
appservice = await setupHarnessWithConfig(config, {
|
||||
ensureAdminRoomAlias: false,
|
||||
});
|
||||
if (appservice.accessControlRoomID.length === 0) {
|
||||
throw new TypeError(
|
||||
"Managed appservice bootstrap did not produce an admin room"
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
initialManager.stop();
|
||||
if (appservice) {
|
||||
await appservice.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
// Copyright 2022 Gnuxie <Gnuxie@protonmail.com>
|
||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
@@ -33,33 +34,38 @@ export async function setupHarness(): Promise<MjolnirAppService> {
|
||||
}
|
||||
|
||||
export async function setupHarnessWithConfig(
|
||||
config: AppserviceConfig
|
||||
config: AppserviceConfig,
|
||||
options: {
|
||||
ensureAdminRoomAlias?: boolean;
|
||||
} = {}
|
||||
): Promise<MjolnirAppService> {
|
||||
const utilityUser = await newTestUser(config.homeserver.url, {
|
||||
name: { contains: "utility" },
|
||||
isAdmin: true,
|
||||
});
|
||||
if (
|
||||
typeof config.adminRoom !== "string" ||
|
||||
!isStringRoomAlias(config.adminRoom)
|
||||
) {
|
||||
throw new TypeError(
|
||||
"This test expects the harness config to have a room alias."
|
||||
if (options.ensureAdminRoomAlias ?? true) {
|
||||
const utilityUser = await newTestUser(config.homeserver.url, {
|
||||
name: { contains: "utility" },
|
||||
isAdmin: true,
|
||||
});
|
||||
if (
|
||||
typeof config.adminRoom !== "string" ||
|
||||
!isStringRoomAlias(config.adminRoom)
|
||||
) {
|
||||
throw new TypeError(
|
||||
"This test expects the harness config to have a room alias."
|
||||
);
|
||||
}
|
||||
await utilityUser
|
||||
.deleteRoomAlias(config.adminRoom)
|
||||
// Ignore if alias doesn't exist yet.
|
||||
.catch((_: unknown) => undefined);
|
||||
const adminRoomID = await ensureAliasedRoomExists(
|
||||
utilityUser,
|
||||
config.adminRoom
|
||||
);
|
||||
await utilityUser.setUserPowerLevel(
|
||||
"@draupnir-moderation:localhost:9999",
|
||||
adminRoomID,
|
||||
100
|
||||
);
|
||||
}
|
||||
await utilityUser
|
||||
.deleteRoomAlias(config.adminRoom)
|
||||
// Ignore if alias doesn't exist yet.
|
||||
.catch((_: unknown) => undefined);
|
||||
const adminRoomID = await ensureAliasedRoomExists(
|
||||
utilityUser,
|
||||
config.adminRoom
|
||||
);
|
||||
await utilityUser.setUserPowerLevel(
|
||||
"@draupnir-moderation:localhost:9999",
|
||||
adminRoomID,
|
||||
100
|
||||
);
|
||||
return await MjolnirAppService.run(
|
||||
9000,
|
||||
config,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0 AND Apache-2.0
|
||||
|
||||
import { configRead, getStoragePath, IConfig } from "../../src/config";
|
||||
import { DraupnirBotModeToggle } from "../../src/DraupnirBotMode";
|
||||
import { makeTopLevelStores } from "../../src/backingstore/DraupnirStores";
|
||||
import { draupnirClient, makeBotModeToggle } from "./mjolnirSetupUtils";
|
||||
import { DefaultEventDecoder } from "matrix-protection-suite";
|
||||
import { StringUserID } from "@the-draupnir-project/matrix-basic-types";
|
||||
|
||||
function makeManagedBotModeConfig(): IConfig {
|
||||
const config = configRead();
|
||||
const username = `managed_bootstrap_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
|
||||
const initialManager = `managed_bootstrap_manager_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
|
||||
const homeserverDomain = new URL(config.homeserverUrl).host;
|
||||
config.pantalaimon.username = username;
|
||||
config.pantalaimon.password = username;
|
||||
config.managementRoom = undefined;
|
||||
config.managedManagementRoom = true;
|
||||
config.initialManager =
|
||||
`@${initialManager}:${homeserverDomain}` as StringUserID;
|
||||
return config;
|
||||
}
|
||||
|
||||
describe("Managed room bootstrap startup integration", function (this: Mocha.Suite) {
|
||||
it("bot mode reaches started stage with managed management room enabled", async function (this: Mocha.Context) {
|
||||
this.timeout(120000);
|
||||
const config = makeManagedBotModeConfig();
|
||||
const storagePath = getStoragePath(config.dataPath);
|
||||
const stores = makeTopLevelStores(storagePath, DefaultEventDecoder, {
|
||||
isRoomStateBackingStoreEnabled:
|
||||
config.roomStateBackingStore.enabled ?? false,
|
||||
});
|
||||
|
||||
let toggle: DraupnirBotModeToggle | undefined;
|
||||
try {
|
||||
toggle = await makeBotModeToggle(config, {
|
||||
stores,
|
||||
allowSafeMode: false,
|
||||
deleteManagementRoomAliasOnStart: false,
|
||||
ensureManagementRoomAlias: false,
|
||||
});
|
||||
await draupnirClient()?.start();
|
||||
await toggle.encryptionInitialized();
|
||||
} finally {
|
||||
await toggle?.[Symbol.asyncDispose]();
|
||||
draupnirClient()?.stop();
|
||||
stores.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
|
||||
// Copyright 2022 - 2024 Gnuxie <Gnuxie@protonmail.com>
|
||||
// Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
@@ -144,11 +145,13 @@ export async function makeBotModeToggle(
|
||||
eraseAccountData,
|
||||
allowSafeMode,
|
||||
deleteManagementRoomAliasOnStart: deleteAlias,
|
||||
ensureManagementRoomAlias,
|
||||
}: {
|
||||
stores: TopLevelStores;
|
||||
eraseAccountData?: boolean;
|
||||
allowSafeMode?: boolean;
|
||||
deleteManagementRoomAliasOnStart?: boolean;
|
||||
ensureManagementRoomAlias?: boolean;
|
||||
} = { stores: { dispose() {} } }
|
||||
): Promise<DraupnirBotModeToggle> {
|
||||
await configureMjolnir(config);
|
||||
@@ -175,13 +178,48 @@ export async function makeBotModeToggle(
|
||||
config.homeserverUrl,
|
||||
await client.getUserId()
|
||||
);
|
||||
// defaults to true
|
||||
if (deleteAlias === undefined || deleteAlias) {
|
||||
await client
|
||||
.deleteRoomAlias(config.managementRoom)
|
||||
.catch((_: unknown) => undefined);
|
||||
if (config.managedManagementRoom && config.initialManager !== undefined) {
|
||||
const managerMatch = /^@([^:]+):/.exec(config.initialManager);
|
||||
if (managerMatch === null || managerMatch[1] === undefined) {
|
||||
throw new TypeError(
|
||||
`${config.initialManager} is not a valid initial manager mxid`
|
||||
);
|
||||
}
|
||||
const managerLocalpart = managerMatch[1];
|
||||
try {
|
||||
await registerUser(
|
||||
config.homeserverUrl,
|
||||
managerLocalpart,
|
||||
managerLocalpart,
|
||||
managerLocalpart,
|
||||
true
|
||||
);
|
||||
} catch (e) {
|
||||
if (
|
||||
typeof e !== "object" ||
|
||||
e === null ||
|
||||
!("body" in e) ||
|
||||
typeof e.body !== "object" ||
|
||||
e.body === null ||
|
||||
!("errcode" in e.body) ||
|
||||
e.body.errcode !== "M_USER_IN_USE"
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
// defaults to true
|
||||
if (ensureManagementRoomAlias ?? true) {
|
||||
if (config.managementRoom === undefined) {
|
||||
throw new TypeError("managementRoom is required when ensuring alias");
|
||||
}
|
||||
if (deleteAlias === undefined || deleteAlias) {
|
||||
await client
|
||||
.deleteRoomAlias(config.managementRoom)
|
||||
.catch((_: unknown) => undefined);
|
||||
}
|
||||
await ensureAliasedRoomExists(client, config.managementRoom);
|
||||
}
|
||||
await ensureAliasedRoomExists(client, config.managementRoom);
|
||||
const toggle = await DraupnirBotModeToggle.create(
|
||||
client,
|
||||
new SafeMatrixEmitterWrapper(client, DefaultEventDecoder),
|
||||
|
||||
@@ -52,6 +52,12 @@ acceptInvitesFromSpace: "!example:example.org"
|
||||
# Whether Draupnir should report ignored invites to the management room (if autojoinOnlyIfManager is true).
|
||||
recordIgnoredInvites: false
|
||||
|
||||
# Enable automatic management-room recovery/creation on first boot.
|
||||
managedManagementRoom: false
|
||||
|
||||
# The initial manager to invite if the management room has to be created.
|
||||
# initialManager: "@admin:example.org"
|
||||
|
||||
# The room ID (or room alias) of the management room, anyone in this room can issue commands to Draupnir.
|
||||
#
|
||||
# Draupnir has no more granular access controls other than this, be sure you trust everyone in this room - secure it!
|
||||
|
||||
Reference in new issue
Block a user