From 5a05e6e1ef8c2381ef20c33d23a1acaa7d092dda Mon Sep 17 00:00:00 2001 From: Fredrik Johansson Date: Sun, 21 Jun 2026 18:13:27 +0200 Subject: [PATCH] Add DM support to test script; fix DM routing and JSON encoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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:" 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 rather than πŸ’¬ #room. - Persistence check now breaks down totals by group vs DM message count. Co-Authored-By: Claude Sonnet 4.6 --- internal/ipc/ipc.go | 7 ++- test-network.sh | 115 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 100 insertions(+), 22 deletions(-) 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