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

@@ -22,6 +22,10 @@ struct Cli {
/// Federation config file (JSON). Enables server-to-server message relay.
#[arg(short, long)]
federation: Option<String>,
/// Enable bot API (disabled by default)
#[arg(long, default_value = "false")]
enable_bots: bool,
}
#[tokio::main]
@@ -49,6 +53,36 @@ async fn main() -> anyhow::Result<()> {
state.federation = Some(handle);
}
// Enable bot API if requested
state.bots_enabled = cli.enable_bots;
if cli.enable_bots {
tracing::info!("Bot API enabled");
// Auto-create BotFather if it doesn't exist
let botfather_fp = "0000000000000000botfather00000000";
let botfather_key = format!("bot_fp:{}", botfather_fp);
if state.db.tokens.get(botfather_key.as_bytes()).ok().flatten().is_none() {
let token = format!("botfather:{}", hex::encode(rand::random::<[u8; 16]>()));
let bot_info = serde_json::json!({
"name": "BotFather",
"fingerprint": botfather_fp,
"token": token,
"owner": "system",
"e2e": false,
"created_at": chrono::Utc::now().timestamp(),
});
let key = format!("bot:{}", token);
let _ = state.db.tokens.insert(key.as_bytes(), serde_json::to_vec(&bot_info).unwrap_or_default());
let _ = state.db.tokens.insert(botfather_key.as_bytes(), token.as_bytes());
// Register alias
let _ = state.db.aliases.insert(b"a:botfather", botfather_fp.as_bytes());
let _ = state.db.aliases.insert(format!("fp:{}", botfather_fp).as_bytes(), b"botfather");
tracing::info!("BotFather created: @botfather (token: {}...)", &token[..20]);
} else {
tracing::info!("BotFather already exists");
}
}
// Spawn federation outgoing WS connection if enabled
if let Some(ref fed) = state.federation {
let handle = fed.clone();