v0.0.27: TG-compatible bots — plaintext send, numeric IDs, webhooks, BotFather

Bot compatibility:
- Clients send plaintext bot_message to bot aliases (no E2E encryption)
- Numeric chat_id: fp_to_numeric_id() deterministic hash, accept string/number
- Webhook delivery: POST updates to bot's webhook URL (async, fire-and-forget)
- getUpdates timeout raised to 50s (was 30, TG uses 50)
- parse_mode HTML rendered in web client
- E2E bot registration: optional seed + bundle for encrypted bot sessions

BotFather + instance control:
- --enable-bots CLI flag (default: disabled)
- BotFather auto-created on first start (@botfather alias)
- Bot ownership: owner fingerprint stored in bot_info
- All bot endpoints return 403 when disabled

Bot Bridge:
- tools/bot-bridge.py: TG-compatible proxy for unmodified TG bots
- Translates chat_id int↔string, proxies getUpdates/sendMessage
- README with python-telegram-bot and Telegraf examples

Test fixes:
- Updated tests for ETH address display in header/messages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-29 09:45:45 +04:00
parent 067f1ea20b
commit 8603087afb
14 changed files with 660 additions and 120 deletions

View File

@@ -541,6 +541,48 @@ impl App {
}
};
// If peer is a bot alias, send plaintext (no E2E)
let is_bot_peer = {
let url = format!("{}/v1/alias/whois/{}", client.base_url, normfp(&peer));
match client.client.get(&url).send().await {
Ok(resp) => resp.json::<serde_json::Value>().await.ok()
.and_then(|d| d.get("alias").and_then(|a| a.as_str().map(|s| s.ends_with("bot") || s.ends_with("Bot") || s.ends_with("_bot"))))
.unwrap_or(false),
Err(_) => false,
}
};
if is_bot_peer {
let msg_id = uuid::Uuid::new_v4().to_string();
let bot_msg = serde_json::json!({
"type": "bot_message",
"id": msg_id,
"from": normfp(&self.our_fp),
"from_name": if self.our_eth.is_empty() { self.our_fp[..12].to_string() } else { self.our_eth.clone() },
"text": text,
"timestamp": chrono::Utc::now().timestamp(),
});
let msg_bytes = serde_json::to_vec(&bot_msg).unwrap_or_default();
match client.send_message(&peer, Some(&self.our_fp), &msg_bytes).await {
Ok(_) => {
self.receipts.lock().unwrap().insert(msg_id.clone(), ReceiptStatus::Sent);
let _ = db.touch_contact(&peer, None);
let _ = db.store_message(&peer, &self.our_fp, &text, true);
self.add_message(ChatLine {
sender: if self.our_eth.is_empty() { self.our_fp[..12].to_string() } else { format!("{}...", &self.our_eth[..self.our_eth.len().min(12)]) },
text: text.clone(),
is_system: false,
is_self: true,
message_id: Some(msg_id), timestamp: Local::now(),
});
}
Err(e) => {
self.add_message(ChatLine { sender: "system".into(), text: format!("Send failed: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
}
}
return;
}
let msg_id = uuid::Uuid::new_v4().to_string();
let our_pub = identity.public_identity();
let mut ratchet = db.load_session(&peer_fp).ok().flatten();