Skip to content

Commit 0ede5c8

Browse files
committed
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
1 parent a0f7ee5 commit 0ede5c8

File tree

5 files changed

+144
-68
lines changed

5 files changed

+144
-68
lines changed

src/appservice/AppService.ts

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
import { DataStore } from ".//datastore";
2222
import { PgDataStore } from "./postgres/PgDataStore";
2323
import { Api } from "./Api";
24-
import { IConfig } from "./config/config";
24+
import { AppserviceConfig } from "./config/config";
2525
import { AccessControl } from "./AccessControl";
2626
import { AppserviceCommandHandler } from "./bot/AppserviceCommandHandler";
2727
import { getStoragePath, SOFTWARE_VERSION } from "../config";
@@ -30,7 +30,6 @@ import {
3030
ClientCapabilityFactory,
3131
RoomStateManagerFactory,
3232
joinedRoomsSafe,
33-
resolveRoomReferenceSafe,
3433
} from "matrix-protection-suite-for-matrix-bot-sdk";
3534
import {
3635
ClientsInRoomMap,
@@ -43,11 +42,11 @@ import {
4342
} from "matrix-protection-suite";
4443
import { AppServiceDraupnirManager } from "./AppServiceDraupnirManager";
4544
import {
45+
isStringRoomAlias,
46+
isStringRoomID,
4647
MatrixRoomReference,
4748
StringRoomID,
4849
StringUserID,
49-
isStringRoomAlias,
50-
isStringRoomID,
5150
} from "@the-draupnir-project/matrix-basic-types";
5251
import { SqliteRoomStateBackingStore } from "../backingstore/better-sqlite3/SqliteRoomStateBackingStore";
5352

@@ -65,7 +64,7 @@ export class MjolnirAppService {
6564
* use `makeMjolnirAppService`.
6665
*/
6766
private constructor(
68-
public readonly config: IConfig,
67+
public readonly config: AppserviceConfig,
6968
public readonly bridge: Bridge,
7069
public readonly draupnirManager: AppServiceDraupnirManager,
7170
public readonly accessControl: AccessControl,
@@ -97,7 +96,7 @@ export class MjolnirAppService {
9796
* @returns A new `MjolnirAppService`.
9897
*/
9998
public static async makeMjolnirAppService(
100-
config: IConfig,
99+
config: AppserviceConfig,
101100
dataStore: DataStore,
102101
eventDecoder: EventDecoder,
103102
registrationFilePath: string,
@@ -122,24 +121,6 @@ export class MjolnirAppService {
122121
disableStores: true,
123122
});
124123
await bridge.initialise();
125-
const adminRoom = (() => {
126-
if (isStringRoomID(config.adminRoom)) {
127-
return MatrixRoomReference.fromRoomID(config.adminRoom);
128-
} else if (isStringRoomAlias(config.adminRoom)) {
129-
return MatrixRoomReference.fromRoomIDOrAlias(config.adminRoom);
130-
} else {
131-
const parseResult = MatrixRoomReference.fromPermalink(config.adminRoom);
132-
if (isError(parseResult)) {
133-
throw new TypeError(
134-
`${config.adminRoom} needs to be a room id, alias or permalink`
135-
);
136-
}
137-
return parseResult.ok;
138-
}
139-
})();
140-
const accessControlRoom = (
141-
await resolveRoomReferenceSafe(bridge.getBot().getClient(), adminRoom)
142-
).expect("Unable to resolve the admin room");
143124
const clientsInRoomMap = new StandardClientsInRoomMap();
144125
const clientProvider = async (clientUserID: StringUserID) =>
145126
bridge.getIntent(clientUserID).matrixClient;
@@ -162,6 +143,24 @@ export class MjolnirAppService {
162143
const botRoomJoiner = clientCapabilityFactory
163144
.makeClientPlatform(botUserID, bridge.getBot().getClient())
164145
.toRoomJoiner();
146+
const adminRoom = (() => {
147+
if (isStringRoomID(config.adminRoom)) {
148+
return MatrixRoomReference.fromRoomID(config.adminRoom);
149+
} else if (isStringRoomAlias(config.adminRoom)) {
150+
return MatrixRoomReference.fromRoomIDOrAlias(config.adminRoom);
151+
} else {
152+
const parseResult = MatrixRoomReference.fromPermalink(config.adminRoom);
153+
if (isError(parseResult)) {
154+
throw new TypeError(
155+
`${config.adminRoom} needs to be a room id, alias or permalink`
156+
);
157+
}
158+
return parseResult.ok;
159+
}
160+
})();
161+
const accessControlRoom = (
162+
await botRoomJoiner.resolveRoom(adminRoom)
163+
).expect("Unable to resolve the admin room");
165164
const appserviceBotPolicyRoomManager =
166165
await roomStateManagerFactory.getPolicyRoomManager(botUserID);
167166
const accessControl = (
@@ -224,15 +223,15 @@ export class MjolnirAppService {
224223
*/
225224
public static async run(
226225
port: number,
227-
config: IConfig,
226+
config: AppserviceConfig,
228227
registrationFilePath: string
229228
): Promise<MjolnirAppService> {
230229
Logger.configure(config.logging ?? { console: "debug" });
231230
const dataStore = new PgDataStore(config.db.connectionString);
232231
await dataStore.init();
233232
const eventDecoder = DefaultEventDecoder;
234233
const storagePath = getStoragePath(config.dataPath);
235-
const backingStore = config.roomStateBackingStore.enabled
234+
const backingStore = config.roomStateBackingStore?.enabled
236235
? SqliteRoomStateBackingStore.create(storagePath, eventDecoder)
237236
: undefined;
238237
const service = await MjolnirAppService.makeMjolnirAppService(

src/appservice/cli.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010

1111
import { Cli } from "matrix-appservice-bridge";
1212
import { MjolnirAppService } from "./AppService";
13-
import { IConfig } from "./config/config";
1413
import { Task } from "matrix-protection-suite";
14+
import { AppserviceConfig } from "./config/config";
15+
import { Value } from "@sinclair/typebox/value";
1516

1617
/**
1718
* This file provides the entrypoint for the appservice mode for draupnir.
@@ -27,15 +28,17 @@ const cli = new Cli({
2728
},
2829
generateRegistration: MjolnirAppService.generateRegistration,
2930
run: function (port: number) {
30-
const config: IConfig | null = cli.getConfig() as unknown as IConfig | null;
31+
const config = cli.getConfig();
3132
if (config === null) {
3233
throw new Error("Couldn't load config");
3334
}
3435
void Task(
3536
(async () => {
3637
await MjolnirAppService.run(
3738
port,
38-
config,
39+
// we use the matrix-appservice-bridge library to handle cli arguments for loading the config
40+
// but we have to still validate it ourselves.
41+
Value.Decode(AppserviceConfig, config),
3942
cli.getRegistrationFilePath()
4043
);
4144
})()

src/appservice/config/config.example.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ webAPI:
1818
port: 9001
1919

2020
# The directory the bot should store various bits of information in
21-
dataPath: "./test/harness/mjolnir-data/"
21+
dataPath: "/data/storage"
2222

2323
roomStateBackingStore:
2424
enabled: false

src/appservice/config/config.ts

Lines changed: 101 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,44 +8,109 @@
88
// https://github.com/matrix-org/mjolnir
99
// </text>
1010

11+
import { Type } from "@sinclair/typebox";
1112
import * as fs from "fs";
1213
import { load } from "js-yaml";
13-
import { LoggingOpts } from "matrix-appservice-bridge";
14+
import { Value } from "@sinclair/typebox/value";
15+
import { EDStatic } from "matrix-protection-suite";
1416

15-
export interface IConfig {
16-
/** Details for the homeserver the appservice will be serving */
17-
homeserver: {
18-
/** The domain of the homeserver that is found at the end of mxids */
19-
domain: string;
20-
/** The url to use to acccess the client server api e.g. "https://matrix-client.matrix.org" */
21-
url: string;
22-
};
23-
/** Details for the database backend */
24-
db: {
25-
/** Postgres connection string */
26-
connectionString: string;
27-
};
28-
/** Config for the web api used to access the appservice via the widget */
29-
webAPI: {
30-
port: number;
31-
};
32-
/** The admin room for the appservice bot. Not called managementRoom like draupnir on purpose, so they're not mixed in code somehow. */
33-
adminRoom: string;
34-
/** configuration for matrix-appservice-bridge's Logger */
35-
logging?: LoggingOpts;
36-
37-
dataPath: string;
38-
39-
// Store room state using sqlite to improve startup time when Synapse responds
40-
// slowly to requests for `/state`.
41-
roomStateBackingStore: {
42-
enabled?: boolean;
43-
};
44-
}
45-
46-
export function read(configPath: string): IConfig {
17+
export function read(configPath: string): AppserviceConfig {
4718
const content = fs.readFileSync(configPath, "utf8");
48-
const parsed = load(content);
49-
const config = parsed as object as IConfig;
50-
return config;
19+
const jsonParsed = load(content);
20+
const decodedConfig = Value.Decode(AppserviceConfig, jsonParsed);
21+
return decodedConfig;
5122
}
23+
24+
export const LoggingOptsSchema = Type.Object({
25+
console: Type.Optional(
26+
Type.Union(
27+
[
28+
Type.Literal("debug"),
29+
Type.Literal("info"),
30+
Type.Literal("warn"),
31+
Type.Literal("error"),
32+
Type.Literal("trace"),
33+
Type.Literal("off"),
34+
],
35+
{ description: "The log level used by the console output." }
36+
)
37+
),
38+
json: Type.Optional(
39+
Type.Boolean({
40+
description:
41+
"Should the logs be outputted in JSON format, for consumption by a collector.",
42+
})
43+
),
44+
colorize: Type.Optional(
45+
Type.Boolean({
46+
description:
47+
"Should the logs color-code the level strings in the output.",
48+
})
49+
),
50+
timestampFormat: Type.Optional(
51+
Type.String({
52+
description: "Timestamp format used in the log output.",
53+
default: "HH:mm:ss:SSS",
54+
})
55+
),
56+
});
57+
58+
export type AppserviceConfig = EDStatic<typeof AppserviceConfig>;
59+
export const AppserviceConfig = Type.Object({
60+
homeserver: Type.Object(
61+
{
62+
domain: Type.String({
63+
description:
64+
"The domain of the homeserver that is found at the end of mxids",
65+
}),
66+
url: Type.String({
67+
description:
68+
"The url to use to access the client server api e.g. 'https://matrix-client.matrix.org'",
69+
}),
70+
},
71+
{
72+
description: "Details for the homeserver the appservice will be serving ",
73+
}
74+
),
75+
db: Type.Object(
76+
{
77+
connectionString: Type.String({
78+
description: "Postgres connection string",
79+
}),
80+
},
81+
{ description: "Details for the database backend" }
82+
),
83+
webAPI: Type.Object(
84+
{
85+
port: Type.Number({
86+
description:
87+
"Port number for the web API used to access the appservice via the widget",
88+
}),
89+
},
90+
{
91+
description:
92+
"Config for the web api used to access the appservice via the widget",
93+
}
94+
),
95+
adminRoom: Type.String({
96+
description:
97+
"The admin room for the appservice bot. Not called managementRoom like draupnir on purpose, so they're not mixed in code somehow.",
98+
}),
99+
roomStateBackingStore: Type.Optional(
100+
Type.Object(
101+
{
102+
enabled: Type.Boolean(),
103+
},
104+
{
105+
description:
106+
"Store room state using sqlite to improve startup time when Synapse responds slowly to requests for `/state`.",
107+
}
108+
)
109+
),
110+
dataPath: Type.String({
111+
description:
112+
"A directory where the appservice can storestore persistent data.",
113+
default: "/data/storage",
114+
}),
115+
logging: Type.Optional(LoggingOptsSchema),
116+
});

test/appservice/utils/harness.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ import { MjolnirAppService } from "../../../src/appservice/AppService";
1313
import { ensureAliasedRoomExists } from "../../integration/mjolnirSetupUtils";
1414
import {
1515
read as configRead,
16-
IConfig,
16+
AppserviceConfig,
1717
} from "../../../src/appservice/config/config";
1818
import { newTestUser } from "../../integration/clientHelper";
1919
import { CreateEvent, MatrixClient } from "matrix-bot-sdk";
2020
import { POLICY_ROOM_TYPE_VARIANTS } from "matrix-protection-suite";
21+
import { isStringRoomAlias } from "@the-draupnir-project/matrix-basic-types";
2122

22-
export function readTestConfig(): IConfig {
23+
export function readTestConfig(): AppserviceConfig {
2324
return configRead(
2425
path.join(__dirname, "../../../src/appservice/config/config.harness.yaml")
2526
);
@@ -30,6 +31,14 @@ export async function setupHarness(): Promise<MjolnirAppService> {
3031
const utilityUser = await newTestUser(config.homeserver.url, {
3132
name: { contains: "utility" },
3233
});
34+
if (
35+
typeof config.adminRoom !== "string" ||
36+
!isStringRoomAlias(config.adminRoom)
37+
) {
38+
throw new TypeError(
39+
"This test expects the harness config to have a room alias."
40+
);
41+
}
3342
await ensureAliasedRoomExists(utilityUser, config.adminRoom);
3443
return await MjolnirAppService.run(
3544
9000,

0 commit comments

Comments
 (0)