223 lines
10 KiB
Rust
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(())
|
|
}
|