buzz listener: since-based replay + 2min reconnect cycle (zombie-socket proof); DM coverage verified
This commit is contained in:
parent
9fd63b176b
commit
6fa28abe20
1 changed files with 25 additions and 13 deletions
|
|
@ -1,20 +1,34 @@
|
||||||
// Raw WebSocket listener for Buzz #general — no relay abstraction.
|
// Buzz presence + inbox listener (raw WebSocket — no relay abstraction).
|
||||||
// Handles NIP-42 auth explicitly: on ["AUTH", challenge] we sign kind 22242.
|
// - Persistent authenticated connection (NIP-42 challenge/response).
|
||||||
// On a matching kind-9 event from another pubkey: spool to inbox, exit 0.
|
// - 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 { finalizeEvent, getPublicKey } from 'nostr-tools/pure'
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import { execFileSync } from 'node:child_process'
|
import { execFileSync } from 'node:child_process'
|
||||||
|
|
||||||
const RELAY = 'wss://ch4t.buzz'
|
const RELAY = 'wss://ch4t.buzz'
|
||||||
const CH = '733ba424-73c7-4170-ac85-ba00d3a80f05'
|
|
||||||
const ME = '4abe5fcdf9695be34bdfc8fe82297aaaf0cf65b5ca92b6c789f6f8783b5ee197'
|
const ME = '4abe5fcdf9695be34bdfc8fe82297aaaf0cf65b5ca92b6c789f6f8783b5ee197'
|
||||||
const INBOX = '/home/opc/.buzz-inbox'
|
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 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 SK = Uint8Array.from(keyText.match(/SECRET:\s*(\S+)/)[1].match(/.{2}/g).map((h) => parseInt(h, 16)))
|
||||||
const MY_PUBKEY = getPublicKey(SK)
|
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 ws = new WebSocket(RELAY)
|
||||||
const seen = new Set()
|
const seen = new Set()
|
||||||
let authEventId = null
|
let authEventId = null
|
||||||
|
|
@ -23,8 +37,8 @@ let subscribed = false
|
||||||
function subscribe() {
|
function subscribe() {
|
||||||
if (subscribed) return
|
if (subscribed) return
|
||||||
subscribed = true
|
subscribed = true
|
||||||
ws.send(JSON.stringify(['REQ', 'general', { kinds: [9], '#h': [CH], since: Math.floor(Date.now() / 1000) }]))
|
ws.send(JSON.stringify(['REQ', 'workspace', { kinds: [9], since: lastSeen() }]))
|
||||||
console.error('subscribed')
|
console.error('subscribed to all kind-9 messages since', lastSeen())
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendAuth(challenge) {
|
function sendAuth(challenge) {
|
||||||
|
|
@ -58,23 +72,21 @@ ws.onmessage = (msg) => {
|
||||||
}
|
}
|
||||||
if (type === 'NOTICE') { console.error('NOTICE:', rest[0]); return }
|
if (type === 'NOTICE') { console.error('NOTICE:', rest[0]); return }
|
||||||
if (type === 'EVENT') {
|
if (type === 'EVENT') {
|
||||||
const [, subid, evt] = data
|
const evt = rest[1]
|
||||||
if (subid !== 'general') return
|
if (!evt || evt.pubkey === ME || seen.has(evt.id)) return
|
||||||
if (evt.pubkey === ME || seen.has(evt.id)) return
|
|
||||||
seen.add(evt.id)
|
seen.add(evt.id)
|
||||||
|
saveLastSeen(evt.created_at + 1)
|
||||||
fs.appendFileSync(INBOX, JSON.stringify(evt) + '\n')
|
fs.appendFileSync(INBOX, JSON.stringify(evt) + '\n')
|
||||||
console.error('message received from', evt.pubkey.slice(0, 10))
|
console.error('message received from', evt.pubkey.slice(0, 10))
|
||||||
process.exit(0)
|
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.onclose = () => { console.error('ws closed'); process.exit(2) }
|
||||||
ws.onerror = () => {}
|
ws.onerror = () => {}
|
||||||
|
|
||||||
setTimeout(() => { console.error('lifetime elapsed'); process.exit(3) }, MAX_LIFETIME)
|
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 BUZZ_CLI = '/usr/local/bin/buzz'
|
||||||
const heartbeat = () => {
|
const heartbeat = () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue