From 0ede5c86826a6f6e6c9826577bc8ee9dfecffd8d Mon Sep 17 00:00:00 2001 From: gnuxie Date: Tue, 11 Mar 2025 23:41:33 +0000 Subject: [PATCH] Add config schema to appservice config. Make appservice datapath example consistent with docker image. Make the appservice config schema check the admin room properly. We now parse the room id/alias/or permalink. Make sure to parse the config from cli.ts --- src/appservice/AppService.ts | 51 ++++---- src/appservice/cli.ts | 9 +- src/appservice/config/config.example.yaml | 2 +- src/appservice/config/config.ts | 137 ++++++++++++++++------ test/appservice/utils/harness.ts | 13 +- 5 files changed, 144 insertions(+), 68 deletions(-) diff --git a/src/appservice/AppService.ts b/src/appservice/AppService.ts index 34616c9f..b10fe7d4 100644 --- a/src/appservice/AppService.ts +++ b/src/appservice/AppService.ts @@ -21,7 +21,7 @@ import { import { DataStore } from ".//datastore"; import { PgDataStore } from "./postgres/PgDataStore"; import { Api } from "./Api"; -import { IConfig } from "./config/config"; +import { AppserviceConfig } from "./config/config"; import { AccessControl } from "./AccessControl"; import { AppserviceCommandHandler } from "./bot/AppserviceCommandHandler"; import { getStoragePath, SOFTWARE_VERSION } from "../config"; @@ -30,7 +30,6 @@ import { ClientCapabilityFactory, RoomStateManagerFactory, joinedRoomsSafe, - resolveRoomReferenceSafe, } from "matrix-protection-suite-for-matrix-bot-sdk"; import { ClientsInRoomMap, @@ -43,11 +42,11 @@ import { } from "matrix-protection-suite"; import { AppServiceDraupnirManager } from "./AppServiceDraupnirManager"; import { + isStringRoomAlias, + isStringRoomID, MatrixRoomReference, StringRoomID, StringUserID, - isStringRoomAlias, - isStringRoomID, } from "@the-draupnir-project/matrix-basic-types"; import { SqliteRoomStateBackingStore } from "../backingstore/better-sqlite3/SqliteRoomStateBackingStore"; @@ -65,7 +64,7 @@ export class MjolnirAppService { * use `makeMjolnirAppService`. */ private constructor( - public readonly config: IConfig, + public readonly config: AppserviceConfig, public readonly bridge: Bridge, public readonly draupnirManager: AppServiceDraupnirManager, public readonly accessControl: AccessControl, @@ -97,7 +96,7 @@ export class MjolnirAppService { * @returns A new `MjolnirAppService`. */ public static async makeMjolnirAppService( - config: IConfig, + config: AppserviceConfig, dataStore: DataStore, eventDecoder: EventDecoder, registrationFilePath: string, @@ -122,24 +121,6 @@ export class MjolnirAppService { disableStores: true, }); await bridge.initialise(); - 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); - if (isError(parseResult)) { - throw new TypeError( - `${config.adminRoom} needs to be a room id, alias or permalink` - ); - } - return parseResult.ok; - } - })(); - const accessControlRoom = ( - await resolveRoomReferenceSafe(bridge.getBot().getClient(), adminRoom) - ).expect("Unable to resolve the admin room"); const clientsInRoomMap = new StandardClientsInRoomMap(); const clientProvider = async (clientUserID: StringUserID) => bridge.getIntent(clientUserID).matrixClient; @@ -162,6 +143,24 @@ export class MjolnirAppService { 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); + if (isError(parseResult)) { + throw new TypeError( + `${config.adminRoom} 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"); const appserviceBotPolicyRoomManager = await roomStateManagerFactory.getPolicyRoomManager(botUserID); const accessControl = ( @@ -224,7 +223,7 @@ export class MjolnirAppService { */ public static async run( port: number, - config: IConfig, + config: AppserviceConfig, registrationFilePath: string ): Promise { Logger.configure(config.logging ?? { console: "debug" }); @@ -232,7 +231,7 @@ export class MjolnirAppService { await dataStore.init(); const eventDecoder = DefaultEventDecoder; const storagePath = getStoragePath(config.dataPath); - const backingStore = config.roomStateBackingStore.enabled + const backingStore = config.roomStateBackingStore?.enabled ? SqliteRoomStateBackingStore.create(storagePath, eventDecoder) : undefined; const service = await MjolnirAppService.makeMjolnirAppService( diff --git a/src/appservice/cli.ts b/src/appservice/cli.ts index f054732d..909bd399 100644 --- a/src/appservice/cli.ts +++ b/src/appservice/cli.ts @@ -10,8 +10,9 @@ import { Cli } from "matrix-appservice-bridge"; import { MjolnirAppService } from "./AppService"; -import { IConfig } from "./config/config"; import { Task } from "matrix-protection-suite"; +import { AppserviceConfig } from "./config/config"; +import { Value } from "@sinclair/typebox/value"; /** * This file provides the entrypoint for the appservice mode for draupnir. @@ -27,7 +28,7 @@ const cli = new Cli({ }, generateRegistration: MjolnirAppService.generateRegistration, run: function (port: number) { - const config: IConfig | null = cli.getConfig() as unknown as IConfig | null; + const config = cli.getConfig(); if (config === null) { throw new Error("Couldn't load config"); } @@ -35,7 +36,9 @@ const cli = new Cli({ (async () => { await MjolnirAppService.run( port, - config, + // we use the matrix-appservice-bridge library to handle cli arguments for loading the config + // but we have to still validate it ourselves. + Value.Decode(AppserviceConfig, config), cli.getRegistrationFilePath() ); })() diff --git a/src/appservice/config/config.example.yaml b/src/appservice/config/config.example.yaml index 3ea4ec2a..38cf27c6 100644 --- a/src/appservice/config/config.example.yaml +++ b/src/appservice/config/config.example.yaml @@ -18,7 +18,7 @@ webAPI: port: 9001 # The directory the bot should store various bits of information in -dataPath: "./test/harness/mjolnir-data/" +dataPath: "/data/storage" roomStateBackingStore: enabled: false diff --git a/src/appservice/config/config.ts b/src/appservice/config/config.ts index 3bb4480d..70514d34 100644 --- a/src/appservice/config/config.ts +++ b/src/appservice/config/config.ts @@ -8,44 +8,109 @@ // https://github.com/matrix-org/mjolnir // +import { Type } from "@sinclair/typebox"; import * as fs from "fs"; import { load } from "js-yaml"; -import { LoggingOpts } from "matrix-appservice-bridge"; +import { Value } from "@sinclair/typebox/value"; +import { EDStatic } from "matrix-protection-suite"; -export interface IConfig { - /** Details for the homeserver the appservice will be serving */ - homeserver: { - /** The domain of the homeserver that is found at the end of mxids */ - domain: string; - /** The url to use to acccess the client server api e.g. "https://matrix-client.matrix.org" */ - url: string; - }; - /** Details for the database backend */ - db: { - /** Postgres connection string */ - connectionString: string; - }; - /** Config for the web api used to access the appservice via the widget */ - webAPI: { - port: number; - }; - /** The admin room for the appservice bot. Not called managementRoom like draupnir on purpose, so they're not mixed in code somehow. */ - adminRoom: string; - /** configuration for matrix-appservice-bridge's Logger */ - logging?: LoggingOpts; - - dataPath: string; - - // Store room state using sqlite to improve startup time when Synapse responds - // slowly to requests for `/state`. - roomStateBackingStore: { - enabled?: boolean; - }; -} - -export function read(configPath: string): IConfig { +export function read(configPath: string): AppserviceConfig { const content = fs.readFileSync(configPath, "utf8"); - const parsed = load(content); - const config = parsed as object as IConfig; - return config; + const jsonParsed = load(content); + const decodedConfig = Value.Decode(AppserviceConfig, jsonParsed); + return decodedConfig; } + +export const LoggingOptsSchema = Type.Object({ + console: Type.Optional( + Type.Union( + [ + Type.Literal("debug"), + Type.Literal("info"), + Type.Literal("warn"), + Type.Literal("error"), + Type.Literal("trace"), + Type.Literal("off"), + ], + { description: "The log level used by the console output." } + ) + ), + json: Type.Optional( + Type.Boolean({ + description: + "Should the logs be outputted in JSON format, for consumption by a collector.", + }) + ), + colorize: Type.Optional( + Type.Boolean({ + description: + "Should the logs color-code the level strings in the output.", + }) + ), + timestampFormat: Type.Optional( + Type.String({ + description: "Timestamp format used in the log output.", + default: "HH:mm:ss:SSS", + }) + ), +}); + +export type AppserviceConfig = EDStatic; +export const AppserviceConfig = Type.Object({ + homeserver: Type.Object( + { + domain: Type.String({ + description: + "The domain of the homeserver that is found at the end of mxids", + }), + url: Type.String({ + description: + "The url to use to access the client server api e.g. 'https://matrix-client.matrix.org'", + }), + }, + { + description: "Details for the homeserver the appservice will be serving ", + } + ), + db: Type.Object( + { + connectionString: Type.String({ + description: "Postgres connection string", + }), + }, + { description: "Details for the database backend" } + ), + webAPI: Type.Object( + { + port: Type.Number({ + description: + "Port number for the web API used to access the appservice via the widget", + }), + }, + { + description: + "Config for the web api used to access the appservice via the widget", + } + ), + 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.", + }), + roomStateBackingStore: Type.Optional( + Type.Object( + { + enabled: Type.Boolean(), + }, + { + description: + "Store room state using sqlite to improve startup time when Synapse responds slowly to requests for `/state`.", + } + ) + ), + dataPath: Type.String({ + description: + "A directory where the appservice can storestore persistent data.", + default: "/data/storage", + }), + logging: Type.Optional(LoggingOptsSchema), +}); diff --git a/test/appservice/utils/harness.ts b/test/appservice/utils/harness.ts index cdf6c7ef..f71c29c5 100644 --- a/test/appservice/utils/harness.ts +++ b/test/appservice/utils/harness.ts @@ -13,13 +13,14 @@ import { MjolnirAppService } from "../../../src/appservice/AppService"; import { ensureAliasedRoomExists } from "../../integration/mjolnirSetupUtils"; import { read as configRead, - IConfig, + AppserviceConfig, } from "../../../src/appservice/config/config"; import { newTestUser } from "../../integration/clientHelper"; import { CreateEvent, MatrixClient } from "matrix-bot-sdk"; import { POLICY_ROOM_TYPE_VARIANTS } from "matrix-protection-suite"; +import { isStringRoomAlias } from "@the-draupnir-project/matrix-basic-types"; -export function readTestConfig(): IConfig { +export function readTestConfig(): AppserviceConfig { return configRead( path.join(__dirname, "../../../src/appservice/config/config.harness.yaml") ); @@ -30,6 +31,14 @@ export async function setupHarness(): Promise { const utilityUser = await newTestUser(config.homeserver.url, { name: { contains: "utility" }, }); + if ( + typeof config.adminRoom !== "string" || + !isStringRoomAlias(config.adminRoom) + ) { + throw new TypeError( + "This test expects the harness config to have a room alias." + ); + } await ensureAliasedRoomExists(utilityUser, config.adminRoom); return await MjolnirAppService.run( 9000,