From 7ba6f0f37c9c9b5707f016cce67304eb1b305f4e Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Sun, 13 Dec 2020 14:55:36 +0000 Subject: [PATCH] Generate default config automatically --- .github/workflows/main.yml | 15 ++++- config.sample.yml | 74 ++++++++------------ package.json | 5 +- src/AdminRoom.ts | 6 ++ src/App/BridgeApp.ts | 4 +- src/App/GithubWebhookApp.ts | 4 +- src/App/MatrixSenderApp.ts | 4 +- src/Config.ts | 130 ++++++++++++++++++++++-------------- src/Config/Decorators.ts | 11 +++ src/Config/Defaults.ts | 98 +++++++++++++++++++++++++++ src/GithubBridge.ts | 2 +- 11 files changed, 247 insertions(+), 106 deletions(-) create mode 100644 src/Config/Decorators.ts create mode 100644 src/Config/Defaults.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index de2155d6..a71bd801 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,5 +31,18 @@ jobs: with: node-version: ${{ matrix.node_version }} - run: yarn - - run: yarn build - run: yarn test + config: + runs-on: ubuntu-latest + strategy: + matrix: + node_version: ['14', '12'] + steps: + - uses: actions/checkout@v2 + - name: Use Node.js + uses: actions/setup-node@v1 + with: + node-version: 12 + - run: yarn + - run: node lib/Config/Defaults.js > expected-config.sample.yml + - run: cmp --silent config.sample.yml expected-config.sample.yml diff --git a/config.sample.yml b/config.sample.yml index f15bec89..ddc1449c 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -1,55 +1,39 @@ +# This is an example configuration file + +bridge: + # Basic homeserver configuration + # + domain: example.com + url: http://localhost:8008 + mediaUrl: http://example.com + port: 9993 + bindAddress: 127.0.0.1 github: - # You can find this by going to https://github.com/settings/installations, - # clicking on the bridge app and taking note of the ID in the URL. - # E.g. https://github.com/settings/installations/6854059 installationId: 6854059 - auth: + auth: id: 123 - privateKeyFile: "github-key.pem" + privateKeyFile: github-key.pem oauth: client_id: foo client_secret: bar - redirect_uri: baz + redirect_uri: https://example.com/bridge_oauth/ webhook: - secret: webhooksecret - port: 7775 - bindAddress: 0.0.0.0 - userTokens: - # If you want to hardcode a users token in the config - - "@hardcoded:localhost": "foobarbaz" -bridge: - # Homeserver servername - domain: example.com - # Homeserver (internal) url - url: http://localhost:8008 - # This is your public facing url where media is served from - mediaUrl: http://example.com/ - port: 9993 # Port the appservice is binding to - bindAddress: 127.0.0.1 # Address to bind to -queue: - monolithic: true # If this is false, use a Redis server as a queue. - # Redis settings - port: 6379 # Port for the redis server, omit for 6379 - host: localhost # Port for the redis server, omit for localhost - # Note that setting the above will make the bridge use redis as a storage location -logging: - level: "info" # One of "debug", "info", "warn", "error" - -# This is used to securely store passwords. -# Run openssl genpkey -out passkey.pem -outform PEM -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -passFile: "passkey.pem" - + secret: secrettoken +gitlab: + instances: + gitlab.com: + url: https://gitlab.com + webhook: + secret: secrettoken webhook: port: 9000 + bindAddress: 0.0.0.0 +passFile: passkey.pem +queue: + monolithic: true + port: 6379 + host: localhost +logging: + level: info -widgets: - # The port to listen on for the widget API - port: 5000 - # Public url that the widget API is reachable on - publicUrl: "https://example.com/bridgewidgets/" - # Add the widget to admin rooms - addToAdminRooms: true - -bot: - displayname: "GitHub Bot" - avatar: mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d \ No newline at end of file +sdasdsade \ No newline at end of file diff --git a/package.json b/package.json index 0ac46491..eab1c427 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "start:webhooks": "node --require source-map-support/register lib/App/GithubWebhookApp.js", "start:matrixsender": "node --require source-map-support/register lib/App/MatrixSenderApp.js", "test": "mocha -r ts-node/register tests/*.ts", - "lint": "eslint -c .eslintrc.js src/**/*.ts" + "lint": "eslint -c .eslintrc.js src/**/*.ts", + "generate-default-config": "node lib/Config/Defaults.js > config.sample.yml" }, "dependencies": { "@octokit/auth-app": "2.10.2", @@ -42,7 +43,7 @@ "string-argv": "v0.3.1", "uuid": "^8.3.1", "winston": "^3.3.3", - "yaml": "^1.10.0" + "yaml": "^2.0.0-1" }, "devDependencies": { "@prefresh/snowpack": "^2.2.0", diff --git a/src/AdminRoom.ts b/src/AdminRoom.ts index 42f589fa..cc8e4eca 100644 --- a/src/AdminRoom.ts +++ b/src/AdminRoom.ts @@ -288,6 +288,9 @@ export class AdminRoom extends EventEmitter { org, })).data; } catch (ex) { + if (ex.status === 404) { + return this.sendNotice('Not found'); + } log.warn(`Failed to fetch projects:`, ex); return this.sendNotice(`Failed to fetch projects due to an error. See logs for details`); } @@ -318,6 +321,9 @@ export class AdminRoom extends EventEmitter { }); this.emit('open.project', project.data); } catch (ex) { + if (ex.status === 404) { + return this.sendNotice('Not found'); + } log.warn(`Failed to fetch project:`, ex); return this.sendNotice(`Failed to fetch project due to an error. See logs for details`); } diff --git a/src/App/BridgeApp.ts b/src/App/BridgeApp.ts index f5e6c4d2..c2250d62 100644 --- a/src/App/BridgeApp.ts +++ b/src/App/BridgeApp.ts @@ -1,7 +1,7 @@ import { GithubBridge } from "../GithubBridge"; import LogWrapper from "../LogWrapper"; -import { parseConfig, parseRegistrationFile } from "../Config"; +import { BridgeConfig, parseRegistrationFile } from "../Config"; import { GithubWebhooks } from "../GithubWebhooks"; import { MatrixSender } from "../MatrixSender"; @@ -10,7 +10,7 @@ const log = new LogWrapper("App"); async function start() { const configFile = process.argv[2] || "./config.yml"; const registrationFile = process.argv[3] || "./registration.yml"; - const config = await parseConfig(configFile, process.env); + const config = await BridgeConfig.parseConfig(configFile, process.env); const registration = await parseRegistrationFile(registrationFile); LogWrapper.configureLogging(config.logging.level); diff --git a/src/App/GithubWebhookApp.ts b/src/App/GithubWebhookApp.ts index 3857d2fa..bcd5c15e 100644 --- a/src/App/GithubWebhookApp.ts +++ b/src/App/GithubWebhookApp.ts @@ -1,4 +1,4 @@ -import { parseConfig } from "../Config"; +import { BridgeConfig } from "../Config"; import { GithubWebhooks } from "../GithubWebhooks"; import LogWrapper from "../LogWrapper"; @@ -7,7 +7,7 @@ const log = new LogWrapper("App"); async function start() { const configFile = process.argv[2] || "./config.yml"; - const config = await parseConfig(configFile, process.env); + const config = await BridgeConfig.parseConfig(configFile, process.env); LogWrapper.configureLogging(config.logging.level); const webhookHandler = new GithubWebhooks(config); webhookHandler.listen(); diff --git a/src/App/MatrixSenderApp.ts b/src/App/MatrixSenderApp.ts index 2c177368..d3e996f5 100644 --- a/src/App/MatrixSenderApp.ts +++ b/src/App/MatrixSenderApp.ts @@ -1,4 +1,4 @@ -import { parseConfig, parseRegistrationFile } from "../Config"; +import { BridgeConfig, parseRegistrationFile } from "../Config"; import { MatrixSender } from "../MatrixSender"; import LogWrapper from "../LogWrapper"; @@ -8,7 +8,7 @@ const log = new LogWrapper("App"); async function start() { const configFile = process.argv[2] || "./config.yml"; const registrationFile = process.argv[3] || "./registration.yml"; - const config = await parseConfig(configFile, process.env); + const config = await BridgeConfig.parseConfig(configFile, process.env); const registration = await parseRegistrationFile(registrationFile); LogWrapper.configureLogging(config.logging.level); const sender = new MatrixSender(config, registration); diff --git a/src/Config.ts b/src/Config.ts index 338b4488..a4124742 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -1,6 +1,8 @@ import YAML from "yaml"; import { promises as fs } from "fs"; import { IAppserviceRegistration } from "matrix-bot-sdk"; +import * as assert from "assert"; +import { configKey } from "./Config/Decorators"; export interface BridgeConfigGitHub { auth: { @@ -31,10 +33,6 @@ export interface GitLabInstance { } interface BridgeConfigGitLab { - auth: { - id: number|string; - privateKeyFile: string; - }; webhook: { secret: string; }, @@ -47,58 +45,88 @@ interface BridgeWidgetConfig { publicUrl: string; } -export interface BridgeConfig { +interface BridgeConfigBridge { + domain: string; + url: string; + mediaUrl?: string; + port: number; + bindAddress: string; +} + +interface BridgeConfigWebhook { + port: number; + bindAddress: string; +} + +interface BridgeConfigQueue { + monolithic: boolean; + port?: number; + host?: string; +} + +interface BridgeConfigLogging { + level: string; +} + +interface BridgeConfigBot { + displayname?: string; + avatar?: string; +} + +interface BridgeConfigRoot { + bridge: BridgeConfigBridge; + webhook: BridgeConfigWebhook; + queue: BridgeConfigQueue; + logging: BridgeConfigLogging; + passFile: string; github?: BridgeConfigGitHub; gitlab?: BridgeConfigGitLab; - webhook: { - port: number; - bindAddress: string; - }; - bridge: { - domain: string; - url: string; - mediaUrl: string; - port: number; - bindAddress: string; - store: string; - }; - queue: { - monolithic: boolean; - port?: number; - host?: string; - }; - logging: { - level: string; - }; - passFile: string; - bot?: { - displayname?: string; - avatar?: string; - } + bot?: BridgeConfigBot; widgets?: BridgeWidgetConfig; } +export class BridgeConfig { + @configKey("Basic homeserver configuration") + public readonly bridge: BridgeConfigBridge; + public readonly webhook: BridgeConfigWebhook; + public readonly queue: BridgeConfigQueue; + public readonly logging: BridgeConfigLogging; + public readonly passFile: string; + public readonly github?: BridgeConfigGitHub; + public readonly gitlab?: BridgeConfigGitLab; + public readonly bot?: BridgeConfigBot; + public readonly widgets?: BridgeWidgetConfig; + + constructor(configData: BridgeConfigRoot, env: {[key: string]: string|undefined}) { + this.bridge = configData.bridge; + assert.ok(this.bridge); + this.github = configData.github; + this.gitlab = configData.gitlab; + this.webhook = configData.webhook; + this.passFile = configData.passFile; + assert.ok(this.webhook); + this.queue = configData.queue || { + monolithic: true, + }; + this.logging = configData.logging || { + level: "info", + } + // TODO: Formalize env support + if (env.CFG_QUEUE_MONOLITHIC && ["false", "off", "no"].includes(env.CFG_QUEUE_MONOLITHIC)) { + this.queue.monolithic = false; + this.queue.host = env.CFG_QUEUE_HOST; + this.queue.port = env.CFG_QUEUE_POST ? parseInt(env.CFG_QUEUE_POST, 10) : undefined; + } + + } + + static async parseConfig(filename: string, env: {[key: string]: string|undefined}) { + const file = await fs.readFile(filename, "utf-8"); + return new BridgeConfig(YAML.parse(file), env); + } +} + export async function parseRegistrationFile(filename: string) { const file = await fs.readFile(filename, "utf-8"); return YAML.parse(file) as IAppserviceRegistration; -} - -export async function parseConfig(filename: string, env: {[key: string]: string|undefined}) { - const file = await fs.readFile(filename, "utf-8"); - const config = YAML.parse(file) as BridgeConfig; - config.queue = config.queue || { - monolithic: true, - }; - if (!config.logging || !config.logging.level) { - config.logging = { - level: "info", - }; - } - config.bridge.mediaUrl = config.bridge.mediaUrl || config.bridge.url; - if (env.CFG_QUEUE_MONOLITHIC && ["false", "off", "no"].includes(env.CFG_QUEUE_MONOLITHIC)) { - config.queue.monolithic = false; - config.queue.host = env.CFG_QUEUE_HOST; - config.queue.port = env.CFG_QUEUE_POST ? parseInt(env.CFG_QUEUE_POST, 10) : undefined; - } - return config; -} +} \ No newline at end of file diff --git a/src/Config/Decorators.ts b/src/Config/Decorators.ts new file mode 100644 index 00000000..9dad102e --- /dev/null +++ b/src/Config/Decorators.ts @@ -0,0 +1,11 @@ +import "reflect-metadata"; + +const configKeyMetadataKey = Symbol("configKey"); + +export function configKey(comment?: string, optional = false) { + return Reflect.metadata(configKeyMetadataKey, [comment, optional]); +} + +export function getConfigKeyMetadata(target: any, propertyKey: string): [string, boolean] { + return Reflect.getMetadata(configKeyMetadataKey, target, propertyKey); +} \ No newline at end of file diff --git a/src/Config/Defaults.ts b/src/Config/Defaults.ts new file mode 100644 index 00000000..421370fe --- /dev/null +++ b/src/Config/Defaults.ts @@ -0,0 +1,98 @@ +import { BridgeConfig } from "../Config"; +import YAML from "yaml"; +import { getConfigKeyMetadata } from "./Decorators"; +import { Node, YAMLSeq } from "yaml/types"; + +const DefaultConfig = new BridgeConfig({ + bridge: { + domain: "example.com", + url: "http://localhost:8008", + mediaUrl: "http://example.com", + port: 9993, + bindAddress: "127.0.0.1", + }, + queue: { + monolithic: true, + port: 6379, + host: "localhost", + }, + logging: { + level: "info", + }, + passFile: "passkey.pem", + webhook: { + port: 9000, + bindAddress: "0.0.0.0" + }, + widgets: { + port: 5000, + publicUrl: "https://example.com/bridge_widget/", + addToAdminRooms: true, + }, + bot: { + displayname: "GitHub Bot", + avatar: "mxc://half-shot.uk/2876e89ccade4cb615e210c458e2a7a6883fe17d" + }, + github: { + installationId: 6854059, + auth: { + id: 123, + privateKeyFile: "github-key.pem", + }, + oauth: { + client_id: "foo", + client_secret: "bar", + redirect_uri: "https://example.com/bridge_oauth/", + }, + webhook: { + secret: "secrettoken", + }, + }, + gitlab: { + instances: { + "gitlab.com": { + url: "https://gitlab.com", + } + }, + webhook: { + secret: "secrettoken", + } + } +}, {}); + +function renderSection(doc: YAML.Document, obj: Record, parentNode?: YAMLSeq) { + const entries = Object.entries(obj); + entries.forEach(([key, value], i) => { + let newNode: Node; + if (typeof value === "object") { + newNode = doc.createNode({}); + renderSection(doc, value as any, newNode as YAMLSeq); + } else { + newNode = doc.createNode(value); + } + + const metadata = getConfigKeyMetadata(obj, key); + if (metadata) { + newNode.commentBefore = `${metadata[1] ? '(Optional)' : ''} ${metadata[0]}\n`; + } + + if (parentNode) { + parentNode.add({key, value: newNode}); + } else { + doc.add({key, value: newNode}); + } + }) + +} + +function renderDefaultConfig() { + const doc = new YAML.Document({}); + doc.commentBefore = ' This is an example configuration file'; + // Needed because the entries syntax below would not work otherwise + //const typeLessDefaultConfig = DefaultConfig as any; + renderSection(doc, DefaultConfig as any); + return doc.toString(); +} + +// Can be called directly +console.log(renderDefaultConfig()) \ No newline at end of file diff --git a/src/GithubBridge.ts b/src/GithubBridge.ts index 261f5385..6915c71c 100644 --- a/src/GithubBridge.ts +++ b/src/GithubBridge.ts @@ -172,7 +172,7 @@ export class GithubBridge { this.widgetApi = new BridgeWidgetApi(this.adminRooms); - this.commentProcessor = new CommentProcessor(this.as, this.config.bridge.mediaUrl); + this.commentProcessor = new CommentProcessor(this.as, this.config.bridge.mediaUrl || this.config.bridge.url); this.tokenStore = new UserTokenStore(this.config.passFile || "./passkey.pem", this.as.botIntent); await this.tokenStore.load();