v0.0.23: ETH display everywhere, local build, web UX fixes
Version: 0.0.22 → 0.0.23, SW cache wz-v3 → wz-v4 TUI: - Own messages show ETH address (0x...) instead of fingerprint - Received messages: async ETH cache lookup (resolve on first sight) - /info shows Identity + Fingerprint - Welcome message shows ETH address Web: - Header shows only ETH address (single element, click to copy) - Own messages show ETH format - Received messages resolve sender ETH via /v1/resolve/ - /peer 0x... resolves via /v1/resolve/ endpoint - Click messages area → focuses text input Client: - register_bundle sends eth_address to server - ETH↔fingerprint mapping stored on registration Build: - --local: build on current machine (auto-detect apt/dnf/pacman/brew) - --local-ship: build locally + deploy to all servers - --local-clean: build + clean cargo cache Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.0.22"
|
version = "0.0.23"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
|
|||||||
@@ -85,8 +85,16 @@ pub async fn register_with_server_identity(
|
|||||||
.map_err(|_| anyhow::anyhow!("No bundle found. Run `warzone init` first."))?;
|
.map_err(|_| anyhow::anyhow!("No bundle found. Run `warzone init` first."))?;
|
||||||
let bundle: PreKeyBundle = bincode::deserialize(&bundle_bytes)?;
|
let bundle: PreKeyBundle = bincode::deserialize(&bundle_bytes)?;
|
||||||
|
|
||||||
|
// Derive ETH address from seed
|
||||||
|
let eth_address = crate::keystore::load_seed_raw()
|
||||||
|
.map(|seed| {
|
||||||
|
let eth = warzone_protocol::ethereum::derive_eth_identity(&seed);
|
||||||
|
eth.address.to_checksum()
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
let client = ServerClient::new(server_url);
|
let client = ServerClient::new(server_url);
|
||||||
client.register_bundle(&fp, &bundle).await?;
|
client.register_bundle(&fp, &bundle, eth_address).await?;
|
||||||
println!("Bundle registered with {}", server_url);
|
println!("Bundle registered with {}", server_url);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ pub struct ServerClient {
|
|||||||
struct RegisterRequest {
|
struct RegisterRequest {
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
bundle: Vec<u8>,
|
bundle: Vec<u8>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
eth_address: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -43,6 +45,7 @@ impl ServerClient {
|
|||||||
&self,
|
&self,
|
||||||
fingerprint: &str,
|
fingerprint: &str,
|
||||||
bundle: &PreKeyBundle,
|
bundle: &PreKeyBundle,
|
||||||
|
eth_address: Option<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let encoded =
|
let encoded =
|
||||||
bincode::serialize(bundle).context("failed to serialize bundle")?;
|
bincode::serialize(bundle).context("failed to serialize bundle")?;
|
||||||
@@ -51,6 +54,7 @@ impl ServerClient {
|
|||||||
.json(&RegisterRequest {
|
.json(&RegisterRequest {
|
||||||
fingerprint: fingerprint.to_string(),
|
fingerprint: fingerprint.to_string(),
|
||||||
bundle: encoded,
|
bundle: encoded,
|
||||||
|
eth_address,
|
||||||
})
|
})
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -34,13 +34,10 @@ impl App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if text == "/info" {
|
if text == "/info" {
|
||||||
self.add_message(ChatLine {
|
if !self.our_eth.is_empty() {
|
||||||
sender: "system".into(),
|
self.add_message(ChatLine { sender: "system".into(), text: format!("Identity: {}", self.our_eth), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
|
||||||
text: format!("Your fingerprint: {}", self.our_fp),
|
}
|
||||||
is_system: true,
|
self.add_message(ChatLine { sender: "system".into(), text: format!("Fingerprint: {}", self.our_fp), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
|
||||||
is_self: false,
|
|
||||||
message_id: None, timestamp: Local::now(),
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if text == "/help" || text == "/?" {
|
if text == "/help" || text == "/?" {
|
||||||
@@ -634,7 +631,7 @@ impl App {
|
|||||||
let _ = db.touch_contact(&peer, None);
|
let _ = db.touch_contact(&peer, None);
|
||||||
let _ = db.store_message(&peer, &self.our_fp, &text, true);
|
let _ = db.store_message(&peer, &self.our_fp, &text, true);
|
||||||
self.add_message(ChatLine {
|
self.add_message(ChatLine {
|
||||||
sender: self.our_fp[..12].to_string(),
|
sender: if self.our_eth.is_empty() { self.our_fp[..12].to_string() } else { format!("{}...", &self.our_eth[..self.our_eth.len().min(12)]) },
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
is_system: false,
|
is_system: false,
|
||||||
is_self: true,
|
is_self: true,
|
||||||
@@ -879,7 +876,7 @@ impl App {
|
|||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.add_message(ChatLine {
|
self.add_message(ChatLine {
|
||||||
sender: format!("{} [#{}]", &self.our_fp[..12], group_name),
|
sender: format!("{} [#{}]", if self.our_eth.is_empty() { &self.our_fp[..12] } else { &self.our_eth[..self.our_eth.len().min(12)] }, group_name),
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
is_system: false,
|
is_system: false,
|
||||||
is_self: true,
|
is_self: true,
|
||||||
|
|||||||
@@ -43,6 +43,38 @@ fn send_receipt(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ETH address cache: fingerprint → ETH address (populated async, read sync).
|
||||||
|
pub type EthCache = Arc<std::sync::Mutex<HashMap<String, String>>>;
|
||||||
|
|
||||||
|
/// Display a fingerprint as short ETH address if cached, otherwise truncated fingerprint.
|
||||||
|
fn display_sender(fp: &str, eth_cache: &EthCache) -> String {
|
||||||
|
let cache = eth_cache.lock().unwrap();
|
||||||
|
if let Some(eth) = cache.get(fp) {
|
||||||
|
format!("{}...", ð[..eth.len().min(12)])
|
||||||
|
} else {
|
||||||
|
fp[..fp.len().min(12)].to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Async: look up ETH address for a fingerprint and cache it.
|
||||||
|
fn cache_eth_lookup(fp: &str, client: &ServerClient, eth_cache: &EthCache) {
|
||||||
|
let fp = fp.to_string();
|
||||||
|
let client = client.clone();
|
||||||
|
let cache = eth_cache.clone();
|
||||||
|
// Check if already cached
|
||||||
|
if cache.lock().unwrap().contains_key(&fp) { return; }
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let url = format!("{}/v1/resolve/{}", client.base_url, fp);
|
||||||
|
if let Ok(resp) = client.client.get(&url).send().await {
|
||||||
|
if let Ok(data) = resp.json::<serde_json::Value>().await {
|
||||||
|
if let Some(eth) = data.get("eth_address").and_then(|v| v.as_str()) {
|
||||||
|
cache.lock().unwrap().insert(fp, eth.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn store_received(db: &LocalDb, sender_fp: &str, text: &str) {
|
fn store_received(db: &LocalDb, sender_fp: &str, text: &str) {
|
||||||
let _ = db.touch_contact(sender_fp, None);
|
let _ = db.touch_contact(sender_fp, None);
|
||||||
let _ = db.store_message(sender_fp, sender_fp, text, false);
|
let _ = db.store_message(sender_fp, sender_fp, text, false);
|
||||||
@@ -58,10 +90,11 @@ pub 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,
|
||||||
|
eth_cache: &EthCache,
|
||||||
last_dm_peer: &Arc<Mutex<Option<String>>>,
|
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, last_dm_peer),
|
Ok(wire) => process_wire_message(wire, identity, db, messages, receipts, pending_files, our_fp, client, last_dm_peer, eth_cache),
|
||||||
Err(_) => {}
|
Err(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,6 +109,7 @@ fn process_wire_message(
|
|||||||
our_fp: &str,
|
our_fp: &str,
|
||||||
client: &ServerClient,
|
client: &ServerClient,
|
||||||
last_dm_peer: &Arc<Mutex<Option<String>>>,
|
last_dm_peer: &Arc<Mutex<Option<String>>>,
|
||||||
|
eth_cache: &EthCache,
|
||||||
) {
|
) {
|
||||||
match wire {
|
match wire {
|
||||||
WireMessage::KeyExchange {
|
WireMessage::KeyExchange {
|
||||||
@@ -117,7 +151,7 @@ fn process_wire_message(
|
|||||||
}
|
}
|
||||||
store_received(db, &sender_fingerprint, &text);
|
store_received(db, &sender_fingerprint, &text);
|
||||||
messages.lock().unwrap().push(ChatLine {
|
messages.lock().unwrap().push(ChatLine {
|
||||||
sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(),
|
sender: { cache_eth_lookup(&sender_fingerprint, client, eth_cache); display_sender(&sender_fingerprint, eth_cache) },
|
||||||
text,
|
text,
|
||||||
is_system: false,
|
is_system: false,
|
||||||
is_self: false,
|
is_self: false,
|
||||||
@@ -166,7 +200,7 @@ fn process_wire_message(
|
|||||||
}
|
}
|
||||||
store_received(db, &sender_fingerprint, &text);
|
store_received(db, &sender_fingerprint, &text);
|
||||||
messages.lock().unwrap().push(ChatLine {
|
messages.lock().unwrap().push(ChatLine {
|
||||||
sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(),
|
sender: { cache_eth_lookup(&sender_fingerprint, client, eth_cache); display_sender(&sender_fingerprint, eth_cache) },
|
||||||
text,
|
text,
|
||||||
is_system: false,
|
is_system: false,
|
||||||
is_self: false,
|
is_self: false,
|
||||||
@@ -464,7 +498,7 @@ fn process_wire_message(
|
|||||||
} => {
|
} => {
|
||||||
let type_str = format!("{:?}", signal_type);
|
let type_str = format!("{:?}", signal_type);
|
||||||
messages.lock().unwrap().push(ChatLine {
|
messages.lock().unwrap().push(ChatLine {
|
||||||
sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(),
|
sender: { cache_eth_lookup(&sender_fingerprint, client, eth_cache); display_sender(&sender_fingerprint, eth_cache) },
|
||||||
text: format!("\u{1f4de} Call signal: {}", type_str),
|
text: format!("\u{1f4de} Call signal: {}", type_str),
|
||||||
is_system: false,
|
is_system: false,
|
||||||
is_self: false,
|
is_self: false,
|
||||||
@@ -487,6 +521,7 @@ pub async fn poll_loop(
|
|||||||
connected: Arc<AtomicBool>,
|
connected: Arc<AtomicBool>,
|
||||||
) {
|
) {
|
||||||
let fp = normfp(&our_fp);
|
let fp = normfp(&our_fp);
|
||||||
|
let eth_cache: EthCache = Arc::new(std::sync::Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
// Try WebSocket first
|
// Try WebSocket first
|
||||||
let ws_url = client.base_url
|
let ws_url = client.base_url
|
||||||
@@ -511,7 +546,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, &last_dm_peer);
|
process_incoming(&data, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client, ð_cache, &last_dm_peer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,7 +569,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, &last_dm_peer);
|
process_incoming(raw, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client, ð_cache, &last_dm_peer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,9 +63,19 @@ pub struct ChatLine {
|
|||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(our_fp: String, peer_fp: Option<String>, server_url: String) -> Self {
|
pub fn new(our_fp: String, peer_fp: Option<String>, server_url: String) -> Self {
|
||||||
|
// Derive ETH address from seed first (used in welcome messages)
|
||||||
|
let our_eth = crate::keystore::load_seed_raw()
|
||||||
|
.map(|seed| {
|
||||||
|
let eth = warzone_protocol::ethereum::derive_eth_identity(&seed);
|
||||||
|
eth.address.to_checksum()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let identity_display = if our_eth.is_empty() { our_fp.clone() } else { our_eth.clone() };
|
||||||
|
|
||||||
let messages = Arc::new(Mutex::new(vec![ChatLine {
|
let messages = Arc::new(Mutex::new(vec![ChatLine {
|
||||||
sender: "system".into(),
|
sender: "system".into(),
|
||||||
text: format!("You are {}", our_fp),
|
text: format!("You are {}", identity_display),
|
||||||
is_system: true,
|
is_system: true,
|
||||||
is_self: false,
|
is_self: false,
|
||||||
message_id: None,
|
message_id: None,
|
||||||
@@ -101,14 +111,6 @@ impl App {
|
|||||||
timestamp: Local::now(),
|
timestamp: Local::now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Derive ETH address from seed if available
|
|
||||||
let our_eth = crate::keystore::load_seed_raw()
|
|
||||||
.map(|seed| {
|
|
||||||
let eth = warzone_protocol::ethereum::derive_eth_identity(&seed);
|
|
||||||
eth.address.to_checksum()
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
App {
|
App {
|
||||||
input: String::new(),
|
input: String::new(),
|
||||||
messages,
|
messages,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "warzone-protocol"
|
name = "warzone-protocol"
|
||||||
version = "0.0.22"
|
version = "0.0.23"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Core crypto & wire protocol for featherChat (Warzone messenger)"
|
description = "Core crypto & wire protocol for featherChat (Warzone messenger)"
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async fn pwa_manifest() -> impl IntoResponse {
|
|||||||
|
|
||||||
async fn service_worker() -> impl IntoResponse {
|
async fn service_worker() -> impl IntoResponse {
|
||||||
([(header::CONTENT_TYPE, "application/javascript")], r##"
|
([(header::CONTENT_TYPE, "application/javascript")], r##"
|
||||||
const CACHE = 'wz-v3';
|
const CACHE = 'wz-v4';
|
||||||
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
|
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
|
||||||
|
|
||||||
self.addEventListener('install', e => {
|
self.addEventListener('install', e => {
|
||||||
@@ -209,8 +209,8 @@ const WEB_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
<!-- Chat screen -->
|
<!-- Chat screen -->
|
||||||
<div id="chat" class="screen">
|
<div id="chat" class="screen">
|
||||||
<div id="chat-header">
|
<div id="chat-header">
|
||||||
<span class="tag tag-fp" id="hdr-fp"></span>
|
<span class="tag tag-fp" id="hdr-fp" style="cursor:pointer" title="Click to copy"></span>
|
||||||
<span class="tag" id="hdr-eth" style="background:#1a1a3e;color:#4fc3f7;font-size:0.8em;cursor:pointer" title=""></span>
|
<span id="hdr-eth" style="display:none"></span>
|
||||||
<span>→</span>
|
<span>→</span>
|
||||||
<input id="peer-input" placeholder="Paste peer fingerprint..." autocomplete="off">
|
<input id="peer-input" placeholder="Paste peer fingerprint..." autocomplete="off">
|
||||||
<span class="tag-server" id="hdr-server"></span>
|
<span class="tag-server" id="hdr-server"></span>
|
||||||
@@ -242,7 +242,7 @@ let pollTimer = null;
|
|||||||
let ws = null; // WebSocket connection
|
let ws = null; // WebSocket connection
|
||||||
let wasmReady = false;
|
let wasmReady = false;
|
||||||
|
|
||||||
const VERSION = '0.0.22';
|
const VERSION = '0.0.23';
|
||||||
let DEBUG = true; // toggle with /debug command
|
let DEBUG = true; // toggle with /debug command
|
||||||
|
|
||||||
// ── Receipt tracking ──
|
// ── Receipt tracking ──
|
||||||
@@ -616,13 +616,16 @@ async function handleIncomingMessage(bytes) {
|
|||||||
|
|
||||||
let fromLabel = result.sender.slice(0, 19);
|
let fromLabel = result.sender.slice(0, 19);
|
||||||
try {
|
try {
|
||||||
const ar = await fetch(SERVER + '/v1/alias/whois/' + senderFP);
|
const ar = await fetch(SERVER + '/v1/resolve/' + senderFP);
|
||||||
const ad = await ar.json();
|
const ad = await ar.json();
|
||||||
if (ad.alias) fromLabel = '@' + ad.alias;
|
if (ad.eth_address) fromLabel = ad.eth_address.slice(0, 12) + '...';
|
||||||
|
// Alias overrides ETH
|
||||||
|
const aw = await fetch(SERVER + '/v1/alias/whois/' + senderFP);
|
||||||
|
const adata = await aw.json();
|
||||||
|
if (adata.alias) fromLabel = '@' + adata.alias;
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
|
|
||||||
addMsg(fromLabel, result.text, false);
|
addMsg(fromLabel, result.text, false);
|
||||||
// Send delivery receipt
|
|
||||||
if (result.message_id) sendReceipt(result.sender, result.message_id, 'delivered');
|
if (result.message_id) sendReceipt(result.sender, result.message_id, 'delivered');
|
||||||
lastDmPeer = normFP(result.sender);
|
lastDmPeer = normFP(result.sender);
|
||||||
return;
|
return;
|
||||||
@@ -653,13 +656,16 @@ async function handleIncomingMessage(bytes) {
|
|||||||
|
|
||||||
let fromLabel = result.sender.slice(0, 19);
|
let fromLabel = result.sender.slice(0, 19);
|
||||||
try {
|
try {
|
||||||
const ar = await fetch(SERVER + '/v1/alias/whois/' + normFP(result.sender));
|
const rfp = normFP(result.sender);
|
||||||
|
const ar = await fetch(SERVER + '/v1/resolve/' + rfp);
|
||||||
const ad = await ar.json();
|
const ad = await ar.json();
|
||||||
if (ad.alias) fromLabel = '@' + ad.alias;
|
if (ad.eth_address) fromLabel = ad.eth_address.slice(0, 12) + '...';
|
||||||
|
const aw = await fetch(SERVER + '/v1/alias/whois/' + rfp);
|
||||||
|
const adata = await aw.json();
|
||||||
|
if (adata.alias) fromLabel = '@' + adata.alias;
|
||||||
} catch(e2) {}
|
} catch(e2) {}
|
||||||
|
|
||||||
addMsg(fromLabel, result.text, false);
|
addMsg(fromLabel, result.text, false);
|
||||||
// Send delivery receipt
|
|
||||||
if (result.message_id) sendReceipt(result.sender, result.message_id, 'delivered');
|
if (result.message_id) sendReceipt(result.sender, result.message_id, 'delivered');
|
||||||
lastDmPeer = normFP(result.sender);
|
lastDmPeer = normFP(result.sender);
|
||||||
return;
|
return;
|
||||||
@@ -859,18 +865,21 @@ let pendingFiles = {}; // file_id -> { filename, chunks: [], total, received,
|
|||||||
async function enterChat() {
|
async function enterChat() {
|
||||||
document.getElementById('setup').classList.remove('active');
|
document.getElementById('setup').classList.remove('active');
|
||||||
document.getElementById('chat').classList.add('active');
|
document.getElementById('chat').classList.add('active');
|
||||||
document.getElementById('hdr-fp').textContent = myFingerprint.slice(0, 19);
|
|
||||||
document.getElementById('hdr-server').textContent = SERVER;
|
document.getElementById('hdr-server').textContent = SERVER;
|
||||||
|
|
||||||
await registerKey();
|
await registerKey();
|
||||||
addSys('Identity: ' + myEthAddress);
|
// Show ETH in header, fallback to fingerprint
|
||||||
addSys('Fingerprint: ' + myFingerprint);
|
const hdrFp = document.getElementById('hdr-fp');
|
||||||
addSys('Key registered with server');
|
|
||||||
|
|
||||||
if (myEthAddress) {
|
if (myEthAddress) {
|
||||||
document.getElementById('hdr-eth').textContent = myEthAddress.slice(0, 10) + '...';
|
hdrFp.textContent = myEthAddress.slice(0, 12) + '...';
|
||||||
document.getElementById('hdr-eth').title = myEthAddress;
|
hdrFp.title = myEthAddress;
|
||||||
|
hdrFp.onclick = function() { navigator.clipboard.writeText(myEthAddress); addSys('Copied: ' + myEthAddress); };
|
||||||
|
} else {
|
||||||
|
hdrFp.textContent = (myEthAddress ? myEthAddress.slice(0,12) + '...' : myFingerprint.slice(0,19));
|
||||||
|
hdrFp.title = myFingerprint;
|
||||||
}
|
}
|
||||||
|
addSys('Identity: ' + (myEthAddress || myFingerprint));
|
||||||
|
addSys('Key registered with server');
|
||||||
|
|
||||||
addSys('v' + VERSION + ' | DM: paste peer fingerprint or @alias above');
|
addSys('v' + VERSION + ' | DM: paste peer fingerprint or @alias above');
|
||||||
addSys('/alias · /g · /gleave · /gkick · /gmembers · /glist · /friend · /file · /info');
|
addSys('/alias · /g · /gleave · /gkick · /gmembers · /glist · /friend · /file · /info');
|
||||||
@@ -986,7 +995,7 @@ async function sendToGroup(groupName, text) {
|
|||||||
body: JSON.stringify({ from: myFP, messages })
|
body: JSON.stringify({ from: myFP, messages })
|
||||||
});
|
});
|
||||||
|
|
||||||
addMsg(myFingerprint.slice(0, 19) + ' [' + groupName + ']', text, true, null);
|
addMsg((myEthAddress ? myEthAddress.slice(0,12) + '...' : myFingerprint.slice(0,19)) + ' [' + groupName + ']', text, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Send handler ──
|
// ── Send handler ──
|
||||||
@@ -1092,18 +1101,19 @@ async function doSend() {
|
|||||||
$peerInput.value = lastDmPeer;
|
$peerInput.value = lastDmPeer;
|
||||||
try {
|
try {
|
||||||
await sendEncrypted(lastDmPeer, replyText.trim());
|
await sendEncrypted(lastDmPeer, replyText.trim());
|
||||||
addMsg(myFingerprint.slice(0, 19), replyText.trim(), true);
|
addMsg((myEthAddress ? myEthAddress.slice(0,12) + '...' : myFingerprint.slice(0,19)), replyText.trim(), true);
|
||||||
} catch(e) { addSys('Reply failed: ' + e.message); }
|
} catch(e) { addSys('Reply failed: ' + e.message); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (text.startsWith('/p ') || text.startsWith('/peer ')) {
|
if (text.startsWith('/p ') || text.startsWith('/peer ')) {
|
||||||
let val = text.startsWith('/p ') ? text.slice(3).trim() : text.slice(6).trim();
|
let val = text.startsWith('/p ') ? text.slice(3).trim() : text.slice(6).trim();
|
||||||
if (val.startsWith('@')) {
|
if (val.startsWith('@') || val.startsWith('0x') || val.startsWith('0X')) {
|
||||||
const resp = await fetch(SERVER + '/v1/alias/resolve/' + val.slice(1));
|
const endpoint = val.startsWith('@') ? '/v1/alias/resolve/' + val.slice(1) : '/v1/resolve/' + val;
|
||||||
|
const resp = await fetch(SERVER + endpoint);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.error) { addSys('Unknown alias ' + val); return; }
|
if (data.error) { addSys('Cannot resolve ' + val + ': ' + data.error); return; }
|
||||||
$peerInput.value = data.fingerprint;
|
$peerInput.value = data.fingerprint;
|
||||||
addSys(val + ' → ' + data.fingerprint.slice(0,16) + '...');
|
addSys(val + ' \u2192 ' + data.fingerprint.slice(0,16) + '...');
|
||||||
} else {
|
} else {
|
||||||
$peerInput.value = val;
|
$peerInput.value = val;
|
||||||
}
|
}
|
||||||
@@ -1197,7 +1207,7 @@ async function doSend() {
|
|||||||
try {
|
try {
|
||||||
const msgId = await sendEncrypted(peer, text);
|
const msgId = await sendEncrypted(peer, text);
|
||||||
sentMsgReceipts[msgId] = { status: 'sent', el: null };
|
sentMsgReceipts[msgId] = { status: 'sent', el: null };
|
||||||
addMsg(myFingerprint.slice(0, 19), text, true, msgId);
|
addMsg((myEthAddress ? myEthAddress.slice(0,12) + '...' : myFingerprint.slice(0,19)), text, true, msgId);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
addSys('Send failed: ' + e.message);
|
addSys('Send failed: ' + e.message);
|
||||||
}
|
}
|
||||||
@@ -1218,6 +1228,7 @@ document.getElementById('btn-show-recover').onclick = () => document.getElementB
|
|||||||
document.getElementById('btn-recover').onclick = () => doRecover();
|
document.getElementById('btn-recover').onclick = () => doRecover();
|
||||||
document.getElementById('btn-enter').onclick = () => enterChat();
|
document.getElementById('btn-enter').onclick = () => enterChat();
|
||||||
document.getElementById('send-btn').onclick = () => doSend();
|
document.getElementById('send-btn').onclick = () => doSend();
|
||||||
|
document.getElementById('messages').onclick = () => document.getElementById('msg-input').focus();
|
||||||
document.getElementById('hdr-eth').onclick = function() {
|
document.getElementById('hdr-eth').onclick = function() {
|
||||||
if (myEthAddress) navigator.clipboard.writeText(myEthAddress).then(() => addSys('Copied ETH address'));
|
if (myEthAddress) navigator.clipboard.writeText(myEthAddress).then(() => addSys('Copied ETH address'));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -384,6 +384,108 @@ do_logs() {
|
|||||||
ssh "$host" "journalctl -u $PROD_SERVICE -f --no-pager"
|
ssh "$host" "journalctl -u $PROD_SERVICE -f --no-pager"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# --local: Build locally on this machine (auto-detect package manager)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
detect_pkg_manager() {
|
||||||
|
if command -v apt-get &>/dev/null; then echo "apt"
|
||||||
|
elif command -v dnf &>/dev/null; then echo "dnf"
|
||||||
|
elif command -v pacman &>/dev/null; then echo "pacman"
|
||||||
|
elif command -v brew &>/dev/null; then echo "brew"
|
||||||
|
else echo "unknown"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
do_local_deps() {
|
||||||
|
local pm
|
||||||
|
pm=$(detect_pkg_manager)
|
||||||
|
echo "[1/4] Installing dependencies ($pm)..."
|
||||||
|
|
||||||
|
case "$pm" in
|
||||||
|
apt)
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq build-essential pkg-config libssl-dev curl >/dev/null 2>&1
|
||||||
|
;;
|
||||||
|
dnf)
|
||||||
|
sudo dnf install -y gcc gcc-c++ make pkg-config openssl-devel curl >/dev/null 2>&1
|
||||||
|
;;
|
||||||
|
pacman)
|
||||||
|
sudo pacman -Sy --noconfirm base-devel pkg-config openssl curl >/dev/null 2>&1
|
||||||
|
;;
|
||||||
|
brew)
|
||||||
|
brew install openssl pkg-config 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "WARNING: Unknown package manager. Ensure build-essential, pkg-config, libssl-dev are installed."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Ensure Rust is installed
|
||||||
|
if ! command -v cargo &>/dev/null; then
|
||||||
|
echo " Installing Rust..."
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||||
|
source "$HOME/.cargo/env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure wasm-pack is installed
|
||||||
|
if ! command -v wasm-pack &>/dev/null; then
|
||||||
|
echo " Installing wasm-pack..."
|
||||||
|
cargo install wasm-pack 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure wasm target
|
||||||
|
rustup target add wasm32-unknown-unknown 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
do_local_build() {
|
||||||
|
local arch
|
||||||
|
arch=$(uname -m)
|
||||||
|
local os
|
||||||
|
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
local out_dir="target/${os}-${arch}"
|
||||||
|
|
||||||
|
echo "=== Local Build (${os}-${arch}) ==="
|
||||||
|
|
||||||
|
do_local_deps
|
||||||
|
|
||||||
|
echo "[2/4] Building WASM..."
|
||||||
|
wasm-pack build crates/warzone-wasm --target web --out-dir ../../wasm-pkg 2>&1 | tail -3
|
||||||
|
|
||||||
|
echo "[3/4] Building release binaries..."
|
||||||
|
cargo build --release --bin warzone-server --bin warzone-client 2>&1
|
||||||
|
|
||||||
|
echo "[4/4] Copying to ${out_dir}..."
|
||||||
|
mkdir -p "$out_dir"
|
||||||
|
cp target/release/warzone-server target/release/warzone-client "$out_dir/"
|
||||||
|
cp federation.example.json "$out_dir/" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Clean cargo cache if requested
|
||||||
|
if [ "${CLEAN_CACHE:-}" = "1" ]; then
|
||||||
|
echo " Cleaning build cache..."
|
||||||
|
cargo clean 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Local Build Complete ==="
|
||||||
|
ls -lh "$out_dir"/warzone-*
|
||||||
|
echo ""
|
||||||
|
echo "Run:"
|
||||||
|
echo " $out_dir/warzone-server --bind 0.0.0.0:7700"
|
||||||
|
echo " $out_dir/warzone-client tui --server http://localhost:7700"
|
||||||
|
}
|
||||||
|
|
||||||
|
do_local_ship() {
|
||||||
|
do_local_build
|
||||||
|
echo ""
|
||||||
|
do_update_all
|
||||||
|
echo ""
|
||||||
|
do_status
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo " LOCAL SHIP COMPLETE"
|
||||||
|
echo "========================================"
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# --ship: Build + deploy to all servers + destroy VM (full pipeline)
|
# --ship: Build + deploy to all servers + destroy VM (full pipeline)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -453,18 +555,30 @@ case "${1:-}" in
|
|||||||
--ship)
|
--ship)
|
||||||
do_ship
|
do_ship
|
||||||
;;
|
;;
|
||||||
|
--local)
|
||||||
|
do_local_build
|
||||||
|
;;
|
||||||
|
--local-ship)
|
||||||
|
do_local_ship
|
||||||
|
;;
|
||||||
|
--local-clean)
|
||||||
|
CLEAN_CACHE=1 do_local_build
|
||||||
|
;;
|
||||||
--upload)
|
--upload)
|
||||||
do_upload
|
do_upload
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: $0 <command> [args]"
|
echo "Usage: $0 <command> [args]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "One command:"
|
echo "Local build:"
|
||||||
echo " --ship Build + deploy to all servers + destroy VM"
|
echo " --local Build locally (auto-detect OS, install deps)"
|
||||||
|
echo " --local-ship Build locally + deploy to all servers"
|
||||||
|
echo " --local-clean Build locally + clean cargo cache after"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Build (Hetzner VM):"
|
echo "Remote build (Hetzner VM):"
|
||||||
|
echo " --ship Build on VM + deploy + destroy VM"
|
||||||
echo " --prepare Create VM, install deps, upload source"
|
echo " --prepare Create VM, install deps, upload source"
|
||||||
echo " --build Build release binaries"
|
echo " --build Build release binaries on VM"
|
||||||
echo " --transfer Download binaries to $OUTPUT_DIR"
|
echo " --transfer Download binaries to $OUTPUT_DIR"
|
||||||
echo " --destroy Delete the build VM"
|
echo " --destroy Delete the build VM"
|
||||||
echo " --all prepare + build + transfer (VM persists)"
|
echo " --all prepare + build + transfer (VM persists)"
|
||||||
|
|||||||
Reference in New Issue
Block a user