Report validation errors.

This has failed miserably because for some reason transform errors
are reported differently to schema validation errors. They aren't
treated like a TypeBox `ValueError`.

`Config.util.extendDeep` also for some obscure reason introduces
`null`s when objects such as `sentry` are undefined,
so the schema had to be changed for that.
This commit is contained in:
gnuxie
2024-06-12 16:43:54 +01:00
parent da033e346a
commit 044e13fe68
+36 -113
View File
@@ -30,6 +30,11 @@ import { load } from "js-yaml";
import { MatrixClient, LogService } from "matrix-bot-sdk";
import Config from "config";
import path from "path";
import { ConfigSchema } from "./ConfigSchema";
import { ActionResult, DecodeException, Logger, StringRoomID, Value, isError } from "matrix-protection-suite";
import { ValueError } from "@sinclair/typebox/errors"
const log = new Logger('config');
/**
* The configuration, as read from production.yaml
@@ -39,106 +44,7 @@ import path from "path";
// The object is magically generated by external lib `config`
// from the file specified by `NODE_ENV`, e.g. production.yaml
// or harness.yaml.
export interface IConfig {
homeserverUrl: string;
rawHomeserverUrl: string;
accessToken: string;
pantalaimon: {
use: boolean;
username: string;
password: string;
};
dataPath: string;
/**
* If true, Mjolnir will only accept invites from users present in managementRoom.
* Otherwise a space must be provided to `acceptInvitesFromSpace`.
*/
autojoinOnlyIfManager: boolean;
/** Mjolnir will accept invites from members of this space if `autojoinOnlyIfManager` is false. */
acceptInvitesFromSpace: string;
recordIgnoredInvites: boolean;
managementRoom: string;
verboseLogging: boolean;
logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
logMutedModules: string[],
syncOnStartup: boolean;
verifyPermissionsOnStartup: boolean;
disableServerACL: boolean;
noop: boolean;
protectedRooms: string[]; // matrix.to urls
fasterMembershipChecks: boolean;
automaticallyRedactForReasons: string[]; // case-insensitive globs
protectAllJoinedRooms: boolean;
/**
* Backgrounded tasks: number of milliseconds to wait between the completion
* of one background task and the start of the next one.
*/
backgroundDelayMS: number;
pollReports: boolean;
/**
* Whether or not new reports, received either by webapi or polling,
* should be printed to our managementRoom.
*/
displayReports: boolean;
admin?: {
enableMakeRoomAdminCommand?: boolean;
}
commands: {
allowNoPrefix: boolean;
additionalPrefixes: string[];
confirmWildcardBan: boolean;
features: string[];
ban: {
defaultReasons: string[]
}
};
protections: {
wordlist: {
words: string[];
minutesBeforeTrusting: number;
};
};
health: {
healthz: {
enabled: boolean;
port: number;
address: string;
endpoint: string;
healthyStatus: number;
unhealthyStatus: number;
};
// If specified, attempt to upload any crash statistics to sentry.
sentry?: {
dsn: string;
// Frequency of performance monitoring.
//
// A number in [0.0, 1.0], where 0.0 means "don't bother with tracing"
// and 1.0 means "trace performance at every opportunity".
tracesSampleRate: number;
};
};
web: {
enabled: boolean;
port: number;
address: string;
abuseReporting: {
enabled: boolean;
}
};
// Store room state using sqlite to improve startup time when Synapse responds
// slowly to requests for `/state`.
roomStateBackingStore: {
enabled?: boolean;
};
// Experimental usage of the matrix-bot-sdk rust crypto.
// This can not be used with Pantalaimon.
experimentalRustCrypto: boolean;
/**
* Config options only set at runtime. Try to avoid using the objects
* here as much as possible.
*/
export interface IConfig extends ConfigSchema {
RUNTIME: {
client?: MatrixClient;
};
@@ -154,19 +60,14 @@ const defaultConfig: IConfig = {
password: "",
},
dataPath: "/data/storage",
acceptInvitesFromSpace: '!noop:example.org',
managementRoom: '!noop:example.org' as StringRoomID,
acceptInvitesFromSpace: '!noop:example.org' as StringRoomID,
autojoinOnlyIfManager: true,
recordIgnoredInvites: false,
managementRoom: "!noop:example.org",
verboseLogging: false,
logLevel: "INFO",
logMutedModules: ['MatrixHttpClient', 'MatrixClientLite'],
syncOnStartup: true,
verifyPermissionsOnStartup: true,
noop: false,
disableServerACL: false,
protectedRooms: [],
fasterMembershipChecks: false,
automaticallyRedactForReasons: ["spam", "advertising"],
protectAllJoinedRooms: false,
backgroundDelayMS: 500,
@@ -176,9 +77,6 @@ const defaultConfig: IConfig = {
allowNoPrefix: false,
additionalPrefixes: ["draupnir"],
confirmWildcardBan: true,
features: [
"synapse admin",
],
ban: {
defaultReasons: [
"spam",
@@ -203,6 +101,7 @@ const defaultConfig: IConfig = {
healthyStatus: 200,
unhealthyStatus: 418,
},
sentry: undefined,
},
web: {
enabled: false,
@@ -229,19 +128,43 @@ export function getDefaultConfig(): IConfig {
/**
* @returns The users's raw config, deep copied over the `defaultConfig`.
*/
function readConfigSource(): IConfig {
function readConfigSource(): Record<string, unknown> {
const explicitConfigPath = getCommandLineOption(process.argv, "--draupnir-config");
if (explicitConfigPath !== undefined) {
const content = fs.readFileSync(explicitConfigPath, "utf8");
const parsed = load(content);
return Config.util.extendDeep({}, defaultConfig, parsed);
} else {
return Config.util.extendDeep({}, defaultConfig, Config.util.toObject()) as IConfig;
return Config.util.extendDeep({}, defaultConfig, Config.util.toObject());
}
}
function readConfigSourceAndValidate(): ActionResult<IConfig, DecodeException> {
const source = readConfigSource();
const decodedSource = Value.Decode(ConfigSchema, source, {suppressLogOnError: true});
return decodedSource as ActionResult<IConfig, DecodeException>;
}
function renderValidationError(error: ValueError): void {
log.error(
`Config value at path ${error.path} failed to validate with value:`,
error.value,
`and the following message:`,
error.message
);
log.info(`Here is the description for ${error.path}:`, error.schema.description);
}
export function read(): IConfig {
const config = readConfigSource();
const configResult = readConfigSourceAndValidate();
if (isError(configResult)) {
configResult.error.errors.forEach(renderValidationError)
if (configResult.error.errors.length === 0) {
log.error(`Config error:`, configResult.error.exception);
}
throw new TypeError(`Config file failed validation`);
}
const config = configResult.ok;
const explicitAccessTokenPath = getCommandLineOption(process.argv, "--access-token-path");
const explicitPantalaimonPasswordPath = getCommandLineOption(process.argv, "--pantalaimon-password-path");
if (explicitAccessTokenPath !== undefined) {