diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go index 510330a..c593944 100644 --- a/internal/ipc/ipc.go +++ b/internal/ipc/ipc.go @@ -160,7 +160,12 @@ func handleClient(conn net.Conn, m *mesh.Mesh, doJoin func(string), doLeave func if err != nil { continue } - m.Broadcast(payload) + if cmd.To != nil { + // DM — send only to the named recipient. + m.SendTo(*cmd.To, payload) + } else { + m.Broadcast(payload) + } m.SaveMessage(msg) m.Emit(proto.IpcMessage{Type: proto.EvtMessageReceived, Message: msg}) diff --git a/test-network.sh b/test-network.sh index dbc8f48..1af2b29 100755 --- a/test-network.sh +++ b/test-network.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -# test-network.sh — spin up anchor + 3 peers, connect them, exchange messages. +# test-network.sh — spin up anchor + 3 peers, connect them, exchange messages + DMs. # Usage: ./test-network.sh -# Requires: go, nc, jq +# Requires: go, nc, jq, sqlite3 set -euo pipefail @@ -32,7 +32,8 @@ cleanup() { kill "$pid" 2>/dev/null || true done wait 2>/dev/null || true - rm -rf "$DATA_ROOT" + 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 @@ -75,11 +76,18 @@ pretty() { echo -e "${color}${BOLD}[${label}]${RESET} ${RED}← peer_disconnected${RESET} ${DIM}${pid}${RESET}" ;; message_received) - local from body room + 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) - echo -e "${color}${BOLD}[${label}]${RESET} ${BOLD}💬 #${room}${RESET} ${DIM}<${from}…>${RESET} ${body}" + 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 @@ -113,6 +121,27 @@ ipc() { 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 +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" @@ -193,8 +222,18 @@ subscribe "$BOB_COLOR" "bob " "$BOB_IPC" subscribe "$CHARLIE_COLOR" "charlie" "$CHARLIE_IPC" sleep 0.3 # let state_snapshot lines arrive -# ── join network ────────────────────────────────────────────────────────────── +# ── 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}" @@ -209,32 +248,63 @@ ipc "$CHARLIE_IPC" "$JOIN" echo -e "${DIM}waiting for ICE / DataChannel setup (up to 10s)…${RESET}" sleep 6 -# ── chat ────────────────────────────────────────────────────────────────────── +# ── group chat ──────────────────────────────────────────────────────────────── echo "" echo -e "${DIM}────────────────────────────────────────────────────────${RESET}" -echo -e "sending messages" +echo -e "group chat (#general)" echo -e "${DIM}────────────────────────────────────────────────────────${RESET}" -ipc "$ALICE_IPC" '{"type":"send_message","room":"general","body":"hey everyone, alice here"}' +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"}' +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 🎉"}' +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"}' +ipc "$BOB_IPC" '{"type":"send_message","room":"general","body":"no central server, fully peer-to-peer"}' sleep 0.4 -# ── state snapshot ──────────────────────────────────────────────────────────── +# ── direct messages ─────────────────────────────────────────────────────────── +# DMs set the "to" field to the recipient's full hex peer ID. +# The room field is used as "dm:" by convention so both sides +# can query their DM history for the same conversation. echo "" echo -e "${DIM}────────────────────────────────────────────────────────${RESET}" -echo -e "requesting state snapshots" +echo -e "direct messages" echo -e "${DIM}────────────────────────────────────────────────────────${RESET}" -ipc "$ALICE_IPC" '{"type":"get_state"}' -ipc "$BOB_IPC" '{"type":"get_state"}' -ipc "$CHARLIE_IPC" '{"type":"get_state"}' -sleep 0.5 + +# 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 "" @@ -244,6 +314,7 @@ echo -e "${DIM}───────────────────── ipc "$CHARLIE_IPC" '{"type":"leave_network"}' sleep 1 +# ── persistence check ───────────────────────────────────────────────────────── echo "" echo -e "${DIM}────────────────────────────────────────────────────────${RESET}" echo -e "verifying persistence (SQLite)" @@ -251,9 +322,11 @@ echo -e "${DIM}───────────────────── for peer in alice bob charlie; do db="$DATA_ROOT/$peer/messages.db" if [ -f "$db" ]; then - count=$(sqlite3 "$db" "SELECT COUNT(*) FROM messages;" 2>/dev/null || echo "?") - peers=$(sqlite3 "$db" "SELECT COUNT(*) FROM peers;" 2>/dev/null || echo "?") - echo -e " ${BOLD}${peer}${RESET}: ${count} messages, ${peers} known peers in messages.db" + 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