ipc: CmdSendMessage now routes to the named recipient only (m.SendTo) when the "to" field is set, rather than broadcasting to all peers. Group messages continue to use Broadcast. This is the correct privacy behaviour for DMs. test-network.sh: - Resolve each peer's hex id from get_state before joining the network, using a retry loop with timeout to handle slow daemon startup. - Added a DM section: alice→bob, bob→alice, charlie→alice, alice→charlie. DMs use the "to" + "room":"dm:<peer_id>" convention. - Fixed jq -cn (compact) instead of jq -n (pretty) so the IPC newline- delimited protocol receives a single JSON line per command. - pretty() now renders DMs as 📨 DM <from→to> rather than 💬 #room. - Persistence check now breaks down totals by group vs DM message count. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
340 lines
16 KiB
Bash
Executable File
340 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# test-network.sh — spin up anchor + 3 peers, connect them, exchange messages + DMs.
|
|
# Usage: ./test-network.sh
|
|
# Requires: go, nc, jq, sqlite3
|
|
|
|
set -euo pipefail
|
|
|
|
# ── colours ───────────────────────────────────────────────────────────────────
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
|
CYAN='\033[0;36m'; MAGENTA='\033[0;35m'; BLUE='\033[0;34m'
|
|
BOLD='\033[1m'; DIM='\033[2m'; RESET='\033[0m'
|
|
|
|
ANCHOR_COLOR=$YELLOW
|
|
ALICE_COLOR=$CYAN
|
|
BOB_COLOR=$MAGENTA
|
|
CHARLIE_COLOR=$GREEN
|
|
|
|
# ── config ────────────────────────────────────────────────────────────────────
|
|
ANCHOR_PORT=19339
|
|
ALICE_IPC=19337
|
|
BOB_IPC=19340
|
|
CHARLIE_IPC=19343
|
|
NETWORK_NAME="test-$(date +%s)"
|
|
DATA_ROOT=$(mktemp -d /tmp/waste-test-XXXXXX)
|
|
|
|
# ── cleanup ───────────────────────────────────────────────────────────────────
|
|
PIDS=()
|
|
cleanup() {
|
|
echo ""
|
|
echo -e "${DIM}── cleaning up ──────────────────────────────────────────${RESET}"
|
|
for pid in "${PIDS[@]:-}"; do
|
|
kill "$pid" 2>/dev/null || true
|
|
done
|
|
wait 2>/dev/null || true
|
|
echo -e "${DIM}data left at: ${DATA_ROOT}${RESET}"
|
|
echo -e "${DIM} inspect: sqlite3 ${DATA_ROOT}/alice/messages.db${RESET}"
|
|
echo -e "${DIM}done.${RESET}"
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# ── helpers ───────────────────────────────────────────────────────────────────
|
|
log() {
|
|
local color="$1"; shift
|
|
local label="$1"; shift
|
|
echo -e "${color}${BOLD}[${label}]${RESET} $*"
|
|
}
|
|
|
|
# Pretty-print a raw IPC JSON line
|
|
pretty() {
|
|
local color="$1"
|
|
local label="$2"
|
|
local line="$3"
|
|
local type
|
|
type=$(echo "$line" | jq -r '.type // "?"' 2>/dev/null)
|
|
case "$type" in
|
|
state_snapshot)
|
|
local id nick
|
|
id=$(echo "$line" | jq -r '.local_peer.id[:16]' 2>/dev/null | sed 's/.\{4\}/& /g')
|
|
nick=$(echo "$line" | jq -r '.local_peer.alias' 2>/dev/null)
|
|
echo -e "${color}${BOLD}[${label}]${RESET} ${DIM}state_snapshot${RESET} id=${CYAN}${id}${RESET} alias=${BOLD}${nick}${RESET}"
|
|
;;
|
|
session_ready)
|
|
local pid nick
|
|
pid=$(echo "$line" | jq -r 'if .peer_id then .peer_id[:16] else "?" end' 2>/dev/null | sed 's/.\{4\}/& /g')
|
|
nick=$(echo "$line" | jq -r '.nick // "?"' 2>/dev/null)
|
|
echo -e "${color}${BOLD}[${label}]${RESET} ${GREEN}✓ session_ready${RESET} peer=${BOLD}${nick}${RESET} (${DIM}${pid}${RESET})"
|
|
;;
|
|
peer_connected)
|
|
local nick
|
|
nick=$(echo "$line" | jq -r '.peer.alias // "?"' 2>/dev/null)
|
|
echo -e "${color}${BOLD}[${label}]${RESET} ${GREEN}→ peer_connected${RESET} ${BOLD}${nick}${RESET}"
|
|
;;
|
|
peer_disconnected)
|
|
local pid
|
|
pid=$(echo "$line" | jq -r 'if .peer_id then .peer_id[:8] else "?" end' 2>/dev/null)
|
|
echo -e "${color}${BOLD}[${label}]${RESET} ${RED}← peer_disconnected${RESET} ${DIM}${pid}${RESET}"
|
|
;;
|
|
message_received)
|
|
local from body room to_field
|
|
from=$(echo "$line" | jq -r '.message.from[:8] // "?"' 2>/dev/null)
|
|
body=$(echo "$line" | jq -r '.message.body // ""' 2>/dev/null)
|
|
room=$(echo "$line" | jq -r '.message.room // ""' 2>/dev/null)
|
|
to_field=$(echo "$line" | jq -r '.message.to // ""' 2>/dev/null)
|
|
if [ -n "$to_field" ]; then
|
|
local to_short
|
|
to_short=$(echo "$to_field" | cut -c1-8)
|
|
echo -e "${color}${BOLD}[${label}]${RESET} ${BLUE}📨 DM${RESET} ${DIM}<${from}…→${to_short}…>${RESET} ${body}"
|
|
else
|
|
echo -e "${color}${BOLD}[${label}]${RESET} ${BOLD}💬 #${room}${RESET} ${DIM}<${from}…>${RESET} ${body}"
|
|
fi
|
|
;;
|
|
error)
|
|
local msg
|
|
msg=$(echo "$line" | jq -r '.error_message // .' 2>/dev/null)
|
|
echo -e "${color}${BOLD}[${label}]${RESET} ${RED}✗ error:${RESET} ${msg}"
|
|
;;
|
|
*)
|
|
echo -e "${color}${BOLD}[${label}]${RESET} ${DIM}${line}${RESET}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Subscribe to a peer's IPC events in the background, pretty-printing each line.
|
|
subscribe() {
|
|
local color="$1"
|
|
local label="$2"
|
|
local port="$3"
|
|
(
|
|
set +e
|
|
nc 127.0.0.1 "$port" 2>/dev/null | while IFS= read -r line; do
|
|
pretty "$color" "$label" "$line"
|
|
done
|
|
) &
|
|
PIDS+=($!)
|
|
}
|
|
|
|
# Send a single JSON command to a peer's IPC port (fire-and-forget).
|
|
ipc() {
|
|
local port="$1"
|
|
local json="$2"
|
|
echo "$json" | nc -q 0 127.0.0.1 "$port" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
# Query get_state and return a single field via jq from the state_snapshot line.
|
|
# Retries for up to 3 seconds in case the daemon is still initialising.
|
|
# Usage: peer_field <ipc_port> <jq_expr>
|
|
peer_field() {
|
|
local port="$1"
|
|
local expr="$2"
|
|
local result=""
|
|
local n=0
|
|
while [ -z "$result" ] || [ "$result" = "null" ]; do
|
|
result=$(echo '{"type":"get_state"}' \
|
|
| timeout 2 nc 127.0.0.1 "$port" 2>/dev/null \
|
|
| grep '"type":"state_snapshot"' \
|
|
| head -1 \
|
|
| jq -r "$expr" 2>/dev/null || true)
|
|
n=$(( n + 1 ))
|
|
[ "$n" -ge 10 ] && break
|
|
[ -z "$result" ] || [ "$result" = "null" ] && sleep 0.3
|
|
done
|
|
echo "$result"
|
|
}
|
|
|
|
# Wait until a TCP port accepts connections (up to N seconds).
|
|
wait_port() {
|
|
local port="$1"
|
|
local label="$2"
|
|
local n=0
|
|
while ! nc -z 127.0.0.1 "$port" 2>/dev/null; do
|
|
sleep 0.1
|
|
n=$(( n + 1 ))
|
|
if [ "$n" -gt 80 ]; then
|
|
echo -e "${RED}timeout waiting for ${label} on :${port}${RESET}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ── build ─────────────────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo -e "${BOLD}waste-go network test${RESET}"
|
|
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
|
|
echo -e "${DIM}network : ${BOLD}${NETWORK_NAME}${RESET}"
|
|
echo -e "${DIM}data : ${DATA_ROOT}${RESET}"
|
|
echo ""
|
|
|
|
echo -e "${DIM}building binaries…${RESET}"
|
|
go build -o "$DATA_ROOT/bin/waste-anchor" ./cmd/anchor
|
|
go build -o "$DATA_ROOT/bin/waste-daemon" ./cmd/daemon
|
|
echo -e "${GREEN}✓ built${RESET}"
|
|
echo ""
|
|
|
|
# ── anchor ────────────────────────────────────────────────────────────────────
|
|
log "$ANCHOR_COLOR" "anchor" "starting on :${ANCHOR_PORT}"
|
|
"$DATA_ROOT/bin/waste-anchor" -bind "127.0.0.1:${ANCHOR_PORT}" \
|
|
2> >(while IFS= read -r l; do echo -e "${ANCHOR_COLOR}${DIM}[anchor] ${l}${RESET}"; done) &
|
|
PIDS+=($!)
|
|
wait_port "$ANCHOR_PORT" "anchor"
|
|
log "$ANCHOR_COLOR" "anchor" "ready"
|
|
echo ""
|
|
|
|
# ── daemons ───────────────────────────────────────────────────────────────────
|
|
ANCHOR_URL="ws://127.0.0.1:${ANCHOR_PORT}/ws"
|
|
|
|
log "$ALICE_COLOR" "alice" "starting daemon (ipc :${ALICE_IPC})"
|
|
"$DATA_ROOT/bin/waste-daemon" \
|
|
-alias alice \
|
|
-data-dir "$DATA_ROOT/alice" \
|
|
-ipc-port "$ALICE_IPC" \
|
|
-anchor "$ANCHOR_URL" \
|
|
2> >(while IFS= read -r l; do echo -e "${ALICE_COLOR}${DIM}[alice] ${l}${RESET}"; done) &
|
|
PIDS+=($!)
|
|
|
|
log "$BOB_COLOR" "bob" "starting daemon (ipc :${BOB_IPC})"
|
|
"$DATA_ROOT/bin/waste-daemon" \
|
|
-alias bob \
|
|
-data-dir "$DATA_ROOT/bob" \
|
|
-ipc-port "$BOB_IPC" \
|
|
-anchor "$ANCHOR_URL" \
|
|
2> >(while IFS= read -r l; do echo -e "${BOB_COLOR}${DIM}[bob] ${l}${RESET}"; done) &
|
|
PIDS+=($!)
|
|
|
|
log "$CHARLIE_COLOR" "charlie" "starting daemon (ipc :${CHARLIE_IPC})"
|
|
"$DATA_ROOT/bin/waste-daemon" \
|
|
-alias charlie \
|
|
-data-dir "$DATA_ROOT/charlie" \
|
|
-ipc-port "$CHARLIE_IPC" \
|
|
-anchor "$ANCHOR_URL" \
|
|
2> >(while IFS= read -r l; do echo -e "${CHARLIE_COLOR}${DIM}[charlie]${l}${RESET}"; done) &
|
|
PIDS+=($!)
|
|
|
|
wait_port "$ALICE_IPC" "alice"
|
|
wait_port "$BOB_IPC" "bob"
|
|
wait_port "$CHARLIE_IPC" "charlie"
|
|
echo ""
|
|
|
|
# ── subscribe ─────────────────────────────────────────────────────────────────
|
|
echo -e "${DIM}subscribing to IPC event streams…${RESET}"
|
|
subscribe "$ALICE_COLOR" "alice " "$ALICE_IPC"
|
|
subscribe "$BOB_COLOR" "bob " "$BOB_IPC"
|
|
subscribe "$CHARLIE_COLOR" "charlie" "$CHARLIE_IPC"
|
|
sleep 0.3 # let state_snapshot lines arrive
|
|
|
|
# ── resolve peer IDs ──────────────────────────────────────────────────────────
|
|
# Each daemon knows its own id from the state_snapshot.
|
|
ALICE_ID=$(peer_field "$ALICE_IPC" '.local_peer.id')
|
|
BOB_ID=$(peer_field "$BOB_IPC" '.local_peer.id')
|
|
CHARLIE_ID=$(peer_field "$CHARLIE_IPC" '.local_peer.id')
|
|
|
|
log "$ANCHOR_COLOR" "ids" "alice = ${ALICE_ID:0:16}…"
|
|
log "$ANCHOR_COLOR" "ids" "bob = ${BOB_ID:0:16}…"
|
|
log "$ANCHOR_COLOR" "ids" "charlie = ${CHARLIE_ID:0:16}…"
|
|
echo ""
|
|
|
|
# ── join network ──────────────────────────────────────────────────────────────
|
|
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
|
|
echo -e "joining all peers to network: ${BOLD}${NETWORK_NAME}${RESET}"
|
|
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
|
|
|
|
JOIN=$(printf '{"type":"join_network","network_name":"%s"}' "$NETWORK_NAME")
|
|
ipc "$ALICE_IPC" "$JOIN"
|
|
sleep 0.2
|
|
ipc "$BOB_IPC" "$JOIN"
|
|
sleep 0.2
|
|
ipc "$CHARLIE_IPC" "$JOIN"
|
|
|
|
echo -e "${DIM}waiting for ICE / DataChannel setup (up to 10s)…${RESET}"
|
|
sleep 6
|
|
|
|
# ── group chat ────────────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
|
|
echo -e "group chat (#general)"
|
|
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
|
|
|
|
ipc "$ALICE_IPC" '{"type":"send_message","room":"general","body":"hey everyone, alice here"}'
|
|
sleep 0.4
|
|
ipc "$BOB_IPC" '{"type":"send_message","room":"general","body":"hi alice! bob checking in"}'
|
|
sleep 0.4
|
|
ipc "$CHARLIE_IPC" '{"type":"send_message","room":"general","body":"charlie online. the mesh works!"}'
|
|
sleep 0.4
|
|
ipc "$ALICE_IPC" '{"type":"send_message","room":"general","body":"nice — three-way chat over WebRTC 🎉"}'
|
|
sleep 0.4
|
|
ipc "$BOB_IPC" '{"type":"send_message","room":"general","body":"no central server, fully peer-to-peer"}'
|
|
sleep 0.4
|
|
|
|
# ── direct messages ───────────────────────────────────────────────────────────
|
|
# DMs set the "to" field to the recipient's full hex peer ID.
|
|
# The room field is used as "dm:<recipient_id>" by convention so both sides
|
|
# can query their DM history for the same conversation.
|
|
echo ""
|
|
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
|
|
echo -e "direct messages"
|
|
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
|
|
|
|
# Alice → Bob (privately)
|
|
ipc "$ALICE_IPC" "$(jq -cn \
|
|
--arg to "$BOB_ID" \
|
|
--arg room "dm:$BOB_ID" \
|
|
--arg body "hey bob, this is just between us" \
|
|
'{"type":"send_message","room":$room,"body":$body,"to":$to}')"
|
|
sleep 0.4
|
|
|
|
# Bob replies to Alice
|
|
ipc "$BOB_IPC" "$(jq -cn \
|
|
--arg to "$ALICE_ID" \
|
|
--arg room "dm:$ALICE_ID" \
|
|
--arg body "got it alice, loud and clear 🤫" \
|
|
'{"type":"send_message","room":$room,"body":$body,"to":$to}')"
|
|
sleep 0.4
|
|
|
|
# Charlie → Alice (privately)
|
|
ipc "$CHARLIE_IPC" "$(jq -cn \
|
|
--arg to "$ALICE_ID" \
|
|
--arg room "dm:$ALICE_ID" \
|
|
--arg body "alice, can you hear me? charlie dm test" \
|
|
'{"type":"send_message","room":$room,"body":$body,"to":$to}')"
|
|
sleep 0.4
|
|
|
|
# Alice replies to Charlie
|
|
ipc "$ALICE_IPC" "$(jq -cn \
|
|
--arg to "$CHARLIE_ID" \
|
|
--arg room "dm:$CHARLIE_ID" \
|
|
--arg body "yes charlie, DMs working perfectly!" \
|
|
'{"type":"send_message","room":$room,"body":$body,"to":$to}')"
|
|
sleep 0.4
|
|
|
|
# ── leave ─────────────────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
|
|
echo -e "charlie leaves the network"
|
|
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
|
|
ipc "$CHARLIE_IPC" '{"type":"leave_network"}'
|
|
sleep 1
|
|
|
|
# ── persistence check ─────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
|
|
echo -e "verifying persistence (SQLite)"
|
|
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
|
|
for peer in alice bob charlie; do
|
|
db="$DATA_ROOT/$peer/messages.db"
|
|
if [ -f "$db" ]; then
|
|
total=$(sqlite3 "$db" "SELECT COUNT(*) FROM messages;" 2>/dev/null || echo "?")
|
|
group=$(sqlite3 "$db" "SELECT COUNT(*) FROM messages WHERE room='general';" 2>/dev/null || echo "?")
|
|
dms=$(sqlite3 "$db" "SELECT COUNT(*) FROM messages WHERE room LIKE 'dm:%';" 2>/dev/null || echo "?")
|
|
peers_count=$(sqlite3 "$db" "SELECT COUNT(*) FROM peers;" 2>/dev/null || echo "?")
|
|
echo -e " ${BOLD}${peer}${RESET}: ${total} total (${group} group, ${dms} DM), ${peers_count} known peers"
|
|
else
|
|
echo -e " ${RED}${peer}: no messages.db found${RESET}"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
|
|
echo -e "${GREEN}${BOLD}test complete${RESET}"
|
|
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
|
|
sleep 0.5
|