use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Config { pub content: ContentConfig, pub crypto: CryptoConfig, pub telegram: TelegramConfig, pub groups: GroupsConfig, pub storage: StorageConfig, pub upload_limits: UploadLimits, pub server: ServerConfig, pub rate_limiting: RateLimitConfig, pub logging: LoggingConfig, pub frontend: FrontendConfig, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ContentConfig { pub keep_content: bool, pub share_mode: ShareMode, pub default_allow_download: bool, #[serde(default)] pub default_max_views: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum ShareMode { B, W, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct CryptoConfig { pub aes_master_key_source: KeySource, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum KeySource { File { path: PathBuf }, Env { var: String }, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct TelegramConfig { pub bot_token: String, pub api_url: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct GroupsConfig { pub admin_group_ids: Vec, pub review_group_ids: Vec, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct StorageConfig { pub paths: StoragePaths, pub chunk_size_bytes: usize, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct StoragePaths { pub media: PathBuf, pub documents: PathBuf, pub text: PathBuf, pub temp: PathBuf, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct UploadLimits { pub max_batch_size: usize, pub max_file_size_bytes: u64, pub max_total_batch_bytes: u64, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ServerConfig { pub base_url: String, pub bind_address: String, pub port: u16, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct RateLimitConfig { pub requests_per_minute: u32, pub burst: u32, pub password_attempts_per_minute: u32, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct LoggingConfig { pub level: String, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct FrontendConfig { pub behavior_toggles: HashMap, } impl Config { pub fn load() -> Result { let s = config::Config::builder() .add_source(config::File::with_name("config/default")) .add_source(config::File::with_name("config/local").required(false)) .add_source( config::Environment::with_prefix("CGCX") .separator("__") .try_parsing(true) .list_separator(","), ) .build() .map_err(|e| cgcx_core::CgcxError::Config(e.to_string()))?; let cfg: Config = s.try_deserialize() .map_err(|e| cgcx_core::CgcxError::Config(e.to_string()))?; cfg.validate()?; Ok(cfg) } pub fn validate(&self) -> Result<(), cgcx_core::CgcxError> { let chunk = self.storage.chunk_size_bytes; const MIN: usize = 8 * 1024 * 1024; const MAX: usize = 256 * 1024 * 1024; if chunk < MIN || chunk > MAX { return Err(cgcx_core::CgcxError::Config(format!( "chunk_size_bytes must be between {} and {}, got {}", MIN, MAX, chunk ))); } if self.telegram.bot_token.is_empty() || self.telegram.bot_token == "BOT_TOKEN_PLACEHOLDER" { return Err(cgcx_core::CgcxError::Config( "telegram.bot_token must be set to a valid bot token".into() )); } if self.server.port == 0 { return Err(cgcx_core::CgcxError::Config( "server.port must be > 0".into() )); } if self.server.base_url.is_empty() { return Err(cgcx_core::CgcxError::Config( "server.base_url must be set".into() )); } if self.upload_limits.max_batch_size == 0 || self.upload_limits.max_file_size_bytes == 0 || self.upload_limits.max_total_batch_bytes == 0 { return Err(cgcx_core::CgcxError::Config( "upload_limits must all be > 0".into() )); } if self.rate_limiting.requests_per_minute == 0 || self.rate_limiting.burst == 0 || self.rate_limiting.password_attempts_per_minute == 0 { return Err(cgcx_core::CgcxError::Config( "rate_limiting values must all be > 0".into() )); } if self.logging.level.is_empty() { return Err(cgcx_core::CgcxError::Config( "logging.level must be set".into() )); } match &self.crypto.aes_master_key_source { KeySource::File { path } if path.as_os_str().is_empty() => { return Err(cgcx_core::CgcxError::Config( "crypto.aes_master_key_source.file path must not be empty".into() )); } KeySource::Env { var } if var.is_empty() => { return Err(cgcx_core::CgcxError::Config( "crypto.aes_master_key_source.env var must not be empty".into() )); } _ => {} } Ok(()) } }