v0.0.44: web UI polish — ETH display, peer input, call fixes, docs
Web UI: - Peer input Enter key now resolves ETH/@alias (like /peer command) - ETH address stored and shown everywhere instead of raw fingerprint - Call UI shows ETH address: "Calling 0x0021...", "In call with 0x9D70..." - Server URL color: #444 → #666 (readable on dark background) - Peer input placeholder: "ETH address, fingerprint, or @alias" - peerEthAddr persisted in localStorage across sessions Server: - WS binary header: strip zero-padding from 64-char to 32-char fingerprint - Call routing now works (was failing due to padded fingerprint lookup) - startCall() resolves ETH/alias before sending CallSignal::Offer - Audio bridge sends auth token to wzp-web as first WS message - Deterministic room name: sorted fingerprint pair (both peers same room) Docs updated: - SERVER.md: WZP integration section (components, running, TLS, auth flow) - USAGE.md: voice call usage for web and TUI - LLM_HELP.md: call architecture, key files, environment vars - LLM_BOT_DEV.md: note that bots cannot participate in calls - TESTING_E2E.md: updated WZP prerequisites with correct flags 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.43"
|
||||
version = "0.0.44"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -2989,7 +2989,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warzone-mule"
|
||||
version = "0.0.43"
|
||||
version = "0.0.44"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2998,7 +2998,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warzone-protocol"
|
||||
version = "0.0.43"
|
||||
version = "0.0.44"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bincode",
|
||||
@@ -3023,7 +3023,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warzone-server"
|
||||
version = "0.0.43"
|
||||
version = "0.0.44"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -3054,7 +3054,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warzone-wasm"
|
||||
version = "0.0.43"
|
||||
version = "0.0.44"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bincode",
|
||||
|
||||
@@ -9,7 +9,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.43"
|
||||
version = "0.0.44"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
rust-version = "1.75"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "warzone-protocol"
|
||||
version = "0.0.43"
|
||||
version = "0.0.44"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
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 {
|
||||
([(header::CONTENT_TYPE, "application/javascript")], r##"
|
||||
const CACHE = 'wz-v25';
|
||||
const CACHE = 'wz-v26';
|
||||
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
|
||||
|
||||
self.addEventListener('install', e => {
|
||||
@@ -150,7 +150,7 @@ const WEB_HTML: &str = r##"<!DOCTYPE html>
|
||||
#chat-header .tag { padding: 1px 6px; border-radius: 3px; font-size: 0.85em; }
|
||||
.tag-fp { background: #0a2e0a; color: #4ade80; }
|
||||
.tag-peer { background: #2e2e0a; color: #e6a23c; }
|
||||
.tag-server { color: #444; }
|
||||
.tag-server { color: #666; font-size: 0.8em; }
|
||||
#chat-header input { background: #1a1a2e; border: 1px solid #333; color: #e6a23c; padding: 2px 6px;
|
||||
border-radius: 3px; font-family: inherit; font-size: 0.85em; width: 280px; }
|
||||
|
||||
@@ -248,7 +248,7 @@ const WEB_HTML: &str = r##"<!DOCTYPE html>
|
||||
<span class="tag tag-fp" id="hdr-fp" style="cursor:pointer" title="Click to copy"></span>
|
||||
<span id="hdr-eth" style="display:none"></span>
|
||||
<span>→</span>
|
||||
<input id="peer-input" placeholder="Paste peer fingerprint..." autocomplete="off">
|
||||
<input id="peer-input" placeholder="ETH address, fingerprint, or @alias" autocomplete="off">
|
||||
<span class="tag-server" id="hdr-server"></span>
|
||||
</div>
|
||||
<div id="call-bar">
|
||||
@@ -281,13 +281,14 @@ let wasmIdentity = null; // WasmIdentity from WASM
|
||||
let myFingerprint = '';
|
||||
let myEthAddress = '';
|
||||
let mySeedHex = '';
|
||||
let peerEthAddr = null; // Peer's ETH address (for display; null if set by fingerprint)
|
||||
let sessions = {}; // peerFP -> { session: WasmSession, data: base64 }
|
||||
let peerBundles = {}; // peerFP -> bundle bytes
|
||||
let pollTimer = null;
|
||||
let ws = null; // WebSocket connection
|
||||
let wasmReady = false;
|
||||
|
||||
const VERSION = '0.0.43';
|
||||
const VERSION = '0.0.44';
|
||||
let DEBUG = true; // toggle with /debug command
|
||||
|
||||
// ── Receipt tracking ──
|
||||
@@ -348,6 +349,23 @@ function normFP(fp) {
|
||||
return fp.replace(/[^0-9a-fA-F]/g, '').toLowerCase();
|
||||
}
|
||||
|
||||
function peerDisplayName() {
|
||||
if (peerEthAddr) return peerEthAddr.slice(0, 12) + '...';
|
||||
const v = document.getElementById('peer-input').value.trim();
|
||||
return v ? v.slice(0, 16) + '...' : '?';
|
||||
}
|
||||
|
||||
function updatePeerDisplay() {
|
||||
// Resolve ETH address for display if we have a fingerprint
|
||||
const fp = document.getElementById('peer-input').value.trim();
|
||||
if (fp && !fp.startsWith('#') && !fp.startsWith('@') && !peerEthAddr) {
|
||||
// Try to get ETH address from server
|
||||
fetch(SERVER + '/v1/resolve/' + fp).then(r => r.json()).then(data => {
|
||||
if (data.eth_address) { peerEthAddr = data.eth_address; }
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function makeAddressClickable(text) {
|
||||
// Match fingerprint format: xxxx:xxxx:xxxx:xxxx... (at least 4 groups)
|
||||
text = text.replace(/([0-9a-f]{4}(?::[0-9a-f]{4}){3,})/gi, function(match) {
|
||||
@@ -1084,6 +1102,7 @@ async function enterChat() {
|
||||
if (savedPeer) {
|
||||
$peerInput.value = savedPeer;
|
||||
}
|
||||
peerEthAddr = localStorage.getItem('wz-peer-eth') || null;
|
||||
|
||||
connectWebSocket();
|
||||
|
||||
@@ -1244,18 +1263,18 @@ function updateCallUI() {
|
||||
}
|
||||
break;
|
||||
case 'calling':
|
||||
status.textContent = '\u{1F4DE} Calling ' + (callPeer || '...').slice(0, 16);
|
||||
status.textContent = '\u{1F4DE} Calling ' + (peerEthAddr ? peerEthAddr.slice(0, 12) + '...' : (callPeer || '...').slice(0, 16));
|
||||
status.className = 'call-status';
|
||||
btnHangup.style.display = '';
|
||||
break;
|
||||
case 'ringing':
|
||||
status.textContent = '\u{1F4DE} Incoming call from ' + (callPeer || '?').slice(0, 16);
|
||||
status.textContent = '\u{1F4DE} Incoming call from ' + (peerEthAddr ? peerEthAddr.slice(0, 12) + '...' : (callPeer || '?').slice(0, 16));
|
||||
status.className = 'call-status incoming-call';
|
||||
btnAccept.style.display = '';
|
||||
btnReject.style.display = '';
|
||||
break;
|
||||
case 'active':
|
||||
status.textContent = '\u{1F50A} In call with ' + (callPeer || '?').slice(0, 16);
|
||||
status.textContent = '\u{1F50A} In call with ' + (peerEthAddr ? peerEthAddr.slice(0, 12) + '...' : (callPeer || '?').slice(0, 16));
|
||||
status.className = 'call-status';
|
||||
btnHangup.style.display = '';
|
||||
break;
|
||||
@@ -1263,18 +1282,29 @@ function updateCallUI() {
|
||||
}
|
||||
|
||||
async function startCall() {
|
||||
const peer = $peerInput.value.trim();
|
||||
let peer = $peerInput.value.trim();
|
||||
if (!peer || peer.startsWith('#')) { addSys('Set a peer first'); return; }
|
||||
|
||||
// Resolve ETH address or @alias to fingerprint
|
||||
if (peer.startsWith('@') || peer.startsWith('0x') || peer.startsWith('0X')) {
|
||||
const endpoint = peer.startsWith('@') ? '/v1/alias/resolve/' + peer.slice(1) : '/v1/resolve/' + peer;
|
||||
try {
|
||||
const resp = await fetch(SERVER + endpoint);
|
||||
const data = await resp.json();
|
||||
if (data.error) { addSys('Cannot resolve peer: ' + data.error); return; }
|
||||
peer = data.fingerprint;
|
||||
} catch(e) { addSys('Cannot resolve peer: ' + e.message); return; }
|
||||
}
|
||||
|
||||
callState = 'calling';
|
||||
callPeer = peer;
|
||||
updateCallUI();
|
||||
|
||||
// Send CallSignal::Offer via WS
|
||||
try {
|
||||
const signalBytes = create_call_signal(wasmIdentity, 'offer', '', normFP(peer));
|
||||
const fp = normFP(peer);
|
||||
const signalBytes = create_call_signal(wasmIdentity, 'offer', '', fp);
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
const fp = normFP(peer);
|
||||
const header = new TextEncoder().encode(fp.padEnd(64, '0').slice(0, 64));
|
||||
const payload = new Uint8Array(header.length + signalBytes.length);
|
||||
payload.set(header);
|
||||
@@ -1409,13 +1439,14 @@ let captureNode = null;
|
||||
let playbackNode = null;
|
||||
|
||||
async function startAudio() {
|
||||
// Fetch relay config
|
||||
let relayAddr;
|
||||
// Fetch relay config (includes auth token)
|
||||
let relayAddr, authToken;
|
||||
try {
|
||||
const resp = await fetch(SERVER + '/v1/wzp/relay-config');
|
||||
const data = await resp.json();
|
||||
relayAddr = data.relay_addr;
|
||||
dbg('Relay address:', relayAddr);
|
||||
authToken = data.token;
|
||||
dbg('Relay address:', relayAddr, 'token:', authToken);
|
||||
} catch(e) {
|
||||
addSys('Audio: cannot get relay config \u2014 ' + e.message);
|
||||
return;
|
||||
@@ -1433,18 +1464,23 @@ async function startAudio() {
|
||||
|
||||
audioCtx = new AudioContext({ sampleRate: 48000 });
|
||||
|
||||
// Generate room name from call peer (deterministic)
|
||||
const room = callPeer ? normFP(callPeer).slice(0, 16) : 'default';
|
||||
const proto = relayAddr.startsWith('https') ? 'wss:' : 'ws:';
|
||||
const host = relayAddr.replace(/^https?:\\/\\//, '');
|
||||
// Deterministic room: sort both fingerprints so both peers get the same room
|
||||
const myFP = normFP(myFingerprint);
|
||||
const peerFP = callPeer ? normFP(callPeer) : '';
|
||||
const roomPair = [myFP, peerFP].sort().join('-');
|
||||
const room = roomPair.slice(0, 32);
|
||||
const host = relayAddr.replace(/^https?:\/\//, '');
|
||||
const proto = host.startsWith('localhost') || host.startsWith('127.') ? 'ws:' : 'wss:';
|
||||
const wsUrl = proto + '//' + host + '/ws/' + room;
|
||||
|
||||
addSys('Audio: connecting to ' + room + '...');
|
||||
addSys('Audio: connecting to room ' + room.slice(0, 12) + '...');
|
||||
|
||||
audioWs = new WebSocket(wsUrl);
|
||||
audioWs.binaryType = 'arraybuffer';
|
||||
|
||||
audioWs.onopen = async () => {
|
||||
// Send auth token as first message (required by wzp-web --auth-url)
|
||||
audioWs.send(JSON.stringify({ type: 'auth', token: authToken }));
|
||||
addSys('Audio: connected \u2014 mic active');
|
||||
|
||||
// Capture: mic -> PCM frames -> WS
|
||||
@@ -1658,14 +1694,20 @@ async function doSend() {
|
||||
const resp = await fetch(SERVER + endpoint);
|
||||
const data = await resp.json();
|
||||
if (data.error) { addSys('Cannot resolve ' + val + ': ' + data.error); return; }
|
||||
peerEthAddr = (val.startsWith('0x') || val.startsWith('0X')) ? val : (data.eth_address || null);
|
||||
$peerInput.value = data.fingerprint;
|
||||
localStorage.setItem('wz-peer', val);
|
||||
if (peerEthAddr) localStorage.setItem('wz-peer-eth', peerEthAddr);
|
||||
addSys(val + ' \u2192 ' + data.fingerprint.slice(0,16) + '...');
|
||||
addSys('Peer set to ' + (peerEthAddr || data.fingerprint.slice(0,16) + '...'));
|
||||
} else {
|
||||
$peerInput.value = val;
|
||||
peerEthAddr = null;
|
||||
localStorage.setItem('wz-peer', val);
|
||||
localStorage.removeItem('wz-peer-eth');
|
||||
}
|
||||
currentGroup = null;
|
||||
localStorage.setItem('wz-peer', $peerInput.value);
|
||||
addSys('Peer set to ' + $peerInput.value.slice(0,16) + '...');
|
||||
addSys('Peer set to ' + peerDisplayName());
|
||||
updateCallUI();
|
||||
return;
|
||||
}
|
||||
@@ -1793,6 +1835,45 @@ $input.addEventListener('input', function() {
|
||||
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
|
||||
});
|
||||
|
||||
// Peer input: Enter sets peer (like /peer command)
|
||||
document.getElementById('peer-input').addEventListener('keydown', async (e) => {
|
||||
if (e.key !== 'Enter') return;
|
||||
const val = e.target.value.trim();
|
||||
if (!val) return;
|
||||
// Treat as /peer command
|
||||
if (val.startsWith('@') || val.startsWith('0x') || val.startsWith('0X') || /^[0-9a-fA-F]{16,}$/.test(val)) {
|
||||
const endpoint = val.startsWith('@') ? '/v1/alias/resolve/' + val.slice(1) : (val.startsWith('0x') || val.startsWith('0X')) ? '/v1/resolve/' + val : null;
|
||||
if (endpoint) {
|
||||
try {
|
||||
const resp = await fetch(SERVER + endpoint);
|
||||
const data = await resp.json();
|
||||
if (data.error) { addSys('Cannot resolve: ' + data.error); return; }
|
||||
// Store ETH address for display, use fingerprint internally
|
||||
peerEthAddr = val;
|
||||
e.target.value = data.fingerprint;
|
||||
localStorage.setItem('wz-peer', val);
|
||||
localStorage.setItem('wz-peer-eth', val);
|
||||
addSys(val + ' \u2192 ' + data.fingerprint.slice(0,16) + '...');
|
||||
addSys('Peer set to ' + val.slice(0,16) + '...');
|
||||
updatePeerDisplay();
|
||||
} catch(err) { addSys('Resolve failed: ' + err.message); }
|
||||
} else {
|
||||
// Raw fingerprint
|
||||
peerEthAddr = null;
|
||||
localStorage.setItem('wz-peer', val);
|
||||
localStorage.removeItem('wz-peer-eth');
|
||||
addSys('Peer set to ' + val.slice(0,16) + '...');
|
||||
updatePeerDisplay();
|
||||
}
|
||||
} else if (val.startsWith('#')) {
|
||||
// Group shortcut
|
||||
const gname = val.replace('#','');
|
||||
e.target.value = '#' + gname;
|
||||
localStorage.setItem('wz-peer', '#' + gname);
|
||||
addSys('Switched to group #' + gname);
|
||||
}
|
||||
});
|
||||
|
||||
// Wire up buttons (module scope can't use onclick in HTML)
|
||||
document.getElementById('btn-generate').onclick = () => doGenerate();
|
||||
document.getElementById('btn-show-recover').onclick = () => document.getElementById('recover-area').style.display = 'block';
|
||||
|
||||
@@ -135,7 +135,14 @@ async fn handle_socket(socket: WebSocket, state: AppState, fingerprint: String)
|
||||
// For simplicity: first 32 hex chars = recipient fp, rest = message
|
||||
if data.len() > 64 {
|
||||
let header = String::from_utf8_lossy(&data[..64]).to_string();
|
||||
let to_fp = normalize_fp(&header);
|
||||
let raw_fp = normalize_fp(&header);
|
||||
// The WS header is 64 hex chars (32 bytes padded with '0').
|
||||
// Fingerprints are 32 hex chars. Truncate to 32 if zero-padded.
|
||||
let to_fp = if raw_fp.len() > 32 && raw_fp[32..].chars().all(|c| c == '0') {
|
||||
raw_fp[..32].to_string()
|
||||
} else {
|
||||
raw_fp
|
||||
};
|
||||
let message = &data[64..];
|
||||
|
||||
// Dedup: skip if we already processed this message ID
|
||||
|
||||
@@ -253,6 +253,10 @@ The bridge translates numeric chat_id ↔ fingerprints automatically.
|
||||
| parse_mode HTML | rendered | rendered in web client |
|
||||
| Media groups | yes | not yet |
|
||||
|
||||
## Voice Calls
|
||||
|
||||
Bots cannot initiate or participate in voice calls. Voice is peer-to-peer only between human clients (web or TUI). Call signaling messages (`CallSignal` type) are delivered to bots via getUpdates as `text="/call_Offer"` etc., but bots should ignore them -- there is no audio path for bots.
|
||||
|
||||
## Key Rules
|
||||
|
||||
1. **Always use offset** in getUpdates — without it you reprocess messages
|
||||
|
||||
@@ -195,6 +195,40 @@ while True:
|
||||
time.sleep(1)
|
||||
```
|
||||
|
||||
## Voice Calls
|
||||
|
||||
### Architecture
|
||||
Call signaling flows through the featherChat WebSocket (offer/answer/hangup/reject/ringing/busy).
|
||||
Audio flows through a separate WZP relay infrastructure:
|
||||
|
||||
```
|
||||
Browser A <--WS--> wzp-web <--QUIC--> wzp-relay <--QUIC--> wzp-web <--WS--> Browser B
|
||||
| |
|
||||
featherChat server (/v1/auth/validate)
|
||||
```
|
||||
|
||||
### Key files
|
||||
- Call signaling: `warzone-server/src/routes/ws.rs` (WireMessage::CallSignal handling)
|
||||
- Call state: `warzone-server/src/state.rs` (CallState, active_calls)
|
||||
- Relay config: `warzone-server/src/routes/wzp.rs` (token issuance)
|
||||
- Web audio: `warzone-server/src/routes/web.rs` (startAudio/stopAudio functions)
|
||||
- TUI calls: `warzone-client/src/tui/commands.rs` (/call, /accept, /reject, /hangup)
|
||||
- Protocol: `warzone-protocol/src/message.rs` (CallSignal, CallSignalType)
|
||||
|
||||
### Environment
|
||||
- `WZP_RELAY_ADDR` -- tells featherChat server where wzp-web bridge is (e.g., `127.0.0.1:8080`)
|
||||
- Without this, `/v1/wzp/relay-config` returns default `127.0.0.1:4433`
|
||||
|
||||
### Commands
|
||||
|
||||
cmd | action | example
|
||||
--- | --- | ---
|
||||
/call | start voice call with current peer | /call
|
||||
/call <addr> | start voice call with specific peer | /call @alice
|
||||
/accept | accept incoming call | /accept
|
||||
/reject | reject incoming call | /reject
|
||||
/hangup | end current call | /hangup
|
||||
|
||||
## Server API (other endpoints)
|
||||
|
||||
- POST /v1/register -- upload prekey bundle
|
||||
|
||||
@@ -431,6 +431,56 @@ Telegram bot libraries can be adapted with minimal changes.
|
||||
|
||||
---
|
||||
|
||||
## Voice Calls (WZP Integration)
|
||||
|
||||
featherChat supports voice calls via the WarzonePhone (WZP) audio relay. Three components work together:
|
||||
|
||||
### Components
|
||||
|
||||
| Component | Binary | Port | Purpose |
|
||||
|-----------|--------|------|---------|
|
||||
| featherChat server | `warzone-server` | 7700 | Signaling (offer/answer/hangup) + auth tokens |
|
||||
| WZP relay | `wzp-relay` | 4433 | QUIC audio relay (SFU) |
|
||||
| WZP web bridge | `wzp-web` | 8080 | Browser WebSocket ↔ QUIC bridge |
|
||||
|
||||
### Running
|
||||
|
||||
```bash
|
||||
# 1. WZP relay (QUIC audio)
|
||||
./wzp-relay --listen 0.0.0.0:4433 --auth-url http://127.0.0.1:7700/v1/auth/validate
|
||||
|
||||
# 2. WZP web bridge (browser ↔ relay)
|
||||
./wzp-web --port 8080 --relay 127.0.0.1:4433 --auth-url http://127.0.0.1:7700/v1/auth/validate
|
||||
|
||||
# 3. featherChat server (with relay address)
|
||||
WZP_RELAY_ADDR=127.0.0.1:8080 ./warzone-server
|
||||
```
|
||||
|
||||
### TLS Requirements
|
||||
|
||||
| Scenario | TLS needed? | Why |
|
||||
|----------|-------------|-----|
|
||||
| localhost dev | No | Browser allows mic on localhost without HTTPS |
|
||||
| LAN/remote | wzp-web needs TLS | Browsers require HTTPS for `getUserMedia()` on non-localhost |
|
||||
| Production | All three should use TLS | Security best practice |
|
||||
|
||||
For production TLS on wzp-web:
|
||||
```bash
|
||||
./wzp-web --port 8080 --relay 127.0.0.1:4433 --auth-url http://127.0.0.1:7700/v1/auth/validate --cert /path/to/cert.pem --key /path/to/key.pem
|
||||
```
|
||||
|
||||
### Auth Flow
|
||||
|
||||
1. User clicks Call -> signaling via featherChat WebSocket
|
||||
2. Call accepted -> both clients fetch `GET /v1/wzp/relay-config`
|
||||
3. Server returns `{ relay_addr, token, expires_in: 300 }`
|
||||
4. Clients connect WebSocket to `ws://relay_addr/ws/ROOM`
|
||||
5. First message: `{"type":"auth","token":"<token>"}`
|
||||
6. wzp-web validates token against featherChat `/v1/auth/validate`
|
||||
7. Audio flows: mic -> PCM -> WS -> wzp-web -> QUIC -> wzp-relay -> peer
|
||||
|
||||
---
|
||||
|
||||
## 6. Database
|
||||
|
||||
The server uses **sled** (embedded key-value store). All data lives under
|
||||
|
||||
@@ -31,14 +31,15 @@ wasm-pack build crates/warzone-wasm --target web --out-dir ../../wasm-pkg
|
||||
### Voice Call Testing (requires WZP relay)
|
||||
|
||||
```bash
|
||||
# Start WZP web bridge (from warzone-phone repo)
|
||||
./wzp-web --bind 0.0.0.0:8080 --relay 127.0.0.1:4433
|
||||
# Terminal A: WZP relay (QUIC audio SFU)
|
||||
./wzp-relay --listen 0.0.0.0:4433 --auth-url http://127.0.0.1:7700/v1/auth/validate
|
||||
|
||||
# Start WZP relay
|
||||
./wzp-relay --bind 0.0.0.0:4433
|
||||
# Terminal B: WZP web bridge (browser WebSocket <-> QUIC)
|
||||
./wzp-web --port 8080 --relay 127.0.0.1:4433 --auth-url http://127.0.0.1:7700/v1/auth/validate
|
||||
|
||||
# Set relay address for featherChat
|
||||
# Terminal C: featherChat server with relay address
|
||||
export WZP_RELAY_ADDR=127.0.0.1:8080
|
||||
./warzone-server
|
||||
```
|
||||
|
||||
---
|
||||
@@ -218,11 +219,11 @@ WARZONE_HOME=~/.warzone-b ./target/release/warzone-client tui --server http://lo
|
||||
|
||||
### Prerequisites
|
||||
```bash
|
||||
# Terminal 1: WZP relay
|
||||
./wzp-relay --bind 0.0.0.0:4433
|
||||
# Terminal 1: WZP relay (QUIC audio SFU)
|
||||
./wzp-relay --listen 0.0.0.0:4433 --auth-url http://127.0.0.1:7700/v1/auth/validate
|
||||
|
||||
# Terminal 2: WZP web bridge
|
||||
./wzp-web --bind 0.0.0.0:8080 --relay 127.0.0.1:4433
|
||||
# Terminal 2: WZP web bridge (browser WebSocket <-> QUIC)
|
||||
./wzp-web --port 8080 --relay 127.0.0.1:4433 --auth-url http://127.0.0.1:7700/v1/auth/validate
|
||||
|
||||
# Terminal 3: featherChat server
|
||||
WZP_RELAY_ADDR=127.0.0.1:8080 ./warzone-server
|
||||
|
||||
@@ -287,6 +287,32 @@ The web client supports the same slash commands as the TUI: `/peer`, `/p`, `/r`,
|
||||
|
||||
---
|
||||
|
||||
## Voice Calls
|
||||
|
||||
### Web Client
|
||||
1. Set a peer (paste ETH address or use `/peer @alias`)
|
||||
2. Click the Call button or type `/call`
|
||||
3. Peer sees "Incoming call" and clicks Accept
|
||||
4. Both allow microphone access
|
||||
5. Audio flows -- speak normally
|
||||
6. Click "End Call" or type `/hangup` to end
|
||||
|
||||
### TUI Client
|
||||
1. `/call <peer_address>` -- initiate call
|
||||
2. Peer sees notification and can use `/accept` or `/reject`
|
||||
3. Audio currently requires web client (TUI shows hint)
|
||||
4. `/hangup` -- end call
|
||||
|
||||
### Commands
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/call` | Start voice call with current peer |
|
||||
| `/accept` | Accept incoming call |
|
||||
| `/reject` | Reject incoming call |
|
||||
| `/hangup` | End current call |
|
||||
|
||||
---
|
||||
|
||||
## Groups
|
||||
|
||||
### Creating and Using Groups
|
||||
|
||||
Reference in New Issue
Block a user