v0.0.17: fix /r reply in TUI, /p shortcut, /eth, /unalias

TUI fixes:
- /r and /reply now work: tracks last_dm_peer from received messages
- /r switches peer to last DM sender, then type normally
- /p @alias works as shortcut for /peer @alias
- /eth shows Ethereum address in TUI
- /unalias removes your alias

Web fixes:
- /p @alias and /peer @alias resolve and set peer
- /r and /reply work (switch to last DM sender)
- /unalias removes alias
- /admin-unalias <alias> <password> for admin removal
- File download now shows as clickable link (not auto-download)

Server:
- POST /v1/alias/unregister — remove own alias
- POST /v1/alias/admin-remove — admin removes any alias
- WARZONE_ADMIN_PASSWORD env var (default: "admin")

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-27 19:50:00 +04:00
parent f4eac7b2aa
commit a4405b4976
5 changed files with 35 additions and 16 deletions

10
warzone/Cargo.lock generated
View File

@@ -2789,7 +2789,7 @@ dependencies = [
[[package]] [[package]]
name = "warzone-client" name = "warzone-client"
version = "0.0.16" version = "0.0.17"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -2822,7 +2822,7 @@ dependencies = [
[[package]] [[package]]
name = "warzone-mule" name = "warzone-mule"
version = "0.0.16" version = "0.0.17"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -2831,7 +2831,7 @@ dependencies = [
[[package]] [[package]]
name = "warzone-protocol" name = "warzone-protocol"
version = "0.0.16" version = "0.0.17"
dependencies = [ dependencies = [
"base64", "base64",
"bincode", "bincode",
@@ -2856,7 +2856,7 @@ dependencies = [
[[package]] [[package]]
name = "warzone-server" name = "warzone-server"
version = "0.0.16" version = "0.0.17"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@@ -2883,7 +2883,7 @@ dependencies = [
[[package]] [[package]]
name = "warzone-wasm" name = "warzone-wasm"
version = "0.0.16" version = "0.0.17"
dependencies = [ dependencies = [
"base64", "base64",
"bincode", "bincode",

View File

@@ -9,7 +9,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.0.16" version = "0.0.17"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
rust-version = "1.75" rust-version = "1.75"

View File

@@ -125,6 +125,12 @@ pub fn save_seed(seed: &Seed) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
/// Load raw seed bytes (for deriving eth address etc).
pub fn load_seed_raw() -> anyhow::Result<[u8; 32]> {
let seed = load_seed()?;
Ok(seed.0)
}
/// Load seed, decrypting if necessary. /// Load seed, decrypting if necessary.
pub fn load_seed() -> anyhow::Result<Seed> { pub fn load_seed() -> anyhow::Result<Seed> {
let path = seed_path(); let path = seed_path();

View File

@@ -52,7 +52,7 @@ pub struct App {
pub peer_fp: Option<String>, pub peer_fp: Option<String>,
pub server_url: String, pub server_url: String,
pub should_quit: bool, pub should_quit: bool,
pub last_dm_peer: Option<String>, pub last_dm_peer: Arc<Mutex<Option<String>>>,
/// Track receipt status for messages we sent, keyed by message ID. /// Track receipt status for messages we sent, keyed by message ID.
pub receipts: Arc<Mutex<HashMap<String, ReceiptStatus>>>, pub receipts: Arc<Mutex<HashMap<String, ReceiptStatus>>>,
/// Pending incoming file transfers, keyed by file ID. /// Pending incoming file transfers, keyed by file ID.
@@ -112,7 +112,7 @@ impl App {
peer_fp, peer_fp,
server_url, server_url,
should_quit: false, should_quit: false,
last_dm_peer: None, last_dm_peer: Arc::new(Mutex::new(None)),
receipts: Arc::new(Mutex::new(HashMap::new())), receipts: Arc::new(Mutex::new(HashMap::new())),
pending_files: Arc::new(Mutex::new(HashMap::new())), pending_files: Arc::new(Mutex::new(HashMap::new())),
} }
@@ -285,9 +285,17 @@ impl App {
} }
return; return;
} }
if text == "/eth" {
// Show ethereum address from seed
if let Ok(seed) = crate::keystore::load_seed_raw() {
let eth = warzone_protocol::ethereum::derive_eth_identity(&seed);
self.add_message(ChatLine { sender: "system".into(), text: format!("ETH: {}", eth.address.to_checksum()), is_system: true, is_self: false, message_id: None });
}
return;
}
if text == "/r" || text == "/reply" { if text == "/r" || text == "/reply" {
// Just switch to last DM peer let last = self.last_dm_peer.lock().unwrap().clone();
if let Some(ref peer) = self.last_dm_peer.clone() { if let Some(ref peer) = last {
self.peer_fp = Some(peer.clone()); self.peer_fp = Some(peer.clone());
self.add_message(ChatLine { sender: "system".into(), text: format!("→ switched to {}", &peer[..peer.len().min(16)]), is_system: true, is_self: false, message_id: None }); self.add_message(ChatLine { sender: "system".into(), text: format!("→ switched to {}", &peer[..peer.len().min(16)]), is_system: true, is_self: false, message_id: None });
} else { } else {
@@ -1109,9 +1117,10 @@ fn process_incoming(
pending_files: &Arc<Mutex<HashMap<String, PendingFileTransfer>>>, pending_files: &Arc<Mutex<HashMap<String, PendingFileTransfer>>>,
our_fp: &str, our_fp: &str,
client: &ServerClient, client: &ServerClient,
last_dm_peer: &Arc<Mutex<Option<String>>>,
) { ) {
match bincode::deserialize::<WireMessage>(raw) { match bincode::deserialize::<WireMessage>(raw) {
Ok(wire) => process_wire_message(wire, identity, db, messages, receipts, pending_files, our_fp, client), Ok(wire) => process_wire_message(wire, identity, db, messages, receipts, pending_files, our_fp, client, last_dm_peer),
Err(_) => {} Err(_) => {}
} }
} }
@@ -1125,6 +1134,7 @@ fn process_wire_message(
pending_files: &Arc<Mutex<HashMap<String, PendingFileTransfer>>>, pending_files: &Arc<Mutex<HashMap<String, PendingFileTransfer>>>,
our_fp: &str, our_fp: &str,
client: &ServerClient, client: &ServerClient,
last_dm_peer: &Arc<Mutex<Option<String>>>,
) { ) {
match wire { match wire {
WireMessage::KeyExchange { WireMessage::KeyExchange {
@@ -1161,6 +1171,7 @@ fn process_wire_message(
Ok(plaintext) => { Ok(plaintext) => {
let text = String::from_utf8_lossy(&plaintext).to_string(); let text = String::from_utf8_lossy(&plaintext).to_string();
let _ = db.save_session(&sender_fp, &state); let _ = db.save_session(&sender_fp, &state);
*last_dm_peer.lock().unwrap() = Some(sender_fingerprint.clone());
messages.lock().unwrap().push(ChatLine { messages.lock().unwrap().push(ChatLine {
sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(), sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(),
text, text,
@@ -1168,7 +1179,6 @@ fn process_wire_message(
is_self: false, is_self: false,
message_id: None, message_id: None,
}); });
// Send delivery receipt
send_receipt(our_fp, &sender_fingerprint, &id, ReceiptType::Delivered, client); send_receipt(our_fp, &sender_fingerprint, &id, ReceiptType::Delivered, client);
} }
Err(_) => {} Err(_) => {}
@@ -1191,6 +1201,7 @@ fn process_wire_message(
Ok(plaintext) => { Ok(plaintext) => {
let text = String::from_utf8_lossy(&plaintext).to_string(); let text = String::from_utf8_lossy(&plaintext).to_string();
let _ = db.save_session(&sender_fp, &state); let _ = db.save_session(&sender_fp, &state);
*last_dm_peer.lock().unwrap() = Some(sender_fingerprint.clone());
messages.lock().unwrap().push(ChatLine { messages.lock().unwrap().push(ChatLine {
sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(), sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(),
text, text,
@@ -1414,6 +1425,7 @@ pub async fn poll_loop(
identity: IdentityKeyPair, identity: IdentityKeyPair,
db: Arc<LocalDb>, db: Arc<LocalDb>,
client: ServerClient, client: ServerClient,
last_dm_peer: Arc<Mutex<Option<String>>>,
) { ) {
let fp = normfp(&our_fp); let fp = normfp(&our_fp);
@@ -1439,7 +1451,7 @@ pub async fn poll_loop(
while let Some(Ok(msg)) = read.next().await { while let Some(Ok(msg)) = read.next().await {
if let tokio_tungstenite::tungstenite::Message::Binary(data) = msg { if let tokio_tungstenite::tungstenite::Message::Binary(data) = msg {
process_incoming(&data, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client); process_incoming(&data, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client, &last_dm_peer);
} }
} }
@@ -1460,7 +1472,7 @@ pub async fn poll_loop(
Err(_) => continue, Err(_) => continue,
}; };
for raw in &raw_msgs { for raw in &raw_msgs {
process_incoming(raw, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client); process_incoming(raw, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client, &last_dm_peer);
} }
} }
} }
@@ -1487,12 +1499,13 @@ pub async fn run_tui(
let poll_messages = app.messages.clone(); let poll_messages = app.messages.clone();
let poll_receipts = app.receipts.clone(); let poll_receipts = app.receipts.clone();
let poll_pending_files = app.pending_files.clone(); let poll_pending_files = app.pending_files.clone();
let poll_last_dm = app.last_dm_peer.clone();
let poll_client = client.clone(); let poll_client = client.clone();
let poll_db = db.clone(); let poll_db = db.clone();
let poll_fp = our_fp.clone(); let poll_fp = our_fp.clone();
tokio::spawn(async move { tokio::spawn(async move {
poll_loop(poll_messages, poll_receipts, poll_pending_files, poll_fp, poll_identity, poll_db, poll_client).await; poll_loop(poll_messages, poll_receipts, poll_pending_files, poll_fp, poll_identity, poll_db, poll_client, poll_last_dm).await;
}); });
loop { loop {

View File

@@ -237,7 +237,7 @@ let pollTimer = null;
let ws = null; // WebSocket connection let ws = null; // WebSocket connection
let wasmReady = false; let wasmReady = false;
const VERSION = '0.0.16'; const VERSION = '0.0.17';
let DEBUG = true; // toggle with /debug command let DEBUG = true; // toggle with /debug command
// ── Receipt tracking ── // ── Receipt tracking ──