use diesel::prelude::*; use diesel::sql_types::Text; use log::{info, warn}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Mutex}; #[derive(Clone)] pub struct AppConfig { pub minio: DriveConfig, pub server: ServerConfig, pub database: DatabaseConfig, pub database_custom: DatabaseConfig, pub email: EmailConfig, pub ai: AIConfig, pub site_path: String, pub s3_bucket: String, pub stack_path: PathBuf, pub db_conn: Option>>, } #[derive(Clone)] pub struct DatabaseConfig { pub username: String, pub password: String, pub server: String, pub port: u32, pub database: String, } #[derive(Clone)] pub struct DriveConfig { pub server: String, pub access_key: String, pub secret_key: String, pub use_ssl: bool, pub org_prefix: String, } #[derive(Clone)] pub struct ServerConfig { pub host: String, pub port: u16, } #[derive(Clone)] pub struct EmailConfig { pub from: String, pub server: String, pub port: u16, pub username: String, pub password: String, } #[derive(Clone)] pub struct AIConfig { pub instance: String, pub key: String, pub version: String, pub endpoint: String, } #[derive(Debug, Clone, Serialize, Deserialize, QueryableByName)] pub struct ServerConfigRow { #[diesel(sql_type = Text)] pub id: String, #[diesel(sql_type = Text)] pub config_key: String, #[diesel(sql_type = Text)] pub config_value: String, #[diesel(sql_type = Text)] pub config_type: String, #[diesel(sql_type = diesel::sql_types::Bool)] pub is_encrypted: bool, } impl AppConfig { pub fn database_url(&self) -> String { format!( "postgres://{}:{}@{}:{}/{}", self.database.username, self.database.password, self.database.server, self.database.port, self.database.database ) } pub fn database_custom_url(&self) -> String { format!( "postgres://{}:{}@{}:{}/{}", self.database_custom.username, self.database_custom.password, self.database_custom.server, self.database_custom.port, self.database_custom.database ) } pub fn component_path(&self, component: &str) -> PathBuf { self.stack_path.join(component) } pub fn bin_path(&self, component: &str) -> PathBuf { self.stack_path.join("bin").join(component) } pub fn data_path(&self, component: &str) -> PathBuf { self.stack_path.join("data").join(component) } pub fn config_path(&self, component: &str) -> PathBuf { self.stack_path.join("conf").join(component) } pub fn log_path(&self, component: &str) -> PathBuf { self.stack_path.join("logs").join(component) } pub fn from_database(conn: &mut PgConnection) -> Self { info!("Loading configuration from database"); let config_map = match Self::load_config_from_db(conn) { Ok(map) => { info!("Loaded {} config values from database", map.len()); map } Err(e) => { warn!("Failed to load config from database: {}. Using defaults", e); HashMap::new() } }; let get_str = |key: &str, default: &str| -> String { config_map .get(key) .map(|v| v.config_value.clone()) .unwrap_or_else(|| default.to_string()) }; let get_u32 = |key: &str, default: u32| -> u32 { config_map .get(key) .and_then(|v| v.config_value.parse().ok()) .unwrap_or(default) }; let get_u16 = |key: &str, default: u16| -> u16 { config_map .get(key) .and_then(|v| v.config_value.parse().ok()) .unwrap_or(default) }; let get_bool = |key: &str, default: bool| -> bool { config_map .get(key) .map(|v| v.config_value.to_lowercase() == "true") .unwrap_or(default) }; let stack_path = PathBuf::from(get_str("STACK_PATH", "./botserver-stack")); // For database credentials, prioritize environment variables over database values // because we need the correct credentials to connect to the database in the first place let database = DatabaseConfig { username: std::env::var("TABLES_USERNAME") .unwrap_or_else(|_| get_str("TABLES_USERNAME", "gbuser")), password: std::env::var("TABLES_PASSWORD") .unwrap_or_else(|_| get_str("TABLES_PASSWORD", "")), server: std::env::var("TABLES_SERVER") .unwrap_or_else(|_| get_str("TABLES_SERVER", "localhost")), port: std::env::var("TABLES_PORT") .ok() .and_then(|p| p.parse().ok()) .unwrap_or_else(|| get_u32("TABLES_PORT", 5432)), database: std::env::var("TABLES_DATABASE") .unwrap_or_else(|_| get_str("TABLES_DATABASE", "botserver")), }; let database_custom = DatabaseConfig { username: std::env::var("CUSTOM_USERNAME") .unwrap_or_else(|_| get_str("CUSTOM_USERNAME", "gbuser")), password: std::env::var("CUSTOM_PASSWORD") .unwrap_or_else(|_| get_str("CUSTOM_PASSWORD", "")), server: std::env::var("CUSTOM_SERVER") .unwrap_or_else(|_| get_str("CUSTOM_SERVER", "localhost")), port: std::env::var("CUSTOM_PORT") .ok() .and_then(|p| p.parse().ok()) .unwrap_or_else(|| get_u32("CUSTOM_PORT", 5432)), database: std::env::var("CUSTOM_DATABASE") .unwrap_or_else(|_| get_str("CUSTOM_DATABASE", "botserver")), }; let minio = DriveConfig { server: get_str("DRIVE_SERVER", "http://localhost:9000"), access_key: get_str("DRIVE_ACCESSKEY", "minioadmin"), secret_key: get_str("DRIVE_SECRET", "minioadmin"), use_ssl: get_bool("DRIVE_USE_SSL", false), org_prefix: get_str("DRIVE_ORG_PREFIX", "botserver"), }; let email = EmailConfig { from: get_str("EMAIL_FROM", "noreply@example.com"), server: get_str("EMAIL_SERVER", "smtp.example.com"), port: get_u16("EMAIL_PORT", 587), username: get_str("EMAIL_USER", "user"), password: get_str("EMAIL_PASS", "pass"), }; let ai = AIConfig { instance: get_str("AI_INSTANCE", "gpt-4"), key: get_str("AI_KEY", ""), version: get_str("AI_VERSION", "2023-12-01-preview"), endpoint: get_str("AI_ENDPOINT", "https://api.openai.com"), }; AppConfig { minio, server: ServerConfig { host: get_str("SERVER_HOST", "127.0.0.1"), port: get_u16("SERVER_PORT", 8080), }, database, database_custom, email, ai, s3_bucket: get_str("DRIVE_BUCKET", "default"), site_path: get_str("SITES_ROOT", "./botserver-stack/sites"), stack_path, db_conn: None, } } pub fn from_env() -> Self { warn!("Loading configuration from environment variables"); let stack_path = std::env::var("STACK_PATH").unwrap_or_else(|_| "./botserver-stack".to_string()); let database_url = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string()); let (db_username, db_password, db_server, db_port, db_name) = parse_database_url(&database_url); let database = DatabaseConfig { username: db_username, password: db_password, server: db_server, port: db_port, database: db_name, }; let database_custom = DatabaseConfig { username: std::env::var("CUSTOM_USERNAME").unwrap_or_else(|_| "gbuser".to_string()), password: std::env::var("CUSTOM_PASSWORD").unwrap_or_else(|_| "".to_string()), server: std::env::var("CUSTOM_SERVER").unwrap_or_else(|_| "localhost".to_string()), port: std::env::var("CUSTOM_PORT") .ok() .and_then(|p| p.parse().ok()) .unwrap_or(5432), database: std::env::var("CUSTOM_DATABASE").unwrap_or_else(|_| "botserver".to_string()), }; let minio = DriveConfig { server: std::env::var("DRIVE_SERVER") .unwrap_or_else(|_| "http://localhost:9000".to_string()), access_key: std::env::var("DRIVE_ACCESSKEY") .unwrap_or_else(|_| "minioadmin".to_string()), secret_key: std::env::var("DRIVE_SECRET").unwrap_or_else(|_| "minioadmin".to_string()), use_ssl: std::env::var("DRIVE_USE_SSL") .unwrap_or_else(|_| "false".to_string()) .parse() .unwrap_or(false), org_prefix: std::env::var("DRIVE_ORG_PREFIX") .unwrap_or_else(|_| "botserver".to_string()), }; let email = EmailConfig { from: std::env::var("EMAIL_FROM").unwrap_or_else(|_| "noreply@example.com".to_string()), server: std::env::var("EMAIL_SERVER") .unwrap_or_else(|_| "smtp.example.com".to_string()), port: std::env::var("EMAIL_PORT") .unwrap_or_else(|_| "587".to_string()) .parse() .unwrap_or(587), username: std::env::var("EMAIL_USER").unwrap_or_else(|_| "user".to_string()), password: std::env::var("EMAIL_PASS").unwrap_or_else(|_| "pass".to_string()), }; let ai = AIConfig { instance: std::env::var("AI_INSTANCE").unwrap_or_else(|_| "gpt-4".to_string()), key: std::env::var("AI_KEY").unwrap_or_else(|_| "".to_string()), version: std::env::var("AI_VERSION") .unwrap_or_else(|_| "2023-12-01-preview".to_string()), endpoint: std::env::var("AI_ENDPOINT") .unwrap_or_else(|_| "https://api.openai.com".to_string()), }; AppConfig { minio, server: ServerConfig { host: std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()), port: std::env::var("SERVER_PORT") .ok() .and_then(|p| p.parse().ok()) .unwrap_or(8080), }, database, database_custom, email, ai, s3_bucket: std::env::var("DRIVE_BUCKET").unwrap_or_else(|_| "default".to_string()), site_path: std::env::var("SITES_ROOT") .unwrap_or_else(|_| "./botserver-stack/sites".to_string()), stack_path: PathBuf::from(stack_path), db_conn: None, } } fn load_config_from_db( conn: &mut PgConnection, ) -> Result, diesel::result::Error> { let results = diesel::sql_query("SELECT id, config_key, config_value, config_type, is_encrypted FROM server_configuration").load::(conn)?; let mut map = HashMap::new(); for row in results { map.insert(row.config_key.clone(), row); } Ok(map) } pub fn set_config( &self, conn: &mut PgConnection, key: &str, value: &str, ) -> Result<(), diesel::result::Error> { diesel::sql_query("SELECT set_config($1, $2)") .bind::(key) .bind::(value) .execute(conn)?; info!("Updated configuration: {} = {}", key, value); Ok(()) } pub fn get_config( &self, conn: &mut PgConnection, key: &str, fallback: Option<&str>, ) -> Result { let fallback_str = fallback.unwrap_or(""); #[derive(Debug, QueryableByName)] struct ConfigValue { #[diesel(sql_type = Text)] value: String, } let result = diesel::sql_query("SELECT get_config($1, $2) as value") .bind::(key) .bind::(fallback_str) .get_result::(conn) .map(|row| row.value)?; Ok(result) } } fn parse_database_url(url: &str) -> (String, String, String, u32, String) { if let Some(stripped) = url.strip_prefix("postgres://") { let parts: Vec<&str> = stripped.split('@').collect(); if parts.len() == 2 { let user_pass: Vec<&str> = parts[0].split(':').collect(); let host_db: Vec<&str> = parts[1].split('/').collect(); if user_pass.len() >= 2 && host_db.len() >= 2 { let username = user_pass[0].to_string(); let password = user_pass[1].to_string(); let host_port: Vec<&str> = host_db[0].split(':').collect(); let server = host_port[0].to_string(); let port = host_port .get(1) .and_then(|p| p.parse().ok()) .unwrap_or(5432); let database = host_db[1].to_string(); return (username, password, server, port, database); } } } ( "gbuser".to_string(), "".to_string(), "localhost".to_string(), 5432, "botserver".to_string(), ) } pub struct ConfigManager { conn: Arc>, } impl ConfigManager { pub fn new(conn: Arc>) -> Self { Self { conn } } pub fn sync_gbot_config( &self, bot_id: &uuid::Uuid, config_path: &str, ) -> Result { use sha2::{Digest, Sha256}; use std::fs; let content = fs::read_to_string(config_path) .map_err(|e| format!("Failed to read config file: {}", e))?; let mut hasher = Sha256::new(); hasher.update(content.as_bytes()); let file_hash = format!("{:x}", hasher.finalize()); let mut conn = self .conn .lock() .map_err(|e| format!("Failed to acquire lock: {}", e))?; #[derive(QueryableByName)] struct SyncHash { #[diesel(sql_type = Text)] file_hash: String, } let last_hash: Option = diesel::sql_query("SELECT file_hash FROM gbot_config_sync WHERE bot_id = $1") .bind::(bot_id) .get_result::(&mut *conn) .optional() .map_err(|e| format!("Database error: {}", e))? .map(|row| row.file_hash); if last_hash.as_ref() == Some(&file_hash) { info!("Config file unchanged for bot {}", bot_id); return Ok(0); } let mut updated = 0; for line in content.lines().skip(1) { let parts: Vec<&str> = line.split(',').collect(); if parts.len() >= 2 { let key = parts[0].trim(); let value = parts[1].trim(); diesel::sql_query("INSERT INTO bot_configuration (id, bot_id, config_key, config_value, config_type) VALUES (gen_random_uuid()::text, $1, $2, $3, 'string') ON CONFLICT (bot_id, config_key) DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()") .bind::(bot_id) .bind::(key) .bind::(value) .execute(&mut *conn) .map_err(|e| format!("Failed to update config: {}", e))?; updated += 1; } } diesel::sql_query("INSERT INTO gbot_config_sync (id, bot_id, config_file_path, file_hash, sync_count) VALUES (gen_random_uuid()::text, $1, $2, $3, 1) ON CONFLICT (bot_id) DO UPDATE SET last_sync_at = NOW(), file_hash = EXCLUDED.file_hash, sync_count = gbot_config_sync.sync_count + 1") .bind::(bot_id) .bind::(config_path) .bind::(&file_hash) .execute(&mut *conn) .map_err(|e| format!("Failed to update sync record: {}", e))?; info!( "Synced {} config values for bot {} from {}", updated, bot_id, config_path ); Ok(updated) } }