diff --git a/Cargo.toml b/Cargo.toml index 039cd4e..ba1c0e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1.8", features = ["v4"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_default = "0.1" +serde-inline-default = "0.2" mdhtml = { path = "mdhtml", features = ["markdown"] } jrd = "0.1" tracing = "0.1" @@ -44,6 +46,7 @@ sea-orm-migration = { version = "0.12", optional = true } mastodon-async-entities = { version = "1.1.0", optional = true } time = { version = "0.3", features = ["serde"], optional = true } async-recursion = "1.1" +toml = "0.8.12" [features] default = ["migrations", "cli"] diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..8be6af3 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,74 @@ + + +#[serde_inline_default::serde_inline_default] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, serde_default::DefaultFromSerde)] +pub struct Config { + #[serde(default)] + pub instance: InstanceConfig, + + #[serde(default)] + pub datasource: DatasourceConfig, + + // TODO should i move app keys here? +} + +#[serde_inline_default::serde_inline_default] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, serde_default::DefaultFromSerde)] +pub struct DatasourceConfig { + #[serde_inline_default("sqlite://./upub.db".into())] + pub connection_string: String, + + #[serde_inline_default(4)] + pub max_connections: u32, + + #[serde_inline_default(1)] + pub min_connections: u32, + + #[serde_inline_default(300u64)] + pub connect_timeout_seconds: u64, + + #[serde_inline_default(300u64)] + pub acquire_timeout_seconds: u64, + + #[serde_inline_default(1u64)] + pub slow_query_warn_seconds: u64, + + #[serde_inline_default(true)] + pub slow_query_warn_enable: bool, +} + +#[serde_inline_default::serde_inline_default] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, serde_default::DefaultFromSerde)] +pub struct InstanceConfig { + #[serde_inline_default("μpub".into())] + pub name: String, + + #[serde_inline_default("micro social network, federated".into())] + pub description: String, + + #[serde_inline_default("upub.social".into())] + pub domain: String, + + #[serde(default)] + pub contact: Option, + + #[serde(default)] + pub frontend: Option, +} + + + + +impl Config { + pub fn load(path: Option) -> Self { + let Some(cfg_path) = path else { return Config::default() }; + match std::fs::read_to_string(cfg_path) { + Ok(x) => match toml::from_str(&x) { + Ok(cfg) => return cfg, + Err(e) => tracing::error!("failed parsing config file: {e}"), + }, + Err(e) => tracing::error!("failed reading config file: {e}"), + } + Config::default() + } +} diff --git a/src/main.rs b/src/main.rs index aeaab1f..51d1065 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ mod model; mod routes; mod errors; - mod config; #[cfg(feature = "cli")] @@ -14,6 +13,8 @@ mod migrations; #[cfg(feature = "migrations")] use sea_orm_migration::MigratorTrait; +use std::path::PathBuf; +use config::Config; use clap::{Parser, Subcommand}; use sea_orm::{ConnectOptions, Database}; @@ -30,13 +31,17 @@ struct Args { /// command to run command: Mode, - #[arg(short = 'd', long = "db", default_value = "sqlite://./upub.db")] - /// database connection uri - database: String, + /// path to config file, leave empty to not use any + #[arg(short, long)] + config: Option, - #[arg(short = 'D', long, default_value = "http://localhost:3000")] - /// instance base domain, for AP ids - domain: String, + #[arg(long = "db")] + /// database connection uri, overrides config value + database: Option, + + #[arg(long)] + /// instance base domain, for AP ids, overrides config value + domain: Option, #[arg(long, default_value_t=false)] /// run with debug level tracing @@ -46,7 +51,10 @@ struct Args { #[derive(Clone, Subcommand)] enum Mode { /// run fediverse server - Serve , + Serve, + + /// print current or default configuration + Config, #[cfg(feature = "migrations")] /// apply database migrations @@ -71,11 +79,24 @@ async fn main() { .with_max_level(if args.debug { tracing::Level::DEBUG } else { tracing::Level::INFO }) .init(); + let config = Config::load(args.config); + + let database = args.database.unwrap_or(config.datasource.connection_string.clone()); + let domain = args.domain.unwrap_or(config.instance.domain.clone()); + // TODO can i do connectoptions.into() or .connect() and skip these ugly bindings? - let mut opts = ConnectOptions::new(&args.database); + let mut opts = ConnectOptions::new(&database); opts - .sqlx_logging_level(tracing::log::LevelFilter::Debug); + .sqlx_logging_level(tracing::log::LevelFilter::Debug) + .max_connections(config.datasource.max_connections) + .min_connections(config.datasource.min_connections) + .acquire_timeout(std::time::Duration::from_secs(config.datasource.acquire_timeout_seconds)) + .connect_timeout(std::time::Duration::from_secs(config.datasource.connect_timeout_seconds)) + .sqlx_slow_statements_logging_settings( + if config.datasource.slow_query_warn_enable { tracing::log::LevelFilter::Warn } else { tracing::log::LevelFilter::Off }, + std::time::Duration::from_secs(config.datasource.slow_query_warn_seconds) + ); let db = Database::connect(opts) .await.expect("error connecting to db"); @@ -88,11 +109,13 @@ async fn main() { #[cfg(feature = "cli")] Mode::Cli { command } => - cli::run(command, db, args.domain) + cli::run(command, db, domain, config) .await.expect("failed running cli task"), + Mode::Config => println!("{}", toml::to_string_pretty(&config).expect("failed serializing config")), + Mode::Serve => { - let ctx = server::Context::new(db, args.domain) + let ctx = server::Context::new(db, domain, config) .await.expect("failed creating server context"); use routes::activitypub::ActivityPubRouter; diff --git a/src/routes/activitypub/application.rs b/src/routes/activitypub/application.rs index 451885a..d524260 100644 --- a/src/routes/activitypub/application.rs +++ b/src/routes/activitypub/application.rs @@ -22,8 +22,8 @@ pub async fn view( serde_json::Value::new_object() .set_id(Some(&url!(ctx, ""))) .set_actor_type(Some(apb::ActorType::Application)) - .set_name(Some("μpub")) - .set_summary(Some("micro social network, federated")) + .set_name(Some(&ctx.cfg().instance.name)) + .set_summary(Some(&ctx.cfg().instance.description)) .set_inbox(apb::Node::link(url!(ctx, "/inbox"))) .set_outbox(apb::Node::link(url!(ctx, "/outbox"))) .set_published(Some(ctx.app().created)) diff --git a/src/routes/mastodon/instance.rs b/src/routes/mastodon/instance.rs index b1b8e23..86be3c9 100644 --- a/src/routes/mastodon/instance.rs +++ b/src/routes/mastodon/instance.rs @@ -8,9 +8,9 @@ pub async fn get( ) -> crate::Result> { Ok(Json(mastodon_async_entities::instance::Instance { uri: ctx.domain().to_string(), - title: "μpub".to_string(), - description: "micro social network, federated".to_string(), - email: "me@alemi.dev".to_string(), + title: ctx.cfg().instance.name.clone(), + description: ctx.cfg().instance.description.clone(), + email: ctx.cfg().instance.contact.as_deref().unwrap_or_default().to_string(), version: crate::VERSION.to_string(), urls: None, stats: None, diff --git a/src/server/context.rs b/src/server/context.rs index a5c281d..a9c2edf 100644 --- a/src/server/context.rs +++ b/src/server/context.rs @@ -3,7 +3,7 @@ use std::{collections::BTreeSet, sync::Arc}; use openssl::rsa::Rsa; use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, SelectColumns, Set}; -use crate::{model, server::fetcher::Fetcher}; +use crate::{config::Config, model, server::fetcher::Fetcher}; use super::dispatcher::Dispatcher; @@ -12,6 +12,7 @@ use super::dispatcher::Dispatcher; pub struct Context(Arc); struct ContextInner { db: DatabaseConnection, + config: Config, domain: String, protocol: String, dispatcher: Dispatcher, @@ -30,7 +31,7 @@ macro_rules! url { impl Context { // TODO slim constructor down, maybe make a builder? - pub async fn new(db: DatabaseConnection, mut domain: String) -> crate::Result { + pub async fn new(db: DatabaseConnection, mut domain: String, config: Config) -> crate::Result { let protocol = if domain.starts_with("http://") { "http://" } else { "https://" }.to_string(); if domain.ends_with('/') { @@ -71,7 +72,7 @@ impl Context { .await?; Ok(Context(Arc::new(ContextInner { - db, domain, protocol, app, dispatcher, + db, domain, protocol, app, dispatcher, config, relays: BTreeSet::from_iter(relays.into_iter()), }))) } @@ -84,6 +85,10 @@ impl Context { &self.0.db } + pub fn cfg(&self) -> &Config { + &self.0.config + } + pub fn domain(&self) -> &str { &self.0.domain }