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, } #[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[..20]); } 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()); } // 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(()) }