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
This commit is contained in:
gnuxie
2025-03-11 23:41:33 +00:00
committed by Gnuxie
parent a0f7ee5bb3
commit 0ede5c8682
5 changed files with 144 additions and 68 deletions
+25 -26
View File
@@ -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<MjolnirAppService> {
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(
+6 -3
View File
@@ -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()
);
})()
+1 -1
View File
@@ -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
+101 -36
View File
@@ -8,44 +8,109 @@
// https://github.com/matrix-org/mjolnir
// </text>
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<typeof AppserviceConfig>;
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),
});
+11 -2
View File
@@ -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<MjolnirAppService> {
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,