Files
featherChat/warzone/crates/warzone-server/src/routes/friends.rs
Siavash Sameni 7b72f7cba5 feat: friend list, bot API, ETH addressing, deep links, docs overhaul
Tier 1 — New features:
- E2E encrypted friend list: server stores opaque blob (POST/GET /v1/friends),
  protocol-level encrypt/decrypt with HKDF-derived key, 4 tests
- Telegram Bot API compatibility: /bot/register, /bot/:token/getUpdates,
  sendMessage, getMe — TG-style Update objects with proper message mapping
- ETH address resolution: GET /v1/resolve/:address (0x.../alias/@.../fp),
  bidirectional ETH↔fp mapping stored on key registration
- Seed recovery: /seed command in TUI + web client
- URL deep links: /message/@alias, /message/0xABC, /group/#ops
- Group members with online status in GET /groups/:name/members

Tier 2 — UX polish:
- TUI: /friend, /friend <addr>, /unfriend <addr> with presence checking
- Web: friend commands, showGroupMembers() on group join
- Web: ETH address in header, clickable addresses (click→peer or copy)
- Bot: full WireMessage→TG Update mapping (encrypted base64, CallSignal,
  FileHeader, bot_message JSON)

Documentation:
- USAGE.md rewritten: complete user guide with all commands
- SERVER.md rewritten: full admin guide with all 50+ endpoints
- CLIENT.md rewritten: architecture, commands, keyboard, storage
- LLM_HELP.md created: 1083-word token-optimized reference for helper LLM

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 07:31:54 +04:00

55 lines
1.7 KiB
Rust

use axum::{
extract::State,
routing::{get, post},
Json, Router,
};
use serde::Deserialize;
use crate::auth_middleware::AuthFingerprint;
use crate::errors::AppResult;
use crate::state::AppState;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/friends", get(get_friends))
.route("/friends", post(save_friends))
}
/// Get the encrypted friend list blob for the authenticated user.
async fn get_friends(
auth: AuthFingerprint,
State(state): State<AppState>,
) -> AppResult<Json<serde_json::Value>> {
match state.db.friends.get(auth.fingerprint.as_bytes())? {
Some(data) => {
let blob = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &data);
Ok(Json(serde_json::json!({
"fingerprint": auth.fingerprint,
"data": blob,
})))
}
None => Ok(Json(serde_json::json!({
"fingerprint": auth.fingerprint,
"data": null,
}))),
}
}
#[derive(Deserialize)]
struct SaveFriendsRequest {
data: String, // base64-encoded encrypted blob
}
/// Save the encrypted friend list blob.
async fn save_friends(
auth: AuthFingerprint,
State(state): State<AppState>,
Json(req): Json<SaveFriendsRequest>,
) -> AppResult<Json<serde_json::Value>> {
let blob = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &req.data)
.map_err(|e| anyhow::anyhow!("invalid base64: {}", e))?;
state.db.friends.insert(auth.fingerprint.as_bytes(), blob)?;
tracing::info!("Saved friend list for {} ({} bytes)", auth.fingerprint, req.data.len());
Ok(Json(serde_json::json!({ "ok": true })))
}