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>
55 lines
1.7 KiB
Rust
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 })))
|
|
}
|