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>
This commit is contained in:
113
warzone/crates/warzone-protocol/src/friends.rs
Normal file
113
warzone/crates/warzone-protocol/src/friends.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
//! Encrypted friend list — stored on server as opaque blob.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::crypto::{aead_encrypt, aead_decrypt, hkdf_derive};
|
||||
|
||||
/// A friend entry.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Friend {
|
||||
/// ETH address or fingerprint
|
||||
pub address: String,
|
||||
/// Optional display name / alias
|
||||
pub alias: Option<String>,
|
||||
/// When this friend was added (unix timestamp)
|
||||
pub added_at: i64,
|
||||
}
|
||||
|
||||
/// The full friend list (plaintext, before encryption).
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct FriendList {
|
||||
pub friends: Vec<Friend>,
|
||||
}
|
||||
|
||||
impl FriendList {
|
||||
pub fn new() -> Self {
|
||||
FriendList { friends: vec![] }
|
||||
}
|
||||
|
||||
pub fn add(&mut self, address: &str, alias: Option<&str>) {
|
||||
// Don't add duplicates
|
||||
if self.friends.iter().any(|f| f.address == address) {
|
||||
return;
|
||||
}
|
||||
self.friends.push(Friend {
|
||||
address: address.to_string(),
|
||||
alias: alias.map(String::from),
|
||||
added_at: chrono::Utc::now().timestamp(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, address: &str) {
|
||||
self.friends.retain(|f| f.address != address);
|
||||
}
|
||||
|
||||
/// Encrypt the friend list for server storage.
|
||||
/// Key is derived from the user's seed: HKDF(seed, info="warzone-friends").
|
||||
pub fn encrypt(&self, seed: &[u8; 32]) -> Vec<u8> {
|
||||
let key_bytes = hkdf_derive(seed, b"", b"warzone-friends", 32);
|
||||
let mut key = [0u8; 32];
|
||||
key.copy_from_slice(&key_bytes);
|
||||
let plaintext = serde_json::to_vec(self).unwrap_or_default();
|
||||
aead_encrypt(&key, &plaintext, b"warzone-friends-aad")
|
||||
}
|
||||
|
||||
/// Decrypt a friend list blob from the server.
|
||||
pub fn decrypt(seed: &[u8; 32], ciphertext: &[u8]) -> Result<Self, crate::errors::ProtocolError> {
|
||||
let key_bytes = hkdf_derive(seed, b"", b"warzone-friends", 32);
|
||||
let mut key = [0u8; 32];
|
||||
key.copy_from_slice(&key_bytes);
|
||||
let plaintext = aead_decrypt(&key, ciphertext, b"warzone-friends-aad")?;
|
||||
serde_json::from_slice(&plaintext)
|
||||
.map_err(|e| crate::errors::ProtocolError::RatchetError(format!("friend list json: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn encrypt_decrypt_roundtrip() {
|
||||
let seed = [42u8; 32];
|
||||
let mut list = FriendList::new();
|
||||
list.add("0x1234abcd", Some("alice"));
|
||||
list.add("0xdeadbeef", None);
|
||||
|
||||
let encrypted = list.encrypt(&seed);
|
||||
let decrypted = FriendList::decrypt(&seed, &encrypted).unwrap();
|
||||
|
||||
assert_eq!(decrypted.friends.len(), 2);
|
||||
assert_eq!(decrypted.friends[0].address, "0x1234abcd");
|
||||
assert_eq!(decrypted.friends[0].alias.as_deref(), Some("alice"));
|
||||
assert_eq!(decrypted.friends[1].address, "0xdeadbeef");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_seed_fails() {
|
||||
let seed = [42u8; 32];
|
||||
let wrong_seed = [99u8; 32];
|
||||
let mut list = FriendList::new();
|
||||
list.add("0x1234", None);
|
||||
|
||||
let encrypted = list.encrypt(&seed);
|
||||
assert!(FriendList::decrypt(&wrong_seed, &encrypted).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_add() {
|
||||
let mut list = FriendList::new();
|
||||
list.add("0x1234", None);
|
||||
list.add("0x1234", Some("alice"));
|
||||
assert_eq!(list.friends.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_works() {
|
||||
let mut list = FriendList::new();
|
||||
list.add("0x1234", None);
|
||||
list.add("0x5678", None);
|
||||
list.remove("0x1234");
|
||||
assert_eq!(list.friends.len(), 1);
|
||||
assert_eq!(list.friends[0].address, "0x5678");
|
||||
}
|
||||
}
|
||||
@@ -12,3 +12,4 @@ pub mod store;
|
||||
pub mod history;
|
||||
pub mod sender_keys;
|
||||
pub mod ethereum;
|
||||
pub mod friends;
|
||||
|
||||
Reference in New Issue
Block a user