diff --git a/README.md b/README.md index c1ce1e7afbf55a5c276424fd3423eeafd6b3d411..ddfead1dd2a9a620e0528006979249c8746526b8 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ should show up in the network list on Riot and other clients. * For the bot to appear online on Discord you need to run the bridge itself. * ``npm start`` +* Particular configuration keys can be overridden by defining corresponding environment variables. For instance, `auth.botToken` can be set with `APPSERVICE_DISCORD_AUTH_BOT_TOKEN`. [Howto](./docs/howto.md) diff --git a/src/config.ts b/src/config.ts index 166604317accbb842d349afe1ca26eb5c4500247..2f0eb76241cfca553350e116ea39b9b45e72d171 100644 --- a/src/config.ts +++ b/src/config.ts @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +const ENV_PREFIX = "APPSERVICE_DISCORD"; +const ENV_KEY_SEPARATOR = "_"; +const ENV_VAL_SEPARATOR = ","; + /** Type annotations for config/config.schema.yaml */ export class DiscordBridgeConfig { public bridge: DiscordBridgeConfigBridge = new DiscordBridgeConfigBridge(); @@ -27,18 +31,48 @@ export class DiscordBridgeConfig { /** * Apply a set of keys and values over the default config. - * @param _config Config keys + * @param newConfig Config keys * @param configLayer Private parameter */ // tslint:disable-next-line no-any - public ApplyConfig(newConfig: {[key: string]: any}, configLayer: any = this) { + public applyConfig(newConfig: {[key: string]: any}, configLayer: {[key: string]: any} = this) { Object.keys(newConfig).forEach((key) => { - if ( typeof(configLayer[key]) === "object" && - !Array.isArray(configLayer[key])) { - this.ApplyConfig(newConfig[key], this[key]); - return; + if (configLayer[key] instanceof Object && !(configLayer[key] instanceof Array)) { + this.applyConfig(newConfig[key], configLayer[key]); + } else { + configLayer[key] = newConfig[key]; + } + }); + } + + /** + * Override configuration keys defined in the supplied environment dictionary. + * @param environment environment variable dictionary + * @param path private parameter:Â config layer path determining the environment key prefix + * @param configLayer private parameter: current layer of configuration to alter recursively + */ + public applyEnvironmentOverrides( + // tslint:disable-next-line no-any + environment: {[key: string]: any}, + path: string[] = [ENV_PREFIX], + // tslint:disable-next-line no-any + configLayer: {[key: string]: any} = this, + ) { + Object.keys(configLayer).forEach((key) => { + // camelCase to THICK_SNAKE + const attributeKey = key.replace(/[A-Z]/g, (prefix) => `${ENV_KEY_SEPARATOR}${prefix}`).toUpperCase(); + const attributePath = path.concat([attributeKey]); + + if (configLayer[key] instanceof Object && !(configLayer[key] instanceof Array)) { + this.applyEnvironmentOverrides(environment, attributePath, configLayer[key]); + } else { + const lookupKey = attributePath.join(ENV_KEY_SEPARATOR); + if (lookupKey in environment) { + configLayer[key] = (configLayer[key] instanceof Array) + ? environment[lookupKey].split(ENV_VAL_SEPARATOR) + : environment[lookupKey]; + } } - configLayer[key] = newConfig[key]; }); } } diff --git a/src/discordas.ts b/src/discordas.ts index 0f4b5e11d6a46c238e1e259316d23b11e06c4da2..1f350c3819a8f6c70c924bbc1f289b7af6f92276 100644 --- a/src/discordas.ts +++ b/src/discordas.ts @@ -59,7 +59,8 @@ type callbackFn = (...args: any[]) => Promise<any>; async function run(port: number, fileConfig: DiscordBridgeConfig) { const config = new DiscordBridgeConfig(); - config.ApplyConfig(fileConfig); + config.applyConfig(fileConfig); + config.applyEnvironmentOverrides(process.env); Log.Configure(config.logging); log.info("Starting Discord AS"); const yamlConfig = yaml.safeLoad(fs.readFileSync(cli.opts.registrationPath, "utf8")); diff --git a/test/test_config.ts b/test/test_config.ts index 0979897b55ea157ff94e02386674097ab0516645..badb662a5c4a9929bfdfacb22c45faeef09a965d 100644 --- a/test/test_config.ts +++ b/test/test_config.ts @@ -22,10 +22,10 @@ import { DiscordBridgeConfig } from "../src/config"; const expect = Chai.expect; -describe("DiscordBridgeConfig.ApplyConfig", () => { +describe("DiscordBridgeConfig.applyConfig", () => { it("should merge configs correctly", () => { const config = new DiscordBridgeConfig(); - config.ApplyConfig({ + config.applyConfig({ bridge: { disableDeletionForwarding: true, disableDiscordMentions: false, @@ -38,17 +38,40 @@ describe("DiscordBridgeConfig.ApplyConfig", () => { console: "warn", }, }); - expect(config.bridge.homeserverUrl, "blah"); + expect(config.bridge.homeserverUrl).to.equal("blah"); expect(config.bridge.disableTypingNotifications).to.be.true; expect(config.bridge.disableDiscordMentions).to.be.false; expect(config.bridge.disableDeletionForwarding).to.be.true; expect(config.bridge.enableSelfServiceBridging).to.be.false; expect(config.bridge.disableJoinLeaveNotifications).to.be.true; - expect(config.logging.console, "warn"); + expect(config.logging.console).to.equal("warn"); + }); + it("should merge environment overrides correctly", () => { + const config = new DiscordBridgeConfig(); + config.applyConfig({ + bridge: { + disableDeletionForwarding: true, + disableDiscordMentions: false, + homeserverUrl: "blah", + }, + logging: { + console: "warn", + }, + }); + config.applyEnvironmentOverrides({ + APPSERVICE_DISCORD_BRIDGE_DISABLE_DELETION_FORWARDING: false, + APPSERVICE_DISCORD_BRIDGE_DISABLE_JOIN_LEAVE_NOTIFICATIONS: true, + APPSERVICE_DISCORD_LOGGING_CONSOLE: "debug", + }); + expect(config.bridge.disableJoinLeaveNotifications).to.be.true; + expect(config.bridge.disableDeletionForwarding).to.be.false; + expect(config.bridge.disableDiscordMentions).to.be.false; + expect(config.bridge.homeserverUrl).to.equal("blah"); + expect(config.logging.console).to.equal("debug"); }); it("should merge logging.files correctly", () => { const config = new DiscordBridgeConfig(); - config.ApplyConfig({ + config.applyConfig({ logging: { console: "silent", files: [ @@ -58,6 +81,6 @@ describe("DiscordBridgeConfig.ApplyConfig", () => { ], }, }); - expect(config.logging.files[0].file, "./bacon.log"); + expect(config.logging.files[0].file).to.equal("./bacon.log"); }); }); diff --git a/test/test_discordmessageprocessor.ts b/test/test_discordmessageprocessor.ts index f78d66abded7dacaa4451b546796459cf7776e64..d66dd949e2cbe76da2780a1853e5acd6775ee4b6 100644 --- a/test/test_discordmessageprocessor.ts +++ b/test/test_discordmessageprocessor.ts @@ -58,8 +58,8 @@ describe("DiscordMessageProcessor", () => { msg.embeds = []; msg.content = "Hello World!"; const result = await processor.FormatMessage(msg); - Chai.assert(result.body, "Hello World!"); - Chai.assert(result.formattedBody, "Hello World!"); + Chai.assert.equal(result.body, "Hello World!"); + Chai.assert.equal(result.formattedBody, "Hello World!"); }); it("processes markdown messages correctly.", async () => { const processor = new DiscordMessageProcessor( diff --git a/tools/chanfix.ts b/tools/chanfix.ts index 8d4f4abf9b8510b3ef666184ee640170836fda11..a3f5095231f489dc78aea5c87d4ed20911b9d2f2 100644 --- a/tools/chanfix.ts +++ b/tools/chanfix.ts @@ -67,7 +67,8 @@ if (options.help) { const yamlConfig = yaml.safeLoad(fs.readFileSync("./discord-registration.yaml", "utf8")); const registration = AppServiceRegistration.fromObject(yamlConfig); const config = new DiscordBridgeConfig(); -config.ApplyConfig(yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig); +config.applyConfig(yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig); +config.applyEnvironmentOverrides(process.env); if (registration === null) { throw new Error("Failed to parse registration file"); diff --git a/tools/ghostfix.ts b/tools/ghostfix.ts index 9ce61d6d6102b4bf61566a0379aaae92b085c884..b4e99e9dc95eaf4aab792f9ad39c4f99c2bc7588 100644 --- a/tools/ghostfix.ts +++ b/tools/ghostfix.ts @@ -76,7 +76,8 @@ if (options.help) { const yamlConfig = yaml.safeLoad(fs.readFileSync("./discord-registration.yaml", "utf8")); const registration = AppServiceRegistration.fromObject(yamlConfig); const config = new DiscordBridgeConfig(); -config.ApplyConfig(yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig); +config.applyConfig(yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig); +config.applyEnvironmentOverrides(process.env); if (registration === null) { throw new Error("Failed to parse registration file");