v0.0.15: unalias, admin alias removal, /reply, web version fix
Aliases: - /unalias — remove your own alias - /admin-unalias <alias> <password> — admin removes any alias - Admin password via WARZONE_ADMIN_PASSWORD env var (default: "admin") - POST /v1/alias/unregister + POST /v1/alias/admin-remove Reply: - /r or /reply — switches peer to whoever last DM'd you - lastDmPeer tracked on both web and TUI - Then type normally to reply Web: - Version bumped to 0.0.15 (was stuck at 0.0.10) - WASM rebuilt with latest protocol Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
10
warzone/Cargo.lock
generated
10
warzone/Cargo.lock
generated
@@ -2789,7 +2789,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-client"
|
name = "warzone-client"
|
||||||
version = "0.0.14"
|
version = "0.0.15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -2822,7 +2822,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-mule"
|
name = "warzone-mule"
|
||||||
version = "0.0.14"
|
version = "0.0.15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -2831,7 +2831,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-protocol"
|
name = "warzone-protocol"
|
||||||
version = "0.0.14"
|
version = "0.0.15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bincode",
|
"bincode",
|
||||||
@@ -2856,7 +2856,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-server"
|
name = "warzone-server"
|
||||||
version = "0.0.14"
|
version = "0.0.15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2883,7 +2883,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-wasm"
|
name = "warzone-wasm"
|
||||||
version = "0.0.14"
|
version = "0.0.15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.0.14"
|
version = "0.0.15"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
|
|||||||
@@ -52,6 +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>,
|
||||||
/// 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.
|
||||||
@@ -111,6 +112,7 @@ impl App {
|
|||||||
peer_fp,
|
peer_fp,
|
||||||
server_url,
|
server_url,
|
||||||
should_quit: false,
|
should_quit: false,
|
||||||
|
last_dm_peer: 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())),
|
||||||
}
|
}
|
||||||
@@ -266,6 +268,33 @@ impl App {
|
|||||||
self.list_aliases(client).await;
|
self.list_aliases(client).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if text == "/unalias" {
|
||||||
|
let url = format!("{}/v1/alias/unregister", client.base_url);
|
||||||
|
match client.client.post(&url)
|
||||||
|
.json(&serde_json::json!({"fingerprint": normfp(&self.our_fp)}))
|
||||||
|
.send().await
|
||||||
|
{
|
||||||
|
Ok(resp) => if let Ok(data) = resp.json::<serde_json::Value>().await {
|
||||||
|
if let Some(err) = data.get("error") {
|
||||||
|
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false, message_id: None });
|
||||||
|
} else {
|
||||||
|
self.add_message(ChatLine { sender: "system".into(), text: "Alias removed".into(), is_system: true, is_self: false, message_id: None });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None }),
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if text == "/r" || text == "/reply" {
|
||||||
|
// Just switch to last DM peer
|
||||||
|
if let Some(ref peer) = self.last_dm_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 });
|
||||||
|
} else {
|
||||||
|
self.add_message(ChatLine { sender: "system".into(), text: "No one to reply to".into(), is_system: true, is_self: false, message_id: None });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if text.starts_with("/peer ") {
|
if text.starts_with("/peer ") {
|
||||||
let raw = text[6..].trim().to_string();
|
let raw = text[6..].trim().to_string();
|
||||||
let fp = if raw.starts_with('@') {
|
let fp = if raw.starts_with('@') {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ pub fn routes() -> Router<AppState> {
|
|||||||
.route("/alias/resolve/:name", get(resolve_alias))
|
.route("/alias/resolve/:name", get(resolve_alias))
|
||||||
.route("/alias/list", get(list_aliases))
|
.route("/alias/list", get(list_aliases))
|
||||||
.route("/alias/whois/:fingerprint", get(reverse_lookup))
|
.route("/alias/whois/:fingerprint", get(reverse_lookup))
|
||||||
|
.route("/alias/unregister", post(unregister_alias))
|
||||||
|
.route("/alias/admin-remove", post(admin_remove_alias))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_fp(fp: &str) -> String {
|
fn normalize_fp(fp: &str) -> String {
|
||||||
@@ -337,3 +339,61 @@ async fn list_aliases(
|
|||||||
|
|
||||||
Ok(Json(serde_json::json!({ "aliases": aliases, "count": aliases.len() })))
|
Ok(Json(serde_json::json!({ "aliases": aliases, "count": aliases.len() })))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct UnregisterRequest {
|
||||||
|
fingerprint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove your own alias.
|
||||||
|
async fn unregister_alias(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<UnregisterRequest>,
|
||||||
|
) -> AppResult<Json<serde_json::Value>> {
|
||||||
|
let fp = normalize_fp(&req.fingerprint);
|
||||||
|
|
||||||
|
let alias = match state.db.aliases.get(format!("fp:{}", fp).as_bytes())? {
|
||||||
|
Some(data) => String::from_utf8_lossy(&data).to_string(),
|
||||||
|
None => return Ok(Json(serde_json::json!({ "error": "no alias registered" }))),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(record) = load_alias_record(&state.db.aliases, &alias) {
|
||||||
|
if record.fingerprint != fp {
|
||||||
|
return Ok(Json(serde_json::json!({ "error": "not your alias" })));
|
||||||
|
}
|
||||||
|
delete_alias_record(&state.db.aliases, &record)?;
|
||||||
|
tracing::info!("Alias '{}' unregistered by {}", alias, fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(serde_json::json!({ "ok": true, "removed": alias })))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Admin password (set via WARZONE_ADMIN_PASSWORD env var, defaults to "admin").
|
||||||
|
fn admin_password() -> String {
|
||||||
|
std::env::var("WARZONE_ADMIN_PASSWORD").unwrap_or_else(|_| "admin".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct AdminRemoveRequest {
|
||||||
|
alias: String,
|
||||||
|
admin_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Admin: remove any alias.
|
||||||
|
async fn admin_remove_alias(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<AdminRemoveRequest>,
|
||||||
|
) -> AppResult<Json<serde_json::Value>> {
|
||||||
|
if req.admin_password != admin_password() {
|
||||||
|
return Ok(Json(serde_json::json!({ "error": "invalid admin password" })));
|
||||||
|
}
|
||||||
|
|
||||||
|
let alias = normalize_alias(&req.alias);
|
||||||
|
if let Some(record) = load_alias_record(&state.db.aliases, &alias) {
|
||||||
|
delete_alias_record(&state.db.aliases, &record)?;
|
||||||
|
tracing::info!("Alias '{}' removed by admin", alias);
|
||||||
|
Ok(Json(serde_json::json!({ "ok": true, "removed": alias })))
|
||||||
|
} else {
|
||||||
|
Ok(Json(serde_json::json!({ "error": "alias not found" })))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ let pollTimer = null;
|
|||||||
let ws = null; // WebSocket connection
|
let ws = null; // WebSocket connection
|
||||||
let wasmReady = false;
|
let wasmReady = false;
|
||||||
|
|
||||||
const VERSION = '0.0.10';
|
const VERSION = '0.0.15';
|
||||||
let DEBUG = true; // toggle with /debug command
|
let DEBUG = true; // toggle with /debug command
|
||||||
|
|
||||||
// ── Receipt tracking ──
|
// ── Receipt tracking ──
|
||||||
@@ -505,6 +505,7 @@ async function handleIncomingMessage(bytes) {
|
|||||||
addMsg(fromLabel, result.text, false);
|
addMsg(fromLabel, result.text, false);
|
||||||
// Send delivery receipt
|
// 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);
|
||||||
return;
|
return;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
dbg('KeyExchange/Receipt parse failed:', e.message || e);
|
dbg('KeyExchange/Receipt parse failed:', e.message || e);
|
||||||
@@ -539,6 +540,7 @@ async function handleIncomingMessage(bytes) {
|
|||||||
addMsg(fromLabel, result.text, false);
|
addMsg(fromLabel, result.text, false);
|
||||||
// Send delivery receipt
|
// 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);
|
||||||
return;
|
return;
|
||||||
} catch(e2) {
|
} catch(e2) {
|
||||||
dbg('Session', senderFP, 'failed:', e2.message || e2);
|
dbg('Session', senderFP, 'failed:', e2.message || e2);
|
||||||
@@ -633,6 +635,7 @@ async function doRecover() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let currentGroup = null; // if set, messages go to group
|
let currentGroup = null; // if set, messages go to group
|
||||||
|
let lastDmPeer = null; // for /r reply
|
||||||
|
|
||||||
async function enterChat() {
|
async function enterChat() {
|
||||||
document.getElementById('setup').classList.remove('active');
|
document.getElementById('setup').classList.remove('active');
|
||||||
@@ -809,7 +812,39 @@ async function doSend() {
|
|||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.error) { addSys('Error: ' + data.error); }
|
if (data.error) { addSys('Error: ' + data.error); }
|
||||||
else { addSys('Alias @' + data.alias + ' registered'); }
|
else { addSys('Alias @' + data.alias + ' registered. Recovery key: ' + (data.recovery_key || 'N/A')); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (text === '/unalias') {
|
||||||
|
const resp = await fetch(SERVER + '/v1/alias/unregister', {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ fingerprint: normFP(myFingerprint) })
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.error) addSys('Error: ' + data.error);
|
||||||
|
else addSys('Alias removed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (text.startsWith('/admin-unalias ')) {
|
||||||
|
const parts = text.slice(15).trim().split(' ');
|
||||||
|
if (parts.length < 2) { addSys('Usage: /admin-unalias <alias> <admin-password>'); return; }
|
||||||
|
const resp = await fetch(SERVER + '/v1/alias/admin-remove', {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ alias: parts[0], admin_password: parts.slice(1).join(' ') })
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.error) addSys('Error: ' + data.error);
|
||||||
|
else addSys('Alias @' + parts[0] + ' removed by admin');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (text.startsWith('/r ') || text.startsWith('/reply ')) {
|
||||||
|
const replyText = text.startsWith('/r ') ? text.slice(3) : text.slice(7);
|
||||||
|
if (!lastDmPeer) { addSys('No one to reply to'); return; }
|
||||||
|
$peerInput.value = lastDmPeer;
|
||||||
|
try {
|
||||||
|
await sendEncrypted(lastDmPeer, replyText.trim());
|
||||||
|
addMsg(myFingerprint.slice(0, 19), replyText.trim(), true);
|
||||||
|
} catch(e) { addSys('Reply failed: ' + e.message); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (text.startsWith('/gcreate ')) { await groupCreate(text.slice(9).trim()); return; }
|
if (text.startsWith('/gcreate ')) { await groupCreate(text.slice(9).trim()); return; }
|
||||||
|
|||||||
Reference in New Issue
Block a user