From 13f2227bf0e3dc6d2cfe8fe04d1a5c1ba7972461 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Sun, 29 Mar 2026 14:07:34 +0400 Subject: [PATCH] =?UTF-8?q?v0.0.32:=20system=20bots=20config=20=E2=80=94?= =?UTF-8?q?=20persist=20across=20data=20wipes,=20welcome=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server: - --bots-config loads JSON array of system bots on startup - Bots auto-created if missing, aliases restored on every start - Bot list stored in DB for welcome screen (system:bot_list key) - GET /v1/bot/list returns system bots (public, no auth) Welcome screen: - Web + TUI show available bots on first login - "Available bots: @helpbot — featherChat help, @codebot — Coding..." - Clickable in web (via address detection) Config: bots.example.json with 10 suggested bots Usage: warzone-server --enable-bots --bots-config bots.json Co-Authored-By: Claude Opus 4.6 (1M context) --- warzone/Cargo.lock | 10 +-- warzone/Cargo.toml | 2 +- warzone/bots.example.json | 12 +++ warzone/crates/warzone-client/src/tui/mod.rs | 17 +++++ warzone/crates/warzone-protocol/Cargo.toml | 2 +- warzone/crates/warzone-server/src/main.rs | 76 +++++++++++++++++++ .../crates/warzone-server/src/routes/bot.rs | 17 +++++ .../crates/warzone-server/src/routes/web.rs | 18 ++++- 8 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 warzone/bots.example.json diff --git a/warzone/Cargo.lock b/warzone/Cargo.lock index b751509..58c81e1 100644 --- a/warzone/Cargo.lock +++ b/warzone/Cargo.lock @@ -2956,7 +2956,7 @@ dependencies = [ [[package]] name = "warzone-client" -version = "0.0.31" +version = "0.0.32" dependencies = [ "anyhow", "argon2", @@ -2989,7 +2989,7 @@ dependencies = [ [[package]] name = "warzone-mule" -version = "0.0.31" +version = "0.0.32" dependencies = [ "anyhow", "clap", @@ -2998,7 +2998,7 @@ dependencies = [ [[package]] name = "warzone-protocol" -version = "0.0.31" +version = "0.0.32" dependencies = [ "base64", "bincode", @@ -3023,7 +3023,7 @@ dependencies = [ [[package]] name = "warzone-server" -version = "0.0.31" +version = "0.0.32" dependencies = [ "anyhow", "axum", @@ -3053,7 +3053,7 @@ dependencies = [ [[package]] name = "warzone-wasm" -version = "0.0.31" +version = "0.0.32" dependencies = [ "base64", "bincode", diff --git a/warzone/Cargo.toml b/warzone/Cargo.toml index 43f7bfc..07af4be 100644 --- a/warzone/Cargo.toml +++ b/warzone/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "0.0.31" +version = "0.0.32" edition = "2021" license = "MIT" rust-version = "1.75" diff --git a/warzone/bots.example.json b/warzone/bots.example.json new file mode 100644 index 0000000..f2c3c22 --- /dev/null +++ b/warzone/bots.example.json @@ -0,0 +1,12 @@ +[ + {"name": "helpbot", "description": "featherChat help & FAQ"}, + {"name": "codebot", "description": "Coding assistant"}, + {"name": "survivalbot", "description": "War/emergency/survival guide"}, + {"name": "farsibot", "description": "Farsi → English translation"}, + {"name": "engbot", "description": "English → Farsi translation"}, + {"name": "mathbot", "description": "Math helper"}, + {"name": "medbot", "description": "First aid & health info"}, + {"name": "writebot", "description": "Writing assistant"}, + {"name": "cookbot", "description": "Cooking with limited ingredients"}, + {"name": "mindbot", "description": "Mental health & stress support"} +] diff --git a/warzone/crates/warzone-client/src/tui/mod.rs b/warzone/crates/warzone-client/src/tui/mod.rs index c53a0e2..4fe02ac 100644 --- a/warzone/crates/warzone-client/src/tui/mod.rs +++ b/warzone/crates/warzone-client/src/tui/mod.rs @@ -64,6 +64,23 @@ pub async fn run_tui( message_id: None, timestamp: chrono::Local::now(), }); + + // Show system bots + if let Ok(resp) = client.client.get(format!("{}/v1/bot/list", client.base_url)).send().await { + if let Ok(data) = resp.json::().await { + if let Some(bots) = data.get("bots").and_then(|v| v.as_array()) { + if !bots.is_empty() { + app.add_message(types::ChatLine { sender: "system".into(), text: "Available bots:".into(), is_system: true, is_self: false, message_id: None, timestamp: chrono::Local::now() }); + for b in bots { + let name = b.get("name").and_then(|v| v.as_str()).unwrap_or("?"); + let desc = b.get("description").and_then(|v| v.as_str()).unwrap_or(""); + app.add_message(types::ChatLine { sender: "system".into(), text: format!(" @{} — {}", name, desc), is_system: true, is_self: false, message_id: None, timestamp: chrono::Local::now() }); + } + app.add_message(types::ChatLine { sender: "system".into(), text: "Message a bot: /peer @botname".into(), is_system: true, is_self: false, message_id: None, timestamp: chrono::Local::now() }); + } + } + } + } } loop { diff --git a/warzone/crates/warzone-protocol/Cargo.toml b/warzone/crates/warzone-protocol/Cargo.toml index 9a744a0..cec0378 100644 --- a/warzone/crates/warzone-protocol/Cargo.toml +++ b/warzone/crates/warzone-protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warzone-protocol" -version = "0.0.31" +version = "0.0.32" edition = "2021" license = "MIT" description = "Core crypto & wire protocol for featherChat (Warzone messenger)" diff --git a/warzone/crates/warzone-server/src/main.rs b/warzone/crates/warzone-server/src/main.rs index c5d65a9..57ef32e 100644 --- a/warzone/crates/warzone-server/src/main.rs +++ b/warzone/crates/warzone-server/src/main.rs @@ -27,6 +27,10 @@ struct Cli { /// 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] @@ -94,6 +98,78 @@ async fn main() -> anyhow::Result<()> { "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[..20]); + 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); + + // 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 diff --git a/warzone/crates/warzone-server/src/routes/bot.rs b/warzone/crates/warzone-server/src/routes/bot.rs index 0764a84..477288f 100644 --- a/warzone/crates/warzone-server/src/routes/bot.rs +++ b/warzone/crates/warzone-server/src/routes/bot.rs @@ -31,6 +31,7 @@ use crate::state::AppState; /// Build the bot API routes (nested under `/v1`). pub fn routes() -> Router { Router::new() + .route("/bot/list", get(list_system_bots)) .route("/bot/register", post(register_bot)) .route("/bot/:token/getMe", get(get_me)) .route("/bot/:token/getUpdates", post(get_updates)) @@ -43,6 +44,22 @@ pub fn routes() -> Router { .route("/bot/:token/sendDocument", post(send_document)) } +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// System bot list (public, no auth needed) +// --------------------------------------------------------------------------- + +/// GET /v1/bot/list — returns system bots for welcome screen. +async fn list_system_bots( + State(state): State, +) -> Json { + let bots = state.db.tokens.get(b"system:bot_list") + .ok().flatten() + .and_then(|v| serde_json::from_slice::>(&v).ok()) + .unwrap_or_default(); + Json(serde_json::json!({ "ok": true, "bots": bots })) +} + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- diff --git a/warzone/crates/warzone-server/src/routes/web.rs b/warzone/crates/warzone-server/src/routes/web.rs index 71827c4..7947efa 100644 --- a/warzone/crates/warzone-server/src/routes/web.rs +++ b/warzone/crates/warzone-server/src/routes/web.rs @@ -50,7 +50,7 @@ async fn pwa_manifest() -> impl IntoResponse { async fn service_worker() -> impl IntoResponse { ([(header::CONTENT_TYPE, "application/javascript")], r##" -const CACHE = 'wz-v13'; +const CACHE = 'wz-v14'; const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json']; self.addEventListener('install', e => { @@ -251,7 +251,7 @@ let pollTimer = null; let ws = null; // WebSocket connection let wasmReady = false; -const VERSION = '0.0.31'; +const VERSION = '0.0.32'; let DEBUG = true; // toggle with /debug command // ── Receipt tracking ── @@ -924,6 +924,20 @@ async function enterChat() { addSys('v' + VERSION + ' | DM: paste peer fingerprint or @alias above'); addSys('/alias · /g · /gleave · /gkick · /gmembers · /glist · /friend · /file · /info'); + // Show system bots if available + try { + const botResp = await fetch(SERVER + '/v1/bot/list'); + const botData = await botResp.json(); + if (botData.ok && botData.bots && botData.bots.length > 0) { + addSys(''); + addSys('Available bots:'); + for (const b of botData.bots) { + addSys(' @' + b.name + ' — ' + b.description); + } + addSys('Message a bot: /peer @botname'); + } + } catch(e) {} + const savedPeer = localStorage.getItem('wz-peer'); if (savedPeer) { $peerInput.value = savedPeer;