use clap::Parser; mod botfather; pub mod auth_middleware; mod config; mod db; mod errors; mod federation; mod routes; mod state; #[derive(Parser)] #[command(name = "warzone-server", about = "Warzone messenger server")] struct Cli { /// Address to bind to #[arg(short, long, default_value = "0.0.0.0:7700")] bind: String, /// Database directory #[arg(short, long, default_value = "./warzone-data")] data_dir: String, /// Federation config file (JSON). Enables server-to-server message relay. #[arg(short, long)] federation: Option, /// Enable bot API (disabled by default) #[arg(long, default_value = "false")] enable_bots: bool, /// System bots config file (JSON array). Bots are auto-created on startup. #[arg(long)] bots_config: Option, } #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "info,tower_http=debug".parse().unwrap()), ) .init(); let cli = Cli::parse(); tracing::info!("Warzone server starting on {}", cli.bind); let mut state = state::AppState::new(&cli.data_dir)?; // Load federation config if provided if let Some(ref fed_path) = cli.federation { let fed_config = federation::load_config(fed_path)?; tracing::info!( "Federation enabled: server_id={}, peer={}@{}", fed_config.server_id, fed_config.peer.id, fed_config.peer.url ); let handle = federation::FederationHandle::new(fed_config); state.federation = Some(handle); } // Enable bot API if requested state.bots_enabled = cli.enable_bots; if cli.enable_bots { tracing::info!("Bot API enabled"); // Auto-create BotFather if it doesn't exist let botfather_fp = "00000000000000000b0ffa00e000000f"; let botfather_key = format!("bot_fp:{}", botfather_fp); if state.db.tokens.get(botfather_key.as_bytes()).ok().flatten().is_none() { let token = format!("botfather:{}", hex::encode(rand::random::<[u8; 16]>())); let bot_info = serde_json::json!({ "name": "BotFather", "fingerprint": botfather_fp, "token": token, "owner": "system", "e2e": false, "created_at": chrono::Utc::now().timestamp(), }); let key = format!("bot:{}", token); let _ = state.db.tokens.insert(key.as_bytes(), serde_json::to_vec(&bot_info).unwrap_or_default()); let _ = state.db.tokens.insert(botfather_key.as_bytes(), token.as_bytes()); // Register alias let _ = state.db.aliases.insert(b"a:botfather", botfather_fp.as_bytes()); let _ = state.db.aliases.insert(format!("fp:{}", botfather_fp).as_bytes(), b"botfather"); tracing::info!("BotFather created: @botfather (token: {})", token); } else { tracing::info!("BotFather already exists"); } // Always ensure alias exists (may have been lost on data wipe) let _ = state.db.aliases.insert(b"a:botfather", botfather_fp.as_bytes()); let _ = state.db.aliases.insert(format!("fp:{}", botfather_fp).as_bytes(), b"botfather"); // Store proper AliasRecord so resolve_alias works let bf_record = serde_json::json!({ "alias": "botfather", "fingerprint": botfather_fp, "recovery_key": "", "registered_at": chrono::Utc::now().timestamp(), "last_active": chrono::Utc::now().timestamp(), }); let _ = state.db.aliases.insert(b"rec:botfather", serde_json::to_vec(&bf_record).unwrap_or_default()); // Load system bots from config file if let Some(ref bots_path) = cli.bots_config { match std::fs::read_to_string(bots_path) { Ok(data) => { if let Ok(bots) = serde_json::from_str::>(&data) { for bot in &bots { let name = bot.get("name").and_then(|v| v.as_str()).unwrap_or(""); let desc = bot.get("description").and_then(|v| v.as_str()).unwrap_or(""); if name.is_empty() { continue; } let alias = name.to_lowercase(); let alias_key = format!("a:{}", alias); // Check if already exists let existing_fp = state.db.aliases.get(alias_key.as_bytes()) .ok().flatten() .map(|v| String::from_utf8_lossy(&v).to_string()); let fp = if let Some(ref efp) = existing_fp { // Bot exists — just ensure alias record is intact efp.clone() } else { // Create new bot let fp_bytes: [u8; 16] = rand::random(); let fp = hex::encode(fp_bytes); let token_rand: [u8; 16] = rand::random(); let token = format!("{}:{}", &fp[..16], hex::encode(token_rand)); let bot_info = serde_json::json!({ "name": name, "fingerprint": fp, "token": token, "owner": "system", "description": desc, "system_bot": true, "e2e": false, "created_at": chrono::Utc::now().timestamp(), }); let _ = state.db.tokens.insert(format!("bot:{}", token).as_bytes(), serde_json::to_vec(&bot_info).unwrap_or_default()); let _ = state.db.tokens.insert(format!("bot_fp:{}", fp).as_bytes(), token.as_bytes()); let _ = state.db.aliases.insert(alias_key.as_bytes(), fp.as_bytes()); let _ = state.db.aliases.insert(format!("fp:{}", fp).as_bytes(), alias.as_bytes()); tracing::info!("System bot @{} created (token: {})", alias, token); fp }; // Always ensure alias record exists let rec = serde_json::json!({ "alias": alias, "fingerprint": fp, "recovery_key": "", "registered_at": chrono::Utc::now().timestamp(), "last_active": chrono::Utc::now().timestamp(), }); let _ = state.db.aliases.insert(format!("rec:{}", alias).as_bytes(), serde_json::to_vec(&rec).unwrap_or_default()); } tracing::info!("Loaded {} system bots from {}", bots.len(), bots_path); // Write tokens to file for easy access let tokens_path = format!("{}/bot-tokens.txt", cli.data_dir); let mut token_lines = Vec::new(); for bot in &bots { let name = bot.get("name").and_then(|v| v.as_str()).unwrap_or(""); if name.is_empty() { continue; } let alias = name.to_lowercase(); if let Some(fp_bytes) = state.db.aliases.get(format!("a:{}", alias).as_bytes()).ok().flatten() { let fp = String::from_utf8_lossy(&fp_bytes).to_string(); if let Some(tok_bytes) = state.db.tokens.get(format!("bot_fp:{}", fp).as_bytes()).ok().flatten() { let tok = String::from_utf8_lossy(&tok_bytes).to_string(); token_lines.push(format!("{}={}", alias.to_uppercase(), tok)); } } } if !token_lines.is_empty() { let _ = std::fs::write(&tokens_path, token_lines.join("\n") + "\n"); tracing::info!("Bot tokens written to {}", tokens_path); } // Store bot list in DB for welcome screen let bot_list: Vec = bots.iter().map(|b| { serde_json::json!({ "name": b.get("name").and_then(|v| v.as_str()).unwrap_or(""), "description": b.get("description").and_then(|v| v.as_str()).unwrap_or(""), }) }).collect(); let _ = state.db.tokens.insert(b"system:bot_list", serde_json::to_vec(&bot_list).unwrap_or_default()); } } Err(e) => tracing::warn!("Failed to load bots config '{}': {}", bots_path, e), } } } // Spawn federation outgoing WS connection if enabled if let Some(ref fed) = state.federation { let handle = fed.clone(); let fed_state = state.clone(); tokio::spawn(async move { federation::outgoing_ws_loop(handle, fed_state).await; }); } let cors = tower_http::cors::CorsLayer::new() .allow_origin(tower_http::cors::Any) .allow_methods(tower_http::cors::Any) .allow_headers(tower_http::cors::Any); let app = axum::Router::new() .merge(routes::web_router()) .nest("/v1", routes::router()) .layer(cors) .layer(tower::limit::ConcurrencyLimitLayer::new(200)) .layer(tower_http::trace::TraceLayer::new_for_http()) .with_state(state); let listener = tokio::net::TcpListener::bind(&cli.bind).await?; tracing::info!("Listening on {}", cli.bind); axum::serve(listener, app).await?; Ok(()) }