feat(contacts): broadcast and ingest friend presence on contacts mesh

Add PRESENCE_UPDATE gossip on pearcord:contacts:{userId} so accepted
friends see online/idle/dnd/offline without joining each other's guilds.

- ingestFriendPresence map with TTL-friendly updates
- broadcastPresence on setPresence and mesh rejoin (_lastSelfPresence)
- getFriendPresenceMap for platform listContacts enrichment
- simulateGossip handles PRESENCE_UPDATE for CI smokes

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-05-21 21:03:44 -04:00
co-authored by Cursor
parent dfab588145
commit 3add6b4207
+46 -1
View File
@@ -5,7 +5,7 @@ const path = require('bare-path')
const EventEmitter = require('bare-events') const EventEmitter = require('bare-events')
const b4a = require('b4a') const b4a = require('b4a')
const { JsonStore } = require('pearcord-db/store-json') const { JsonStore } = require('pearcord-db/store-json')
const { RPC, contactsTopic, topicToBuffer, id, now } = require('pearcord-shared') const { RPC, contactsTopic, topicToBuffer, id, now, USER_STATUS } = require('pearcord-shared')
const { attachContactsMesh, broadcastContactsGossip } = require('./mesh') const { attachContactsMesh, broadcastContactsGossip } = require('./mesh')
const CONTACTS_COLLECTION = '@pearcord/contacts' const CONTACTS_COLLECTION = '@pearcord/contacts'
@@ -34,6 +34,8 @@ class PearcordContacts extends EventEmitter {
this._channels = new Map() this._channels = new Map()
this._meshJoined = false this._meshJoined = false
this._ephemeralTopics = new Set() this._ephemeralTopics = new Set()
this._friendPresence = new Map()
this._lastSelfPresence = null
} }
async ready () { async ready () {
@@ -283,7 +285,46 @@ class PearcordContacts extends EventEmitter {
return null return null
} }
ingestFriendPresence (payload) {
if (!payload?.userId || payload.userId === this.userId) return null
const row = {
userId: payload.userId,
status: payload.status || USER_STATUS.ONLINE,
customStatus: payload.customStatus || null,
at: payload.at || now()
}
this._friendPresence.set(payload.userId, row)
this.emit('friend-presence', row)
return row
}
broadcastPresence (payload) {
if (!payload?.userId) return null
this._lastSelfPresence = {
userId: payload.userId,
status: payload.status || USER_STATUS.ONLINE,
customStatus: payload.customStatus || null,
at: payload.at || now()
}
if (this._meshJoined) {
broadcastContactsGossip(this, RPC.PRESENCE_UPDATE, this._lastSelfPresence)
}
return this._lastSelfPresence
}
getFriendPresenceMap () {
const out = {}
for (const [uid, row] of this._friendPresence) {
out[uid] = row
}
return out
}
_onGossip (method, payload) { _onGossip (method, payload) {
if (method === RPC.PRESENCE_UPDATE) {
this.ingestFriendPresence(payload)
return
}
if (method === RPC.CONTACT_REQUEST) { if (method === RPC.CONTACT_REQUEST) {
this.ingestRequest(payload).catch(() => {}) this.ingestRequest(payload).catch(() => {})
} else if (method === RPC.CONTACT_ACCEPT) { } else if (method === RPC.CONTACT_ACCEPT) {
@@ -300,6 +341,7 @@ class PearcordContacts extends EventEmitter {
} }
async simulateGossip (method, payload) { async simulateGossip (method, payload) {
if (method === RPC.PRESENCE_UPDATE) return this.ingestFriendPresence(payload)
if (method === RPC.CONTACT_REQUEST) return this.ingestRequest(payload) if (method === RPC.CONTACT_REQUEST) return this.ingestRequest(payload)
if (method === RPC.CONTACT_ACCEPT) return this.ingestAccept(payload) if (method === RPC.CONTACT_ACCEPT) return this.ingestAccept(payload)
if (method === RPC.CONTACT_DECLINE) return this.ingestDecline(payload) if (method === RPC.CONTACT_DECLINE) return this.ingestDecline(payload)
@@ -366,6 +408,9 @@ class PearcordContacts extends EventEmitter {
}) })
await this._flushSwarm(3500) await this._flushSwarm(3500)
this._meshJoined = true this._meshJoined = true
if (this._lastSelfPresence) {
broadcastContactsGossip(this, RPC.PRESENCE_UPDATE, this._lastSelfPresence)
}
return this return this
} }