Generate default config automatically

This commit is contained in:
Will Hunt 2020-12-13 14:55:36 +00:00
parent fe0e3637ea
commit 7ba6f0f37c
11 changed files with 247 additions and 106 deletions

View File

@ -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

View File

@ -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:
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
sdasdsade

View File

@ -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",

View File

@ -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`);
}

View File

@ -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);

View File

@ -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();

View File

@ -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);

View File

@ -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 {
github?: BridgeConfigGitHub;
gitlab?: BridgeConfigGitLab;
webhook: {
port: number;
bindAddress: string;
};
bridge: {
interface BridgeConfigBridge {
domain: string;
url: string;
mediaUrl: string;
mediaUrl?: string;
port: number;
bindAddress: string;
store: string;
};
queue: {
}
interface BridgeConfigWebhook {
port: number;
bindAddress: string;
}
interface BridgeConfigQueue {
monolithic: boolean;
port?: number;
host?: string;
};
logging: {
}
interface BridgeConfigLogging {
level: string;
};
passFile: string;
bot?: {
}
interface BridgeConfigBot {
displayname?: string;
avatar?: string;
}
interface BridgeConfigRoot {
bridge: BridgeConfigBridge;
webhook: BridgeConfigWebhook;
queue: BridgeConfigQueue;
logging: BridgeConfigLogging;
passFile: string;
github?: BridgeConfigGitHub;
gitlab?: BridgeConfigGitLab;
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;
}

11
src/Config/Decorators.ts Normal file
View File

@ -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);
}

98
src/Config/Defaults.ts Normal file
View File

@ -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<string, unknown>, 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())

View File

@ -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();