v0.0.46: fix group calls, admin commands, ETH in members
Server: - POST /v1/groups/:name/signal — broadcast plaintext JSON to all online group members via WS push (for call signals, typing, etc.) - No auth required (membership validated from 'from' field) Web client: - Group call signals now use /signal endpoint (was broken with /send) - WS handler detects group_call JSON signals early - Auto-join #ops now properly registers presence + fetches members - /gmembers resolves ETH addresses via /v1/resolve - /admin-calls — list all active calls in the system - /admin-help — list all admin/debug commands 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
@@ -2956,7 +2956,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warzone-client"
|
||||
version = "0.0.45"
|
||||
version = "0.0.46"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -2989,7 +2989,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warzone-mule"
|
||||
version = "0.0.45"
|
||||
version = "0.0.46"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2998,7 +2998,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warzone-protocol"
|
||||
version = "0.0.45"
|
||||
version = "0.0.46"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bincode",
|
||||
@@ -3023,7 +3023,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warzone-server"
|
||||
version = "0.0.45"
|
||||
version = "0.0.46"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -3054,7 +3054,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warzone-wasm"
|
||||
version = "0.0.45"
|
||||
version = "0.0.46"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bincode",
|
||||
|
||||
@@ -9,7 +9,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.45"
|
||||
version = "0.0.46"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
rust-version = "1.75"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "warzone-protocol"
|
||||
version = "0.0.45"
|
||||
version = "0.0.46"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Core crypto & wire protocol for featherChat (Warzone messenger)"
|
||||
|
||||
@@ -18,6 +18,7 @@ pub fn routes() -> Router<AppState> {
|
||||
.route("/groups/:name/leave", post(leave_group))
|
||||
.route("/groups/:name/kick", post(kick_member))
|
||||
.route("/groups/:name/members", get(get_members))
|
||||
.route("/groups/:name/signal", post(signal_group))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
@@ -305,3 +306,47 @@ async fn get_members(
|
||||
"online_count": online_count,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Broadcast a plaintext signal to all online group members via WS push.
|
||||
/// Used for group calls, typing indicators, etc. — NOT for encrypted messages.
|
||||
async fn signal_group(
|
||||
State(state): State<AppState>,
|
||||
Path(name): Path<String>,
|
||||
Json(req): Json<serde_json::Value>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
let group = match load_group(&state.db.groups, &name) {
|
||||
Some(g) => g,
|
||||
None => return Ok(Json(serde_json::json!({ "error": "group not found" }))),
|
||||
};
|
||||
|
||||
let from = req
|
||||
.get("from")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let from = normalize_fp(&from);
|
||||
if !group.members.contains(&from) {
|
||||
return Ok(Json(serde_json::json!({ "error": "not a member" })));
|
||||
}
|
||||
|
||||
// Broadcast the raw JSON payload to all online members except sender
|
||||
let payload = serde_json::to_vec(&req).unwrap_or_default();
|
||||
let mut pushed = 0;
|
||||
for member in &group.members {
|
||||
if *member == from {
|
||||
continue;
|
||||
}
|
||||
if state.push_to_client(member, &payload).await {
|
||||
pushed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Group '{}' signal from {}: pushed to {}/{} members",
|
||||
name,
|
||||
from,
|
||||
pushed,
|
||||
group.members.len() - 1
|
||||
);
|
||||
Ok(Json(serde_json::json!({ "ok": true, "pushed": pushed })))
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ async fn pwa_manifest() -> impl IntoResponse {
|
||||
|
||||
async fn service_worker() -> impl IntoResponse {
|
||||
([(header::CONTENT_TYPE, "application/javascript")], r##"
|
||||
const CACHE = 'wz-v27';
|
||||
const CACHE = 'wz-v28';
|
||||
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
|
||||
|
||||
self.addEventListener('install', e => {
|
||||
@@ -288,7 +288,7 @@ let pollTimer = null;
|
||||
let ws = null; // WebSocket connection
|
||||
let wasmReady = false;
|
||||
|
||||
const VERSION = '0.0.45';
|
||||
const VERSION = '0.0.46';
|
||||
let DEBUG = true; // toggle with /debug command
|
||||
|
||||
// ── Receipt tracking ──
|
||||
@@ -705,6 +705,16 @@ function connectWebSocket() {
|
||||
async function handleIncomingMessage(bytes) {
|
||||
dbg('Processing message,', bytes.length, 'bytes, sessions:', Object.keys(sessions));
|
||||
|
||||
// Check for plaintext JSON signals (group call, typing, etc.) from /signal endpoint
|
||||
try {
|
||||
const textData = new TextDecoder().decode(bytes);
|
||||
const signalData = JSON.parse(textData);
|
||||
if (signalData.type === 'group_call') {
|
||||
handleGroupCallSignal(signalData);
|
||||
return;
|
||||
}
|
||||
} catch(e) {} // Not JSON or not a signal, continue with normal handling
|
||||
|
||||
// Quick check: try to parse as Receipt first (no session needed, no decrypt)
|
||||
try {
|
||||
const resultStr = decrypt_wire_message(mySeedHex, mySpkSecretHex, bytes, null);
|
||||
@@ -1120,13 +1130,21 @@ async function enterChat() {
|
||||
if (!savedPeer) {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// Create #ops if it doesn't exist (ignore error if already exists)
|
||||
await fetch(SERVER + '/v1/groups/create', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({name:'ops', creator: normFP(myFingerprint)}) });
|
||||
// Join (no auth needed for join in current setup)
|
||||
await fetch(SERVER + '/v1/groups/ops/join', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({fingerprint: normFP(myFingerprint)}) });
|
||||
// Switch to #ops with full presence registration
|
||||
currentGroup = 'ops';
|
||||
$peerInput.value = '#ops';
|
||||
localStorage.setItem('wz-peer', '#ops');
|
||||
addSys('Welcome! You have been added to #ops');
|
||||
// Fetch member list to confirm
|
||||
const resp = await fetch(SERVER + '/v1/groups/ops');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
if (data.members) {
|
||||
addSys('Switched to group "ops" (' + data.members.length + ' members)');
|
||||
}
|
||||
}
|
||||
} catch(e) { dbg('Auto-join #ops failed:', e); }
|
||||
}, 500);
|
||||
}
|
||||
@@ -1697,12 +1715,12 @@ async function startGroupCall() {
|
||||
groupCallParticipants = [normFP(myFingerprint)];
|
||||
updateGroupCallUI();
|
||||
|
||||
// Notify group members
|
||||
// Notify group members via signal endpoint (plaintext broadcast)
|
||||
const msg = JSON.stringify({ type: 'group_call', action: 'started', group: gname, room: groupCallRoom, from: normFP(myFingerprint) });
|
||||
await fetch(SERVER + '/v1/groups/' + gname + '/send', {
|
||||
await fetch(SERVER + '/v1/groups/' + gname + '/signal', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ from: normFP(myFingerprint), messages: [{ to: '__broadcast__', message: Array.from(new TextEncoder().encode(msg)) }] })
|
||||
body: msg
|
||||
}).catch(() => {});
|
||||
|
||||
addSys('Group call started in #' + gname + ' \u2014 waiting for others to join');
|
||||
@@ -1717,12 +1735,12 @@ async function joinGroupCall(gname, room) {
|
||||
}
|
||||
updateGroupCallUI();
|
||||
|
||||
// Notify others we joined
|
||||
// Notify others we joined via signal endpoint
|
||||
const msg = JSON.stringify({ type: 'group_call', action: 'joined', group: gname, room: room, from: normFP(myFingerprint) });
|
||||
await fetch(SERVER + '/v1/groups/' + gname + '/send', {
|
||||
await fetch(SERVER + '/v1/groups/' + gname + '/signal', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ from: normFP(myFingerprint), messages: [{ to: '__broadcast__', message: Array.from(new TextEncoder().encode(msg)) }] })
|
||||
body: msg
|
||||
}).catch(() => {});
|
||||
|
||||
addSys('Joined group call in #' + gname);
|
||||
@@ -1807,12 +1825,12 @@ function leaveGroupCall() {
|
||||
if (!groupCallGroup) return;
|
||||
const gname = groupCallGroup;
|
||||
|
||||
// Notify others
|
||||
// Notify others via signal endpoint
|
||||
const msg = JSON.stringify({ type: 'group_call', action: 'left', group: gname, room: groupCallRoom, from: normFP(myFingerprint) });
|
||||
fetch(SERVER + '/v1/groups/' + gname + '/send', {
|
||||
fetch(SERVER + '/v1/groups/' + gname + '/signal', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ from: normFP(myFingerprint), messages: [{ to: '__broadcast__', message: Array.from(new TextEncoder().encode(msg)) }] })
|
||||
body: msg
|
||||
}).catch(() => {});
|
||||
|
||||
stopAudio();
|
||||
@@ -2003,6 +2021,32 @@ async function doSend() {
|
||||
else addSys('Alias @' + parts[0] + ' removed by admin');
|
||||
return;
|
||||
}
|
||||
if (text === '/admin-calls') {
|
||||
try {
|
||||
const resp = await fetch(SERVER + '/v1/calls/active');
|
||||
const data = await resp.json();
|
||||
if (data.calls && data.calls.length > 0) {
|
||||
addSys('Active calls (' + data.calls.length + '):');
|
||||
data.calls.forEach(c => {
|
||||
addSys(' ' + c.call_id.slice(0,8) + ' ' + c.caller_fp.slice(0,12) + ' \u2192 ' + c.callee_fp.slice(0,12) + ' [' + c.status + '] ' + (c.group_name ? '#' + c.group_name : 'DM'));
|
||||
});
|
||||
} else {
|
||||
addSys('No active calls');
|
||||
}
|
||||
} catch(e) { addSys('Error: ' + e.message); }
|
||||
return;
|
||||
}
|
||||
if (text === '/admin-help' || text === '/admin') {
|
||||
addSys('Admin commands:');
|
||||
addSys(' /admin-calls \u2014 list all active calls');
|
||||
addSys(' /admin-unalias <a> <pw> \u2014 force-remove an alias');
|
||||
addSys(' /bundleinfo \u2014 debug key bundle info');
|
||||
addSys(' /sessions \u2014 list cached sessions');
|
||||
addSys(' /selftest \u2014 run WASM self-test');
|
||||
addSys(' /debug \u2014 toggle debug mode');
|
||||
addSys(' /reset \u2014 clear all local data');
|
||||
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; }
|
||||
@@ -2072,11 +2116,19 @@ async function doSend() {
|
||||
const r = await fetch(SERVER+'/v1/groups/'+currentGroup+'/members');
|
||||
const d = await r.json();
|
||||
if (d.error) { addSys('Error: '+d.error); return; }
|
||||
addSys('Members of #'+currentGroup+':');
|
||||
for (const m of d.members) {
|
||||
const a = m.alias ? '@'+m.alias : m.fingerprint.slice(0,12)+'...';
|
||||
addSys(' '+a+(m.is_creator?' ★':''));
|
||||
}
|
||||
const onlineCount = d.online_count || d.members.filter(m => m.online).length;
|
||||
addSys('Members of #'+currentGroup+' ('+onlineCount+'/'+d.members.length+' online):');
|
||||
// Resolve ETH addresses for members without aliases
|
||||
const ethPromises = d.members.map(m =>
|
||||
m.alias ? Promise.resolve('@'+m.alias) :
|
||||
fetch(SERVER+'/v1/resolve/'+m.fingerprint).then(r2=>r2.json()).then(rd=>rd.eth_address ? rd.eth_address.slice(0,12)+'...' : m.fingerprint.slice(0,12)+'...').catch(()=>m.fingerprint.slice(0,12)+'...')
|
||||
);
|
||||
const labels = await Promise.all(ethPromises);
|
||||
d.members.forEach((m, i) => {
|
||||
const status = m.online ? '\u{1F7E2}' : '\u26AB';
|
||||
const isSelf = m.fingerprint === normFP(myFingerprint);
|
||||
addSys(' '+status+' '+labels[i]+(m.is_creator?' ★':'')+(isSelf?' *':''));
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (text === '/friend' || text === '/friends') {
|
||||
|
||||
Reference in New Issue
Block a user