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>
122 lines
4.3 KiB
Rust
122 lines
4.3 KiB
Rust
use axum::{
|
|
extract::{Path, State},
|
|
routing::get,
|
|
Json, Router,
|
|
};
|
|
|
|
use crate::errors::AppResult;
|
|
use crate::state::AppState;
|
|
|
|
/// Convert a fingerprint hex string to a stable i64 ID (for Telegram compatibility).
|
|
/// Uses first 8 bytes of the fingerprint as a positive i64.
|
|
pub fn fp_to_numeric_id(fp: &str) -> i64 {
|
|
let clean: String = fp.chars().filter(|c| c.is_ascii_hexdigit()).take(16).collect();
|
|
let bytes = hex::decode(&clean).unwrap_or_default();
|
|
if bytes.len() >= 8 {
|
|
let mut arr = [0u8; 8];
|
|
arr.copy_from_slice(&bytes[..8]);
|
|
i64::from_be_bytes(arr) & 0x7FFFFFFFFFFFFFFF // ensure positive
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
pub fn routes() -> Router<AppState> {
|
|
Router::new().route("/resolve/:address", get(resolve_address))
|
|
}
|
|
|
|
/// Resolve an address to a fingerprint.
|
|
///
|
|
/// Accepts: ETH address (`0x...`), alias (`@name`), or raw fingerprint.
|
|
async fn resolve_address(
|
|
State(state): State<AppState>,
|
|
Path(address): Path<String>,
|
|
) -> AppResult<Json<serde_json::Value>> {
|
|
let addr = address.trim().to_lowercase();
|
|
|
|
// ETH address: 0x...
|
|
if addr.starts_with("0x") {
|
|
if let Some(fp_bytes) = state.db.eth_addresses.get(addr.as_bytes())? {
|
|
let fp = String::from_utf8_lossy(&fp_bytes).to_string();
|
|
return Ok(Json(serde_json::json!({
|
|
"address": address,
|
|
"fingerprint": fp,
|
|
"numeric_id": fp_to_numeric_id(&fp),
|
|
"type": "eth",
|
|
})));
|
|
}
|
|
// Try federation
|
|
if let Some(ref federation) = state.federation {
|
|
let url = format!("{}/v1/resolve/{}", federation.config.peer.url, addr);
|
|
if let Ok(resp) = federation.client.get(&url).send().await {
|
|
if resp.status().is_success() {
|
|
if let Ok(data) = resp.json::<serde_json::Value>().await {
|
|
if let Some(fp) = data.get("fingerprint").and_then(|v| v.as_str()) {
|
|
return Ok(Json(serde_json::json!({
|
|
"address": address,
|
|
"fingerprint": fp,
|
|
"numeric_id": fp_to_numeric_id(fp),
|
|
"type": "eth",
|
|
"federated": true,
|
|
})));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return Ok(Json(serde_json::json!({ "error": "address not found" })));
|
|
}
|
|
|
|
// Alias: @name
|
|
if addr.starts_with('@') {
|
|
let alias = &addr[1..];
|
|
// Try local alias resolution
|
|
let alias_key = format!("a:{}", alias);
|
|
if let Some(fp_bytes) = state.db.aliases.get(alias_key.as_bytes())? {
|
|
let fp = String::from_utf8_lossy(&fp_bytes).to_string();
|
|
return Ok(Json(serde_json::json!({
|
|
"address": address,
|
|
"fingerprint": fp,
|
|
"numeric_id": fp_to_numeric_id(&fp),
|
|
"type": "alias",
|
|
})));
|
|
}
|
|
// Try federation
|
|
if let Some(ref federation) = state.federation {
|
|
if let Some(fp) = federation.resolve_remote_alias(alias).await {
|
|
return Ok(Json(serde_json::json!({
|
|
"address": address,
|
|
"fingerprint": fp,
|
|
"numeric_id": fp_to_numeric_id(&fp),
|
|
"type": "alias",
|
|
"federated": true,
|
|
})));
|
|
}
|
|
}
|
|
return Ok(Json(serde_json::json!({ "error": "alias not found" })));
|
|
}
|
|
|
|
// Raw fingerprint: just echo back with optional reverse ETH lookup
|
|
let fp = addr
|
|
.chars()
|
|
.filter(|c| c.is_ascii_hexdigit())
|
|
.collect::<String>();
|
|
if fp.len() == 32 {
|
|
let rev_key = format!("rev:{}", fp);
|
|
let eth = state
|
|
.db
|
|
.eth_addresses
|
|
.get(rev_key.as_bytes())?
|
|
.map(|v| String::from_utf8_lossy(&v).to_string());
|
|
return Ok(Json(serde_json::json!({
|
|
"address": address,
|
|
"fingerprint": fp,
|
|
"numeric_id": fp_to_numeric_id(&fp),
|
|
"eth_address": eth,
|
|
"type": "fingerprint",
|
|
})));
|
|
}
|
|
|
|
Ok(Json(serde_json::json!({ "error": "unrecognized address format" })))
|
|
}
|