Files
featherChat/warzone/crates/warzone-server/src/main.rs
Siavash Sameni 3489a7cf74 fix: log full bot tokens + write to data_dir/bot-tokens.txt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:05:11 +04:00

223 lines
10 KiB
Rust

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<String>,
/// 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<String>,
}
#[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::<Vec<serde_json::Value>>(&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<serde_json::Value> = 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(())
}