283 lines
9.8 KiB
Rust
283 lines
9.8 KiB
Rust
//! 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 <name> - Delete a bot\n\
|
|
/token <name> - 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 <name>
|
|
let name = text.strip_prefix("/newbot").unwrap_or("").trim();
|
|
if name.is_empty() {
|
|
return "Usage: /newbot <botname>\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 <botname>".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::<serde_json::Value>(&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::<serde_json::Value>(&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 <name> 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 <botname>".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::<serde_json::Value>(&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);
|
|
}
|
|
}
|