diff --git a/Cargo.lock b/Cargo.lock index 79e3896..00337a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -429,6 +429,7 @@ dependencies = [ "thiserror", "tokio", "toml", + "url", "validator", "yaml-rust", ] diff --git a/Cargo.toml b/Cargo.toml index 70b82c9..d0de845 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ serde = { version = "1.0", features = ["derive"] } sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] } tokio = { version = "1.38", features = ["full"] } toml = "0.8" +url = "2.5" validator = { version = "0.18", features = ["derive"] } yaml-rust = "0.4" diff --git a/src/command/config/migrate/v20240701.rs b/src/command/config/migrate/v20240701.rs index 40e5a12..53f77d7 100644 --- a/src/command/config/migrate/v20240701.rs +++ b/src/command/config/migrate/v20240701.rs @@ -2,8 +2,10 @@ #![allow(clippy::type_complexity)] +use crate::config::{client, server}; use sqlx::{postgres::PgConnectOptions, ConnectOptions}; use std::{collections::HashMap, env, fs, io::Read}; +use url::Url; use yaml_rust::{Yaml, YamlLoader}; #[derive(thiserror::Error, Debug)] @@ -12,6 +14,10 @@ pub(crate) enum Error { ReadYaml(#[from] ReadYamlConfigError), #[error("failed to read the meta table")] ReadMeta(#[from] sqlx::Error), + #[error("invalid config ({0})")] + InvalidConfig(&'static str), + #[error("failed to parse server URL")] + InvalidUrl(#[from] url::ParseError), } #[derive(thiserror::Error, Debug)] @@ -20,7 +26,7 @@ pub(crate) enum ReadYamlConfigError { ReadFile(#[from] std::io::Error), #[error(transparent)] Yaml(#[from] yaml_rust::ScanError), - #[error("invalid config file ({0})")] + #[error("invalid config ({0})")] InvalidConfig(String), } @@ -56,7 +62,7 @@ fn read_default_yml() -> Result, ReadYamlConfigError> { for (key, val) in content { let Some(key) = key.as_str() else { return Err(ReadYamlConfigError::InvalidConfig(format!( - "non-string key found: {:?}", + "non-string key: {:?}", key ))); }; @@ -66,7 +72,7 @@ fn read_default_yml() -> Result, ReadYamlConfigError> { Ok(res) } -#[derive(sqlx::FromRow, Debug)] +#[derive(sqlx::FromRow)] #[sqlx(rename_all = "camelCase")] struct Meta { name: Option, @@ -172,23 +178,199 @@ async fn read_meta_table( Ok(meta) } -pub(super) async fn run() -> Result<(), Error> { - let old_config = read_default_yml()?; - - for (k, v) in &old_config { - println!("{}:\n {:?}", k, v); - } - +async fn read_old_config() -> Result<(HashMap, Meta), Error> { + let default_yml = read_default_yml()?; + let db = default_yml + .get("db") + .ok_or(Error::InvalidConfig("`db` is missing"))?; let meta = read_meta_table( - old_config["db"]["host"].as_str().unwrap(), - old_config["db"]["port"].as_i64().unwrap() as u16, - old_config["db"]["user"].as_str().unwrap(), - old_config["db"]["pass"].as_str().unwrap(), - old_config["db"]["db"].as_str().unwrap(), + db["host"].as_str().unwrap(), + db["port"].as_i64().unwrap() as u16, + db["user"].as_str().unwrap(), + db["pass"].as_str().unwrap(), + db["db"].as_str().unwrap(), ) .await?; - println!("Meta: {:#?}", meta); + Ok((default_yml, meta)) +} + +async fn create_new_config( + default_yml: HashMap, + meta: Meta, +) -> Result<(server::Config, client::Config), Error> { + let db = default_yml + .get("db") + .map(|db| db.as_hash()) + .flatten() + .ok_or(Error::InvalidConfig("`db` is missing"))?; + + let redis = default_yml + .get("redis") + .map(|redis| redis.as_hash()) + .flatten() + .ok_or(Error::InvalidConfig("`redis` is missing"))?; + + let server_url = default_yml + .get("url") + .map(|url| url.as_str()) + .flatten() + .ok_or(Error::InvalidConfig("`url` is missing"))?; + + let parsed_server_url = Url::parse(server_url)?; + let protocol = parsed_server_url.scheme().to_owned(); + + if protocol != "https" && protocol != "http" { + return Err(Error::InvalidConfig( + "server URL must start with http:// or https://", + )); + } + + let hostname = parsed_server_url.host_str().ok_or(Error::InvalidConfig( + "hostname is missing in the server url", + ))?; + let host = match parsed_server_url.port() { + Some(port) => format!("{}:{}", hostname, port), + None => hostname.to_owned(), + }; + + let repository_url = match meta.repository_url.as_ref() { + "https://codeberg.org/firefish/firefish" + | "https://git.joinfirefish.org/firefish/firefish" => { + "https://firefish.dev/firefish/firefish".to_owned() + } + url => url.to_owned(), + }; + + let mut server_config = server::Config { + info: Some(server::Info { + name: meta.name, + description: meta.description, + maintainer_name: meta.maintainer_name, + contact_info: meta.maintainer_email, + open_registrations: !meta.disable_registration, + repository_url: Some(repository_url), + }), + timelines: Some(server::Timelines { + local: !meta.disable_local_timeline, + global: !meta.disable_global_timeline, + recommended: !meta.disable_recommended_timeline, + guest: meta.enable_guest_timeline, + }), + network: server::Network { + protocol: match protocol.as_str() { + "http" => Some(server::HttpProtocol::Http), + _ => Some(server::HttpProtocol::Https), + }, + host, + port: default_yml + .get("port") + .map(|v| v.as_i64()) + .flatten() + .ok_or(Error::InvalidConfig("port"))? as u16, + }, + database: server::Database { + host: db + .get(&Yaml::String("host".to_string())) + .map(|v| v.as_str()) + .flatten() + .ok_or(Error::InvalidConfig("db.host"))? + .to_string(), + port: db + .get(&Yaml::String("port".to_string())) + .map(|v| v.as_i64()) + .flatten() + .ok_or(Error::InvalidConfig("db.port"))? as u16, + user: db + .get(&Yaml::String("user".to_string())) + .map(|v| v.as_str()) + .flatten() + .ok_or(Error::InvalidConfig("db.user"))? + .to_string(), + password: db + .get(&Yaml::String("pass".to_string())) + .map(|v| v.as_str()) + .flatten() + .ok_or(Error::InvalidConfig("db.pass"))? + .to_string(), + name: db + .get(&Yaml::String("db".to_string())) + .map(|v| v.as_str()) + .flatten() + .ok_or(Error::InvalidConfig("db.db"))? + .to_string(), + }, + cache_server: server::CacheServer { + host: redis + .get(&Yaml::String("host".to_string())) + .map(|v| v.as_str()) + .flatten() + .ok_or(Error::InvalidConfig("redis.host"))? + .to_string(), + port: redis + .get(&Yaml::String("port".to_string())) + .map(|v| v.as_i64()) + .flatten() + .ok_or(Error::InvalidConfig("redis.port"))? as u16, + user: match redis.get(&Yaml::String("user".to_string())) { + Some(user) => Some( + user.as_str() + .ok_or(Error::InvalidConfig("redis.user"))? + .to_string(), + ), + None => None, + }, + password: match redis.get(&Yaml::String("pass".to_string())) { + Some(user) => Some( + user.as_str() + .ok_or(Error::InvalidConfig("redis.pass"))? + .to_string(), + ), + None => None, + }, + index: match redis.get(&Yaml::String("db".to_string())) { + Some(user) => Some(user.as_i64().ok_or(Error::InvalidConfig("redis.db"))? as u8), + None => None, + }, + prefix: match redis.get(&Yaml::String("prefix".to_string())) { + Some(user) => Some( + user.as_str() + .ok_or(Error::InvalidConfig("redis.prefix"))? + .to_string(), + ), + None => None, + }, + }, + id: None, + }; + + if let Some(id) = default_yml.get("cuid") { + let id = id.as_hash().ok_or(Error::InvalidConfig("cuid"))?; + server_config.id = Some(server::Id { + length: match id.get(&Yaml::String("length".to_string())) { + Some(length) => { + Some(length.as_i64().ok_or(Error::InvalidConfig("cuid.length"))? as u8) + } + None => None, + }, + fingerprint: match id.get(&Yaml::String("fingerprint".to_string())) { + Some(length) => Some( + length + .as_str() + .ok_or(Error::InvalidConfig("cuid.fingerpring"))? + .to_string(), + ), + None => None, + }, + }); + } + + Ok((server_config, todo!())) +} + +pub(super) async fn run() -> Result<(), Error> { + let (default_yml, meta) = read_old_config().await?; + let (server_config, client_config) = create_new_config(default_yml, meta).await?; Ok(()) } diff --git a/src/config/server.rs b/src/config/server.rs index a9f603e..4fdba54 100644 --- a/src/config/server.rs +++ b/src/config/server.rs @@ -10,45 +10,47 @@ use validator::Validate; #[derive(Deserialize, Serialize, Validate, Debug)] pub struct Config { - info: Option, - timelines: Option, - network: Network, - database: Database, - cache_server: CacheServer, - id: Option, + pub info: Option, + pub timelines: Option, + pub network: Network, + pub database: Database, + pub cache_server: CacheServer, + pub id: Option, } #[derive(Deserialize, Serialize, Validate, Debug)] pub struct Info { /// Server name - name: Option, + pub name: Option, /// Server description - description: Option, + pub description: Option, /// Name/handle of the server maintainer - maintainer_name: Option, + pub maintainer_name: Option, /// Contact info (e.g., email) of the server maintainer - contact_info: Option, + pub contact_info: Option, /// Whether the server allows open self-registration - open_registrations: bool, + pub open_registrations: bool, /// Repository URL - repository_url: Option, + pub repository_url: Option, } #[derive(Deserialize, Serialize, Validate, Debug)] pub struct Timelines { /// Whether to enable the local timeline - local: bool, + pub local: bool, /// Whether to enable the global timeline - global: bool, + pub global: bool, + /// Whether to enable the recommended timeline + pub recommended: bool, /// Whether to publish the local/global timelines to signed out users - guest: bool, + pub guest: bool, } #[derive(Deserialize, Serialize, Validate, Debug)] pub struct Network { - protocol: Option, - host: String, - port: u16, + pub protocol: Option, + pub host: String, + pub port: u16, } #[derive(Deserialize, Serialize, Debug)] @@ -59,26 +61,26 @@ pub enum HttpProtocol { #[derive(Deserialize, Serialize, Validate, Debug)] pub struct Database { - host: String, - port: u16, - user: String, - password: String, - name: String, + pub host: String, + pub port: u16, + pub user: String, + pub password: String, + pub name: String, } #[derive(Deserialize, Serialize, Validate, Debug)] pub struct CacheServer { - host: String, - port: u16, - user: Option, - password: Option, - index: Option, - prefix: Option, + pub host: String, + pub port: u16, + pub user: Option, + pub password: Option, + pub index: Option, + pub prefix: Option, } #[derive(Deserialize, Serialize, Validate, Debug)] pub struct Id { #[validate(range(min = 16, max = 24))] - length: Option, - fingerprint: Option, + pub length: Option, + pub fingerprint: Option, }