1010
1111import * as fs from "fs" ;
1212import { load } from "js-yaml" ;
13- import { MatrixClient , LogService } from "matrix-bot-sdk" ;
13+ import { LogService , RichConsoleLogger } from "matrix-bot-sdk" ;
1414import Config from "config" ;
1515import path from "path" ;
1616import { 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
142177const 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
225260export 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 */
232313function 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+ }
0 commit comments