v0.0.32: system bots config — persist across data wipes, welcome screen
Server: - --bots-config <path> 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) <noreply@anthropic.com>
This commit is contained in:
10
warzone/Cargo.lock
generated
10
warzone/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -9,7 +9,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.31"
|
||||
version = "0.0.32"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
rust-version = "1.75"
|
||||
|
||||
12
warzone/bots.example.json
Normal file
12
warzone/bots.example.json
Normal file
@@ -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"}
|
||||
]
|
||||
@@ -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::<serde_json::Value>().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 {
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
#[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::<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[..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<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
|
||||
|
||||
@@ -31,6 +31,7 @@ use crate::state::AppState;
|
||||
/// Build the bot API routes (nested under `/v1`).
|
||||
pub fn routes() -> Router<AppState> {
|
||||
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<AppState> {
|
||||
.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<AppState>,
|
||||
) -> Json<serde_json::Value> {
|
||||
let bots = state.db.tokens.get(b"system:bot_list")
|
||||
.ok().flatten()
|
||||
.and_then(|v| serde_json::from_slice::<Vec<serde_json::Value>>(&v).ok())
|
||||
.unwrap_or_default();
|
||||
Json(serde_json::json!({ "ok": true, "bots": bots }))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user