//! Built-in BotFather: processes messages to @botfather and manages bot lifecycle. //! //! Supports: /start, /newbot, /mybots, /deletebot, /help //! Runs as a server-side handler — no external process needed. use crate::state::AppState; const BOTFATHER_FP: &str = "00000000000000000b0ffa00e000000f"; /// Check if a message is destined for BotFather and handle it. /// Called from deliver_or_queue when the recipient is the BotFather fingerprint. /// Returns true if handled (message consumed). pub async fn handle_botfather_message(state: &AppState, from_fp: &str, message: &[u8]) -> bool { if !state.bots_enabled { return false; } // Try to parse as plaintext bot_message JSON let bot_msg: serde_json::Value = match serde_json::from_slice(message) { Ok(v) => v, Err(_) => return false, // Encrypted messages can't be processed by built-in handler }; if bot_msg.get("type").and_then(|v| v.as_str()) != Some("bot_message") { return false; } let text = bot_msg .get("text") .and_then(|v| v.as_str()) .unwrap_or("") .trim(); let from_name = bot_msg .get("from_name") .and_then(|v| v.as_str()) .unwrap_or(from_fp); tracing::info!( "BotFather: message from {} ({}): {}", from_fp, from_name, text ); let response = match text { "/start" | "/help" => { "Welcome to BotFather! I can help you create and manage bots.\n\n\ Commands:\n\ /newbot - Create a new bot\n\ /mybots - List your bots\n\ /deletebot - Delete a bot\n\ /token - Get bot token\n\ /help - Show this message" .to_string() } t if t.starts_with("/newbot") => handle_newbot(state, from_fp, t).await, t if t.starts_with("/deletebot") => handle_deletebot(state, from_fp, t).await, "/mybots" => handle_mybots(state, from_fp).await, t if t.starts_with("/token") => handle_token(state, from_fp, t).await, _ => "I don't understand that command. Try /help".to_string(), }; // Send response back to the user send_botfather_reply(state, from_fp, &response).await; true } async fn handle_newbot(state: &AppState, owner_fp: &str, text: &str) -> String { // Parse: /newbot let name = text.strip_prefix("/newbot").unwrap_or("").trim(); if name.is_empty() { return "Usage: /newbot \n\nExample: /newbot WeatherBot\n\n\ The name must end with 'bot' or 'Bot'." .to_string(); } // Validate name if name.len() > 32 || name.len() < 3 { return "Bot name must be 3-32 characters.".to_string(); } let name_lower = name.to_lowercase(); if !name_lower.ends_with("bot") { return "Bot name must end with 'bot' or 'Bot'. Example: WeatherBot, my_bot".to_string(); } // Check if alias is taken let alias_key = format!("a:{}", name_lower); if state .db .aliases .get(alias_key.as_bytes()) .ok() .flatten() .is_some() { return format!( "Sorry, @{} is already taken. Try a different name.", name_lower ); } // Generate fingerprint and token 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)); // Store bot info let bot_info = serde_json::json!({ "name": name, "fingerprint": fp, "token": token, "owner": owner_fp, "e2e": false, "created_at": chrono::Utc::now().timestamp(), }); let bot_key = format!("bot:{}", token); let _ = state.db.tokens.insert( bot_key.as_bytes(), serde_json::to_vec(&bot_info).unwrap_or_default(), ); let fp_key = format!("bot_fp:{}", fp); let _ = state.db.tokens.insert(fp_key.as_bytes(), token.as_bytes()); // Register alias (all 3 keys needed for resolve_alias to work) let _ = state.db.aliases.insert(alias_key.as_bytes(), fp.as_bytes()); let _ = state.db.aliases.insert(format!("fp:{}", fp).as_bytes(), name_lower.as_bytes()); let alias_record = serde_json::json!({ "alias": name_lower, "fingerprint": fp, "recovery_key": "", "registered_at": chrono::Utc::now().timestamp(), "last_active": chrono::Utc::now().timestamp(), }); let _ = state.db.aliases.insert(format!("rec:{}", name_lower).as_bytes(), serde_json::to_vec(&alias_record).unwrap_or_default()); tracing::info!( "BotFather: created bot @{} for owner {}", name_lower, owner_fp ); format!( "Done! Your new bot @{} is ready.\n\n\ Token: {}\n\n\ Keep this token secret! Use it to access the Bot API.\n\n\ API endpoint: /v1/bot/{}/getUpdates", name_lower, token, token ) } async fn handle_deletebot(state: &AppState, owner_fp: &str, text: &str) -> String { let name = text .strip_prefix("/deletebot") .unwrap_or("") .trim() .to_lowercase(); if name.is_empty() { return "Usage: /deletebot ".to_string(); } // Find the bot let alias_key = format!("a:{}", name); let bot_fp = match state.db.aliases.get(alias_key.as_bytes()).ok().flatten() { Some(v) => String::from_utf8_lossy(&v).to_string(), None => return format!("Bot @{} not found.", name), }; // Get bot info to verify ownership let token_key = format!("bot_fp:{}", bot_fp); let token = match state.db.tokens.get(token_key.as_bytes()).ok().flatten() { Some(v) => String::from_utf8_lossy(&v).to_string(), None => return format!("Bot @{} not found in registry.", name), }; let bot_key = format!("bot:{}", token); if let Some(info_bytes) = state.db.tokens.get(bot_key.as_bytes()).ok().flatten() { if let Ok(info) = serde_json::from_slice::(&info_bytes) { let owner = info.get("owner").and_then(|v| v.as_str()).unwrap_or(""); if owner != owner_fp && owner != "system" { return format!("You don't own @{}. Only the owner can delete it.", name); } } } // Delete everything let _ = state.db.tokens.remove(bot_key.as_bytes()); let _ = state.db.tokens.remove(token_key.as_bytes()); let _ = state.db.aliases.remove(alias_key.as_bytes()); let _ = state .db .aliases .remove(format!("fp:{}", bot_fp).as_bytes()); let _ = state.db.keys.remove(bot_fp.as_bytes()); tracing::info!("BotFather: deleted bot @{} by owner {}", name, owner_fp); format!("Bot @{} has been deleted.", name) } async fn handle_mybots(state: &AppState, owner_fp: &str) -> String { let mut bots = Vec::new(); for item in state.db.tokens.iter().flatten() { let key = String::from_utf8_lossy(&item.0).to_string(); if !key.starts_with("bot:") { continue; } if let Ok(info) = serde_json::from_slice::(&item.1) { let owner = info.get("owner").and_then(|v| v.as_str()).unwrap_or(""); if owner == owner_fp { let name = info.get("name").and_then(|v| v.as_str()).unwrap_or("?"); let e2e = info.get("e2e").and_then(|v| v.as_bool()).unwrap_or(false); let mode = if e2e { "E2E" } else { "plaintext" }; bots.push(format!(" @{} ({})", name.to_lowercase(), mode)); } } } if bots.is_empty() { "You have no bots. Use /newbot to create one.".to_string() } else { format!("Your bots ({}):\n{}", bots.len(), bots.join("\n")) } } async fn handle_token(state: &AppState, owner_fp: &str, text: &str) -> String { let name = text .strip_prefix("/token") .unwrap_or("") .trim() .to_lowercase(); if name.is_empty() { return "Usage: /token ".to_string(); } let alias_key = format!("a:{}", name); let bot_fp = match state.db.aliases.get(alias_key.as_bytes()).ok().flatten() { Some(v) => String::from_utf8_lossy(&v).to_string(), None => return format!("Bot @{} not found.", name), }; let token_key = format!("bot_fp:{}", bot_fp); let token = match state.db.tokens.get(token_key.as_bytes()).ok().flatten() { Some(v) => String::from_utf8_lossy(&v).to_string(), None => return format!("Token not found for @{}.", name), }; // Verify ownership let bot_key = format!("bot:{}", token); if let Some(info_bytes) = state.db.tokens.get(bot_key.as_bytes()).ok().flatten() { if let Ok(info) = serde_json::from_slice::(&info_bytes) { let owner = info.get("owner").and_then(|v| v.as_str()).unwrap_or(""); if owner != owner_fp { return format!("You don't own @{}.", name); } } } format!("Token for @{}:\n{}", name, token) } /// Send a reply from BotFather to a user. async fn send_botfather_reply(state: &AppState, to_fp: &str, text: &str) { let msg = serde_json::json!({ "type": "bot_message", "id": uuid::Uuid::new_v4().to_string(), "from": BOTFATHER_FP, "from_name": "BotFather", "text": text, "timestamp": chrono::Utc::now().timestamp(), }); let msg_bytes = serde_json::to_vec(&msg).unwrap_or_default(); // Deliver directly (don't go through deliver_or_queue to avoid recursion) if !state.push_to_client(to_fp, &msg_bytes).await { // Queue for offline pickup let key = format!("queue:{}:{}", to_fp, uuid::Uuid::new_v4()); let _ = state.db.messages.insert(key.as_bytes(), msg_bytes); } }