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 RELAY = 'wss://ch4t.buzz'
|
||||||
const ME = '4abe5fcdf9695be34bdfc8fe82297aaaf0cf65b5ca92b6c789f6f8783b5ee197'
|
const ME = '4abe5fcdf9695be34bdfc8fe82297aaaf0cf65b5ca92b6c789f6f8783b5ee197'
|
||||||
const DM_CHANNEL = '4113fff6-f288-46af-927e-094b5a8386f2'
|
|
||||||
const INBOX = '/home/opc/.buzz-inbox'
|
const INBOX = '/home/opc/.buzz-inbox'
|
||||||
|
const REACTED = '/home/opc/.buzz-reacted'
|
||||||
const STATE = '/home/opc/.buzz-last-seen'
|
const STATE = '/home/opc/.buzz-last-seen'
|
||||||
const MAX_LIFETIME = 120 * 1000 // reconnect every 2 min: replays since lastSeen (zombie-socket proof)
|
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 ws = new WebSocket(RELAY)
|
||||||
const seen = new Set()
|
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 authEventId = null
|
||||||
let subscribed = false
|
let subscribed = false
|
||||||
|
|
||||||
|
|
@ -74,19 +100,36 @@ 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 evt = rest[1]
|
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)
|
seen.add(evt.id)
|
||||||
saveLastSeen(evt.created_at + 1)
|
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))
|
||||||
// Instant DM receipt: the sender should never wonder if we're alive.
|
const inCh = (evt.tags.find((t) => t[0] === 'h') || [])[1]
|
||||||
const chTag = (evt.tags.find((t) => t[0] === 'h') || [])[1] || ''
|
if (inCh) startTyping(inCh)
|
||||||
if (chTag === DM_CHANNEL) {
|
// 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 {
|
try {
|
||||||
execFileSync(BUZZ_CLI, ['messages', 'send', '--channel', chTag, '--reply-to', evt.id, '--content', '✓ received — will respond in a moment'], { timeout: 15000, stdio: 'ignore' })
|
execFileSync(BUZZ_CLI, ['reactions', 'add', '--event', evt.id, '--emoji', '👀'], {
|
||||||
} catch {}
|
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')
|
if (type === 'EOSE') console.error('EOSE — live')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue