Skip to content

Commit 4015543

Browse files
authored
Filesystem config improvements (#604)
* Rename read to configRead as it should have always been. * Got a way to extract non-default values. Now let's try unknown configuration values. * Show unknown property paths with a warning. Now we just need to make this scrap available in commands. * Remove the old Mjolnir horrible RUNTIME client. * Make the path that is used to load the config available. * Warn when `--draupnir-config` isn't used. * Introduce configMeta so that we can log meta on process.exit later. * Only show non-default config values when draupnir is exiting. to reduce noise. * Get consistent with logging. So it turns out that mps4bot-sdk is using a different instance of the bot-sdk module than Draupnir, i think. Since we used to tell MPS's logger to use the bot-sdk's `LogService`, but the `setLogger` that was used was obviously inconsistent with Draupnir's. Obviously the bot-sdk should be a peer dependency in the bot-sdk to prevent this happening in future.
1 parent f9a7bb8 commit 4015543

File tree

7 files changed

+233
-54
lines changed

7 files changed

+233
-54
lines changed

src/DraupnirBotMode.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import {
1212
StandardClientsInRoomMap,
1313
DefaultEventDecoder,
14-
setGlobalLoggerProvider,
1514
RoomStateBackingStore,
1615
ClientsInRoomMap,
1716
Task,
@@ -21,7 +20,6 @@ import {
2120
ConfigRecoverableError,
2221
} from "matrix-protection-suite";
2322
import {
24-
BotSDKLogServiceLogger,
2523
ClientCapabilityFactory,
2624
MatrixSendClient,
2725
RoomStateManagerFactory,
@@ -51,8 +49,6 @@ import { ResultError } from "@gnuxie/typescript-result";
5149
import { SafeModeCause, SafeModeReason } from "./safemode/SafeModeCause";
5250
import { SafeModeBootOption } from "./safemode/BootOption";
5351

54-
setGlobalLoggerProvider(new BotSDKLogServiceLogger());
55-
5652
const log = new Logger("DraupnirBotMode");
5753

5854
export function constructWebAPIs(draupnir: Draupnir): WebAPIs {

src/config.ts

Lines changed: 187 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,35 @@
1010

1111
import * as fs from "fs";
1212
import { load } from "js-yaml";
13-
import { MatrixClient, LogService } from "matrix-bot-sdk";
13+
import { LogService, RichConsoleLogger } from "matrix-bot-sdk";
1414
import Config from "config";
1515
import path from "path";
1616
import { SafeModeBootOption } from "./safemode/BootOption";
17+
import { Logger, setGlobalLoggerProvider } from "matrix-protection-suite";
18+
19+
LogService.setLogger(new RichConsoleLogger());
20+
setGlobalLoggerProvider(new RichConsoleLogger());
21+
const log = new Logger("Draupnir config");
22+
23+
/**
24+
* The version of the configuration that has been explicitly provided,
25+
* and does not contain default values. Secrets are marked with "REDACTED".
26+
*/
27+
export function getNonDefaultConfigProperties(
28+
config: IConfig
29+
): Record<string, unknown> {
30+
const nonDefault = Config.util.diffDeep(defaultConfig, config);
31+
if ("accessToken" in nonDefault) {
32+
nonDefault.accessToken = "REDACTED";
33+
}
34+
if (
35+
"pantalaimon" in nonDefault &&
36+
typeof nonDefault.pantalaimon === "object"
37+
) {
38+
nonDefault.pantalaimon.password = "REDACTED";
39+
}
40+
return nonDefault;
41+
}
1742

1843
/**
1944
* The configuration, as read from production.yaml
@@ -64,9 +89,11 @@ export interface IConfig {
6489
* should be printed to our managementRoom.
6590
*/
6691
displayReports: boolean;
67-
admin?: {
68-
enableMakeRoomAdminCommand?: boolean;
69-
};
92+
admin?:
93+
| {
94+
enableMakeRoomAdminCommand?: boolean;
95+
}
96+
| undefined;
7097
commands: {
7198
allowNoPrefix: boolean;
7299
additionalPrefixes: string[];
@@ -103,15 +130,17 @@ export interface IConfig {
103130
unhealthyStatus: number;
104131
};
105132
// If specified, attempt to upload any crash statistics to sentry.
106-
sentry?: {
107-
dsn: string;
133+
sentry?:
134+
| {
135+
dsn: string;
108136

109-
// Frequency of performance monitoring.
110-
//
111-
// A number in [0.0, 1.0], where 0.0 means "don't bother with tracing"
112-
// and 1.0 means "trace performance at every opportunity".
113-
tracesSampleRate: number;
114-
};
137+
// Frequency of performance monitoring.
138+
//
139+
// A number in [0.0, 1.0], where 0.0 means "don't bother with tracing"
140+
// and 1.0 means "trace performance at every opportunity".
141+
tracesSampleRate: number;
142+
}
143+
| undefined;
115144
};
116145
web: {
117146
enabled: boolean;
@@ -130,13 +159,19 @@ export interface IConfig {
130159
// This can not be used with Pantalaimon.
131160
experimentalRustCrypto: boolean;
132161

133-
/**
134-
* Config options only set at runtime. Try to avoid using the objects
135-
* here as much as possible.
136-
*/
137-
RUNTIME: {
138-
client?: MatrixClient;
139-
};
162+
configMeta:
163+
| {
164+
/**
165+
* The path that the configuration file was loaded from.
166+
*/
167+
configPath: string;
168+
169+
isDraupnirConfigOptionUsed: boolean;
170+
171+
isAccessTokenPathOptionUsed: boolean;
172+
isPasswordPathOptionUsed: boolean;
173+
}
174+
| undefined;
140175
}
141176

142177
const defaultConfig: IConfig = {
@@ -204,7 +239,9 @@ const defaultConfig: IConfig = {
204239
healthyStatus: 200,
205240
unhealthyStatus: 418,
206241
},
242+
sentry: undefined,
207243
},
244+
admin: undefined,
208245
web: {
209246
enabled: false,
210247
port: 8080,
@@ -217,37 +254,95 @@ const defaultConfig: IConfig = {
217254
enabled: false,
218255
},
219256
experimentalRustCrypto: false,
220-
221-
// Needed to make the interface happy.
222-
RUNTIME: {},
257+
configMeta: undefined,
223258
};
224259

225260
export function getDefaultConfig(): IConfig {
226261
return Config.util.cloneDeep(defaultConfig);
227262
}
228263

264+
function logNonDefaultConfiguration(config: IConfig): void {
265+
log.info(
266+
"non-default configuration properties:",
267+
JSON.stringify(getNonDefaultConfigProperties(config), null, 2)
268+
);
269+
}
270+
271+
function logConfigMeta(config: IConfig): void {
272+
log.info("Configuration meta:", JSON.stringify(config.configMeta, null, 2));
273+
}
274+
275+
function getConfigPath(): {
276+
isDraupnirPath: boolean;
277+
path: string;
278+
} {
279+
const draupnirPath = getCommandLineOption(process.argv, "--draupnir-config");
280+
if (draupnirPath) {
281+
return { isDraupnirPath: true, path: draupnirPath };
282+
}
283+
const mjolnirPath = getCommandLineOption(process.argv, "--mjolnir-config");
284+
if (mjolnirPath) {
285+
return { isDraupnirPath: false, path: mjolnirPath };
286+
}
287+
const path = Config.util.getConfigSources().at(-1)?.name;
288+
if (path === undefined) {
289+
throw new TypeError("No configuration path has been found for Draupnir");
290+
}
291+
return { isDraupnirPath: false, path };
292+
}
293+
294+
function getConfigMeta(): NonNullable<IConfig["configMeta"]> {
295+
const { isDraupnirPath, path } = getConfigPath();
296+
return {
297+
configPath: path,
298+
isDraupnirConfigOptionUsed: isDraupnirPath,
299+
isAccessTokenPathOptionUsed: isCommandLineOptionPresent(
300+
process.argv,
301+
"--access-token-path"
302+
),
303+
isPasswordPathOptionUsed: isCommandLineOptionPresent(
304+
process.argv,
305+
"--pantalaimon-password-path"
306+
),
307+
};
308+
}
309+
229310
/**
230311
* @returns The users's raw config, deep copied over the `defaultConfig`.
231312
*/
232313
function readConfigSource(): IConfig {
233-
const explicitConfigPath = getCommandLineOption(
234-
process.argv,
235-
"--draupnir-config"
236-
);
237-
if (explicitConfigPath !== undefined) {
238-
const content = fs.readFileSync(explicitConfigPath, "utf8");
314+
const configMeta = getConfigMeta();
315+
const config = (() => {
316+
const content = fs.readFileSync(configMeta.configPath, "utf8");
239317
const parsed = load(content);
240-
return Config.util.extendDeep({}, defaultConfig, parsed);
241-
} else {
242-
return Config.util.extendDeep(
243-
{},
244-
defaultConfig,
245-
Config.util.toObject()
246-
) as IConfig;
318+
return Config.util.extendDeep({}, defaultConfig, parsed, {
319+
configMeta: configMeta,
320+
}) as IConfig;
321+
})();
322+
logConfigMeta(config);
323+
if (!configMeta.isDraupnirConfigOptionUsed) {
324+
log.warn(
325+
"DEPRECATED",
326+
"Starting Draupnir without the --draupnir-config option is deprecated. Please provide Draupnir's configuration explicitly with --draupnir-config.",
327+
"config path used:",
328+
config.configMeta?.configPath
329+
);
247330
}
331+
const unknownProperties = getUnknownConfigPropertyPaths(config);
332+
if (unknownProperties.length > 0) {
333+
log.warn(
334+
"There are unknown configuration properties, possibly a result of typos:",
335+
unknownProperties
336+
);
337+
}
338+
process.on("exit", () => {
339+
logNonDefaultConfiguration(config);
340+
logConfigMeta(config);
341+
});
342+
return config;
248343
}
249344

250-
export function read(): IConfig {
345+
export function configRead(): IConfig {
251346
const config = readConfigSource();
252347
const explicitAccessTokenPath = getCommandLineOption(
253348
process.argv,
@@ -290,7 +385,7 @@ export function getProvisionedMjolnirConfig(managementRoomId: string): IConfig {
290385
"backgroundDelayMS",
291386
"safeMode",
292387
];
293-
const configTemplate = read(); // we use the standard bot config as a template for every provisioned draupnir.
388+
const configTemplate = configRead(); // we use the standard bot config as a template for every provisioned draupnir.
294389
const unusedKeys = Object.keys(configTemplate).filter(
295390
(key) => !allowedKeys.includes(key)
296391
);
@@ -391,3 +486,58 @@ function getCommandLineOption(
391486
// No value was provided, or the next argument is another option
392487
throw new Error(`No value provided for ${optionName}`);
393488
}
489+
490+
type UnknownPropertyPaths = string[];
491+
492+
export function getUnknownPropertiesHelper(
493+
rawConfig: unknown,
494+
rawDefaults: unknown,
495+
currentPathProperties: string[]
496+
): UnknownPropertyPaths {
497+
const unknownProperties: UnknownPropertyPaths = [];
498+
if (
499+
typeof rawConfig !== "object" ||
500+
rawConfig === null ||
501+
Array.isArray(rawConfig)
502+
) {
503+
return unknownProperties;
504+
}
505+
if (rawDefaults === undefined || rawDefaults == null) {
506+
// the top level property should have been defined, these could be and
507+
// probably are custom properties.
508+
return unknownProperties;
509+
}
510+
if (typeof rawDefaults !== "object") {
511+
throw new TypeError("default and normal config are out of sync");
512+
}
513+
const defaultConfig = rawDefaults as Record<string, unknown>;
514+
const config = rawConfig as Record<string, unknown>;
515+
for (const key of Object.keys(config)) {
516+
if (!(key in defaultConfig)) {
517+
unknownProperties.push("/" + [...currentPathProperties, key].join("/"));
518+
} else {
519+
const unknownSubProperties = getUnknownPropertiesHelper(
520+
config[key],
521+
defaultConfig[key] as Record<string, unknown>,
522+
[...currentPathProperties, key]
523+
);
524+
unknownProperties.push(...unknownSubProperties);
525+
}
526+
}
527+
return unknownProperties;
528+
}
529+
530+
/**
531+
* Return a list of JSON paths to properties in the given config object that are not present in the default config.
532+
* This is used to detect typos in the config file.
533+
*/
534+
export function getUnknownConfigPropertyPaths(config: unknown): string[] {
535+
if (typeof config !== "object" || config === null) {
536+
return [];
537+
}
538+
return getUnknownPropertiesHelper(
539+
config,
540+
defaultConfig as unknown as Record<string, unknown>,
541+
[]
542+
);
543+
}

src/index.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ import {
1515
LogService,
1616
MatrixClient,
1717
PantalaimonClient,
18-
RichConsoleLogger,
1918
SimpleFsStorageProvider,
2019
RustSdkCryptoStorageProvider,
2120
} from "matrix-bot-sdk";
2221
import { StoreType } from "@matrix-org/matrix-sdk-crypto-nodejs";
23-
import { read as configRead } from "./config";
22+
import { configRead as configRead } from "./config";
2423
import { initializeSentry, patchMatrixClient } from "./utils";
2524
import { DraupnirBotModeToggle } from "./DraupnirBotMode";
2625
import { SafeMatrixEmitterWrapper } from "matrix-protection-suite-for-matrix-bot-sdk";
@@ -30,9 +29,6 @@ import { SqliteRoomStateBackingStore } from "./backingstore/better-sqlite3/Sqlit
3029
void (async function () {
3130
const config = configRead();
3231

33-
config.RUNTIME = {};
34-
35-
LogService.setLogger(new RichConsoleLogger());
3632
LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG));
3733

3834
LogService.info("index", "Starting bot...");
@@ -48,6 +44,7 @@ void (async function () {
4844
}
4945

5046
let bot: DraupnirBotModeToggle | null = null;
47+
let client: MatrixClient;
5148
try {
5249
const storagePath = path.isAbsolute(config.dataPath)
5350
? config.dataPath
@@ -56,7 +53,6 @@ void (async function () {
5653
path.join(storagePath, "bot.json")
5754
);
5855

59-
let client: MatrixClient;
6056
if (config.pantalaimon.use && !config.experimentalRustCrypto) {
6157
const pantalaimon = new PantalaimonClient(config.homeserverUrl, storage);
6258
client = await pantalaimon.createClientWithCredentials(
@@ -88,7 +84,6 @@ void (async function () {
8884
);
8985
}
9086
patchMatrixClient();
91-
config.RUNTIME.client = client;
9287
const eventDecoder = DefaultEventDecoder;
9388
const store = config.roomStateBackingStore.enabled
9489
? new SqliteRoomStateBackingStore(
@@ -115,7 +110,7 @@ void (async function () {
115110
throw err;
116111
}
117112
try {
118-
await config.RUNTIME.client.start();
113+
await client.start();
119114
await bot.encryptionInitialized();
120115
healthz.isHealthy = true;
121116
} catch (err) {

test/integration/fixtures.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
MJOLNIR_PROTECTED_ROOMS_EVENT_TYPE,
1313
MJOLNIR_WATCHED_POLICY_ROOMS_EVENT_TYPE,
1414
} from "matrix-protection-suite";
15-
import { read as configRead } from "../../src/config";
15+
import { configRead } from "../../src/config";
1616
import { patchMatrixClient } from "../../src/utils";
1717
import {
1818
DraupnirTestContext,
@@ -52,7 +52,6 @@ export const mochaHooks = {
5252
if (draupnirMatrixClient === null) {
5353
throw new TypeError(`setup code is broken`);
5454
}
55-
config.RUNTIME.client = draupnirMatrixClient;
5655
await draupnirClient()?.start();
5756
await this.toggle.encryptionInitialized();
5857
console.log("mochaHooks.beforeEach DONE");

0 commit comments

Comments
 (0)