buzz listener: instant emoji receipt (kind 7) + kind 20002 working indicator on every message
This commit is contained in:
parent
db9a520dd3
commit
ee331db179
1 changed files with 51 additions and 8 deletions
|
|
@ -13,8 +13,8 @@ import { execFileSync } from 'node:child_process'
|
|||
|
||||
const RELAY = 'wss://ch4t.buzz'
|
||||
const ME = '4abe5fcdf9695be34bdfc8fe82297aaaf0cf65b5ca92b6c789f6f8783b5ee197'
|
||||
const DM_CHANNEL = '4113fff6-f288-46af-927e-094b5a8386f2'
|
||||
const INBOX = '/home/opc/.buzz-inbox'
|
||||
const REACTED = '/home/opc/.buzz-reacted'
|
||||
const STATE = '/home/opc/.buzz-last-seen'
|
||||
const MAX_LIFETIME = 120 * 1000 // reconnect every 2 min: replays since lastSeen (zombie-socket proof)
|
||||
|
||||
|
|
@ -32,6 +32,32 @@ function saveLastSeen(ts) {
|
|||
|
||||
const ws = new WebSocket(RELAY)
|
||||
const seen = new Set()
|
||||
const typingIntervals = new Map() // channel -> interval publishing kind 20002
|
||||
const TYPING_PUBLISH_MS = 3000
|
||||
const TYPING_MAX_MS = 120 * 1000 // hard stop: never type longer than this
|
||||
|
||||
function startTyping(ch) {
|
||||
stopTyping(ch) // reset both the interval and the max-duration timer
|
||||
const publish = () => {
|
||||
try {
|
||||
const ev = finalizeEvent(
|
||||
{ kind: 20002, created_at: Math.floor(Date.now() / 1000), tags: [['h', ch]], content: '' },
|
||||
SK
|
||||
)
|
||||
ws.send(JSON.stringify(['EVENT', ev]))
|
||||
} catch {}
|
||||
}
|
||||
publish()
|
||||
typingIntervals.set(ch, {
|
||||
iv: setInterval(publish, TYPING_PUBLISH_MS),
|
||||
max: setTimeout(() => stopTyping(ch), TYPING_MAX_MS),
|
||||
})
|
||||
console.error('typing indicator on:', ch.slice(0, 8))
|
||||
}
|
||||
function stopTyping(ch) {
|
||||
const t = typingIntervals.get(ch)
|
||||
if (t) { clearInterval(t.iv); clearTimeout(t.max); typingIntervals.delete(ch) }
|
||||
}
|
||||
let authEventId = null
|
||||
let subscribed = false
|
||||
|
||||
|
|
@ -74,19 +100,36 @@ ws.onmessage = (msg) => {
|
|||
if (type === 'NOTICE') { console.error('NOTICE:', rest[0]); return }
|
||||
if (type === 'EVENT') {
|
||||
const evt = rest[1]
|
||||
if (!evt || evt.pubkey === ME || seen.has(evt.id)) return
|
||||
if (!evt) return
|
||||
if (evt.pubkey === ME) {
|
||||
const chTag = (evt.tags.find((t) => t[0] === 'h') || [])[1]
|
||||
if (chTag) stopTyping(chTag)
|
||||
return
|
||||
}
|
||||
if (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))
|
||||
// Instant DM receipt: the sender should never wonder if we're alive.
|
||||
const chTag = (evt.tags.find((t) => t[0] === 'h') || [])[1] || ''
|
||||
if (chTag === DM_CHANNEL) {
|
||||
const inCh = (evt.tags.find((t) => t[0] === 'h') || [])[1]
|
||||
if (inCh) startTyping(inCh)
|
||||
// Instant receipt: emoji reaction (NIP-25 kind 7) on the sender's message,
|
||||
// so the sender always sees that zai is alive — before any LLM work starts.
|
||||
let reacted = false
|
||||
try { reacted = fs.readFileSync(REACTED, 'utf8').split('\n').includes(evt.id) } catch {}
|
||||
if (!reacted) {
|
||||
try {
|
||||
execFileSync(BUZZ_CLI, ['messages', 'send', '--channel', chTag, '--reply-to', evt.id, '--content', '✓ received — will respond in a moment'], { timeout: 15000, stdio: 'ignore' })
|
||||
} catch {}
|
||||
execFileSync(BUZZ_CLI, ['reactions', 'add', '--event', evt.id, '--emoji', '👀'], {
|
||||
env: { ...process.env, BUZZ_RELAY_URL: 'https://ch4t.buzz', BUZZ_PRIVATE_KEY: keyText.match(/SECRET:\s*(\S+)/)[1] },
|
||||
stdio: 'ignore',
|
||||
timeout: 15000,
|
||||
})
|
||||
fs.appendFileSync(REACTED, evt.id + '\n')
|
||||
console.error('👀 receipt sent')
|
||||
} catch (e) {
|
||||
console.error('reaction failed:', String(e).slice(0, 100))
|
||||
}
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
if (type === 'EOSE') console.error('EOSE — live')
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue