social bookmarking for atproto

[backend] Rewrite the config parsing system

hexmani.ac 6f60f052 dc8f2047

verified
Changed files
+67 -58
.idea
dictionaries
backend
+2
.idea/dictionaries/project.xml
··· 6 6 <w>appview</w> 7 7 <w>atcute</w> 8 8 <w>atproto</w> 9 + <w>atprotocol</w> 9 10 <w>bluesky</w> 10 11 <w>bsky</w> 11 12 <w>clippr</w> ··· 17 18 <w>llms</w> 18 19 <w>llmstxt</w> 19 20 <w>multiformats</w> 21 + <w>nodeinfo</w> 20 22 <w>nsid</w> 21 23 <w>outdir</w> 22 24 <w>rkey</w>
+7 -5
backend/config.example.toml
··· 1 1 ## This is a configuration file for Clippr. 2 2 ## Please copy to "config.example.toml" before starting the server, 3 3 ## otherwise it will not start. Modify as necessary. 4 - ## All keys that are listed are expected to be included in the config, even if not explicity declared as required. 5 4 6 5 ## Where the server is broadcasted to. 7 6 hostname = "localhost" 8 7 port = 9090 9 - webDomain = "https://localhost" 8 + 9 + ## Where the server will be deployed to. 10 + baseUrl = "https://localhost:9090" 10 11 11 12 ## For most deployments, you will want to keep the log level at "info". 12 13 ## If you are a developer, move it down to "debug" for more information. ··· 22 23 logLevel = "info" 23 24 24 25 ## How the SQLite database is stored. 25 - ## For experimenting, you can store the database in memory with ":memory:" 26 + ## NOTE: Storing the database in-memory does not work as the schema is not properly loaded. Fixme! 26 27 [database] 27 28 ## Paths can be used here. 28 29 name = "file:clippr.db" ··· 30 31 ## How the server interacts with the ATproto network. 31 32 [network] 32 33 ## What Jetstream instance to use for receiving content from the network. 34 + ## Non-Bluesky relay: relay2.fire.hose.cam 33 35 firehose = "jetstream1.us-east.bsky.network" 34 36 ## What DID to use for service proxying. This should be the domain that the API is accessible from. 35 37 ## Default: "did:web:localhost%3A9090" 36 38 serviceDid = "did:web:localhost%3A9090" 37 - ## A multibase public key to use for signing in the service proxy DID, formatted as "did:key:[key]". Required. 38 - ## Do not use the default key outside of testing. 39 + ## A multibase public key to use for DID service proxying, formatted as "did:key:[key]". 40 + ## Do not use the default key outside of development. 39 41 didSigningKey = "did:key:zDnaeuuRRQuYp4S76LwosLhHbpU1HJcg6S5oJAUHmdZLVdLM5"
+2 -5
backend/drizzle.config.ts
··· 8 8 // @ts-expect-error Read from the TypeScript file instead of assuming that it's JavaScript 9 9 import { Config } from "./src/config.ts"; 10 10 11 - const config = Config.getInstance(); 11 + const config = Config.getInstance().getConfig(); 12 12 let dbname; 13 13 14 - dbname = config.get("database.name"); 15 - if (typeof dbname !== "string") { 16 - dbname = "file:clippr.db"; // Only way to disable linter error 17 - } 14 + dbname = config.database.name; 18 15 19 16 export default defineConfig({ 20 17 out: "./drizzle",
+35 -14
backend/src/config.ts
··· 7 7 import { readFileSync } from "fs"; 8 8 import * as toml from "toml"; 9 9 10 + interface ConfigSchema { 11 + hostname: string | "localhost"; 12 + port: number | 9090; 13 + baseUrl: string | "http://localhost:9090"; 14 + logLevel: string | "debug"; 15 + database: { 16 + name: string | "file:clippr.db"; 17 + }; 18 + network: { 19 + firehose: string | "jetstream1.us-east.bsky.network"; 20 + serviceDid: string | "did:web:localhost%3A9090"; 21 + didSigningKey: string | "did:key:zDnaeuuRRQuYp4S76LwosLhHbpU1HJcg6S5oJAUHmdZLVdLM5"; 22 + }; 23 + } 24 + 25 + class ConfigError extends Error { 26 + constructor(message: string) { 27 + super(message); 28 + this.name = "ConfigError"; 29 + } 30 + } 31 + 10 32 export class Config { 11 33 private static instance: Config; 12 - private readonly configData; 34 + private readonly configData: ConfigSchema; 13 35 14 36 private constructor() { 15 37 let tomlString; 16 38 try { 17 39 tomlString = readFileSync("config.toml", "utf-8"); 18 40 } catch { 19 - throw new Error("Config file not found"); 41 + throw new ConfigError("Config file not found"); 20 42 } 21 - this.configData = toml.parse(tomlString); 43 + 44 + try { 45 + this.configData = toml.parse(tomlString); 46 + } catch { 47 + throw new ConfigError(`Config file is not valid TOML`); 48 + } 22 49 } 23 50 51 + /** 52 + * Gets a singleton instance of the program configuration 53 + * @throws {ConfigError} if the config file cannot be read or parsed 54 + */ 24 55 static getInstance(): Config { 25 56 if (!Config.instance) { 26 57 try { ··· 33 64 return Config.instance; 34 65 } 35 66 36 - get<T>(path: string): T | undefined { 37 - const keys = path.split("."); 38 - let value = this.configData; 39 - for (const key of keys) { 40 - if (value == null) return undefined; 41 - value = value[key]; 42 - } 43 - return value; 44 - } 45 - 46 - getAll() { 67 + getConfig(): ConfigSchema { 47 68 return this.configData; 48 69 } 49 70 }
+2 -2
backend/src/db/database.ts
··· 8 8 import { Config } from "../config.js"; 9 9 import Logger from "../logger.js"; 10 10 11 - const config = Config.getInstance(); 12 - const dbname = config.get("database.name"); 11 + const config = Config.getInstance().getConfig(); 12 + const dbname = config.database.name; 13 13 14 14 export class Database { 15 15 private static instance: Database;
+3 -2
backend/src/logger.ts
··· 5 5 */ 6 6 7 7 import { createLogger, format, transports } from "winston"; 8 + import { Config } from "./config.js"; 8 9 9 - // TODO: I can't seem to actually get the config setting for the log level yet. 10 - const logLevel = "debug"; 10 + const config = Config.getInstance().getConfig(); 11 + const logLevel = config.logLevel; 11 12 12 13 const Logger = createLogger({ 13 14 level: logLevel,
+4 -4
backend/src/main.ts
··· 20 20 logger.info("Clippr-BE starting..."); 21 21 22 22 logger.verbose("Reading configuration..."); 23 - const config = Config.getInstance(); 23 + const config = Config.getInstance().getConfig(); 24 24 25 25 logger.verbose("Initializing database..."); 26 26 Database.getInstance(); ··· 30 30 readFromFirehose(); 31 31 32 32 const server: ServerType = serve({ 33 - port: config.get("port"), 34 - hostname: config.get("hostname"), 33 + port: config.port, 34 + hostname: config.hostname, 35 35 fetch: app.fetch, 36 36 }); 37 37 38 38 logger.info( 39 - `XRPC server launched at http://${config.get("hostname")}:${config.get("port")}`, 39 + `XRPC server launched at http://${config.hostname}:${config.port}`, 40 40 ); 41 41 42 42 process.removeAllListeners("SIGINT");
+2 -2
backend/src/network/jetstream.ts
··· 9 9 import { handleClip, handleProfile, handleTag } from "./commit.js"; 10 10 import Logger from "../logger.js"; 11 11 12 - const config = Config.getInstance(); 13 - const hostname = config.get("network.firehose"); 12 + const config = Config.getInstance().getConfig(); 13 + const hostname = config.network.firehose; 14 14 15 15 const jetstream = new Jetstream({ 16 16 endpoint: `wss://${hostname}/subscribe`,
+10 -24
backend/src/routes/well-known.ts
··· 9 9 import { getStats } from "../api/stats.js"; 10 10 11 11 const app = new Hono(); 12 - const config = Config.getInstance(); 12 + const config = Config.getInstance().getConfig(); 13 13 14 - const serviceDid: string = 15 - config.get("network.serviceDid") || 16 - `did:web:${config.get("hostname")}%3A${config.get("port")}/`; 17 - const signingKey: string | unknown = config.get("network.didSigningKey"); 18 - let webDomain: string = 19 - config.get("network.webDomain") || 20 - `http://${config.get("hostname")}:${config.get("port")}`; 14 + const serviceDid: string = config.network.serviceDid; 15 + const signingKey: string = config.network.didSigningKey; 16 + let baseUrl: string = config.baseUrl 21 17 22 - if (!webDomain.startsWith("http://") || !webDomain.startsWith("https://")) { 23 - webDomain = `http://${webDomain.replace(/^https?:\/\//, "")}`; 18 + if (!baseUrl.startsWith("http://") || !baseUrl.startsWith("https://")) { 19 + baseUrl = `http://${baseUrl.replace(/^https?:\/\//, "")}`; 24 20 } 25 21 26 22 app.get("/.well-known/nodeinfo", (c) => { ··· 28 24 links: [ 29 25 { 30 26 rel: "https://nodeinfo.diaspora.software/ns/schema/2.2", 31 - href: `${webDomain}/nodeinfo/2.2`, 27 + href: `${baseUrl}/nodeinfo/2.2`, 32 28 }, 33 29 { 34 30 rel: "https://nodeinfo.diaspora.software/ns/schema/2.1", 35 - href: `${webDomain}/nodeinfo/2.1`, 31 + href: `${baseUrl}/nodeinfo/2.1`, 36 32 }, 37 33 { 38 34 rel: "https://nodeinfo.diaspora.software/ns/schema/2.0", 39 - href: `${webDomain}/nodeinfo/2.0`, 35 + href: `${baseUrl}/nodeinfo/2.0`, 40 36 }, 41 37 ], 42 38 }); ··· 152 148 ); 153 149 } 154 150 155 - if (typeof signingKey !== "string") { 156 - return c.json( 157 - { 158 - error: "Internal Server Error", 159 - message: "Server is not properly configured", 160 - }, 161 - 500, 162 - ); 163 - } 164 - 165 151 if (!signingKey.replace("did:key:", "").startsWith("z")) { 166 152 console.log(signingKey); 167 153 return c.json( ··· 191 177 { 192 178 id: "#clippr_appview", 193 179 type: "ClipprAppView", 194 - serviceEndpoint: `${webDomain}`, 180 + serviceEndpoint: `${baseUrl}`, 195 181 }, 196 182 ], 197 183 });