diff --git a/buzz/ws-listener/listener-raw.mjs b/buzz/ws-listener/listener-raw.mjs index 88a8d17..8f9e1e8 100644 --- a/buzz/ws-listener/listener-raw.mjs +++ b/buzz/ws-listener/listener-raw.mjs @@ -1,20 +1,34 @@ -// Raw WebSocket listener for Buzz #general — no relay abstraction. -// Handles NIP-42 auth explicitly: on ["AUTH", challenge] we sign kind 22242. -// On a matching kind-9 event from another pubkey: spool to inbox, exit 0. +// Buzz presence + inbox listener (raw WebSocket — no relay abstraction). +// - Persistent authenticated connection (NIP-42 challenge/response). +// - Subscribes to ALL kind-9 chat messages in the workspace (any channel, DMs +// included). On a new message from another pubkey: spool the event to the +// inbox and exit 0 — the ZCode background-task notification wakes the agent. +// - Presence: `buzz users set-presence --status online` heartbeat every 60s +// (relay TTL 180s). Listener alive = zai "online"; dead = offline in ~3 min. +// - Reconnect/expires: exits 2 (closed) or 3 (4h lifetime); the supervisor +// (ZCode background task) restarts it. import { finalizeEvent, getPublicKey } from 'nostr-tools/pure' import fs from 'node:fs' import { execFileSync } from 'node:child_process' const RELAY = 'wss://ch4t.buzz' -const CH = '733ba424-73c7-4170-ac85-ba00d3a80f05' const ME = '4abe5fcdf9695be34bdfc8fe82297aaaf0cf65b5ca92b6c789f6f8783b5ee197' const INBOX = '/home/opc/.buzz-inbox' -const MAX_LIFETIME = 4 * 3600 * 1000 +const STATE = '/home/opc/.buzz-last-seen' +const MAX_LIFETIME = 120 * 1000 // reconnect every 2 min: replays since lastSeen (zombie-socket proof) const keyText = fs.readFileSync('/home/opc/buzz-agent-key.txt', 'utf8') const SK = Uint8Array.from(keyText.match(/SECRET:\s*(\S+)/)[1].match(/.{2}/g).map((h) => parseInt(h, 16))) const MY_PUBKEY = getPublicKey(SK) +function lastSeen() { + try { return parseInt(fs.readFileSync(STATE, 'utf8').trim(), 10) } catch { return Math.floor(Date.now() / 1000) } +} +function saveLastSeen(ts) { + const cur = lastSeen() + if (ts > cur) fs.writeFileSync(STATE, String(ts)) +} + const ws = new WebSocket(RELAY) const seen = new Set() let authEventId = null @@ -23,8 +37,8 @@ let subscribed = false function subscribe() { if (subscribed) return subscribed = true - ws.send(JSON.stringify(['REQ', 'general', { kinds: [9], '#h': [CH], since: Math.floor(Date.now() / 1000) }])) - console.error('subscribed') + ws.send(JSON.stringify(['REQ', 'workspace', { kinds: [9], since: lastSeen() }])) + console.error('subscribed to all kind-9 messages since', lastSeen()) } function sendAuth(challenge) { @@ -58,23 +72,21 @@ ws.onmessage = (msg) => { } if (type === 'NOTICE') { console.error('NOTICE:', rest[0]); return } if (type === 'EVENT') { - const [, subid, evt] = data - if (subid !== 'general') return - if (evt.pubkey === ME || seen.has(evt.id)) return + const evt = rest[1] + if (!evt || evt.pubkey === ME || seen.has(evt.id)) return seen.add(evt.id) + saveLastSeen(evt.created_at + 1) fs.appendFileSync(INBOX, JSON.stringify(evt) + '\n') console.error('message received from', evt.pubkey.slice(0, 10)) process.exit(0) } - if (type === 'EOSE') console.error('EOSE — waiting for live events') + if (type === 'EOSE') console.error('EOSE — live') } ws.onclose = () => { console.error('ws closed'); process.exit(2) } ws.onerror = () => {} setTimeout(() => { console.error('lifetime elapsed'); process.exit(3) }, MAX_LIFETIME) -// Presence heartbeat: relay TTL is 180s, contract says refresh every 60s. -// Listener alive = zai "online"; backend dead = offline within 3 minutes. const BUZZ_CLI = '/usr/local/bin/buzz' const heartbeat = () => { try {