Add DM support to test script; fix DM routing and JSON encoding

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>
This commit is contained in:
Fredrik Johansson
2026-06-21 18:13:27 +02:00
parent cde611b261
commit 5a05e6e1ef
2 changed files with 100 additions and 22 deletions

View File

@@ -160,7 +160,12 @@ func handleClient(conn net.Conn, m *mesh.Mesh, doJoin func(string), doLeave func
if err != nil {
continue
}
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})

View File

@@ -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)
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 <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"
@@ -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,10 +248,10 @@ 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"}'
@@ -226,15 +265,46 @@ sleep 0.4
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:<recipient_id>" 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