/**
* topic-announce.js — DHT announce/search for discoverable rooms
*
* Rooms with discoverable: true in _hs:room-meta announce on
* topic-hashed DHT keys. Search by topic to find rooms across
* the network without shared registry membership.
*
* Uses keet-core's existing DHT for both announce/lookup and
* mutablePut/mutableGet (room metadata). No separate swarm needed.
*
* Flow:
* announce: mutablePut(roomKeyPair, metadata) + dht.announce(topicKey, roomKeyPair)
* search: dht.lookup(topicKey) → publicKeys → mutableGet(pk) → metadata
*/
import crypto from 'hypercore-crypto'
const TOPIC_PREFIX = 'hyper-social:topic:'
export class TopicAnnounce {
constructor (core, backend, roomMeta) {
this._core = core
this._backend = backend
this._roomMeta = roomMeta
this._announced = new Map() // roomId -> { keyPair, topics }
}
_topicKey (topic) {
return crypto.hash(Buffer.from(TOPIC_PREFIX + topic.toLowerCase().trim()))
}
_roomKeyPair (roomId) {
const seed = crypto.hash(Buffer.from('hyper-social:announce:' + roomId))
return crypto.keyPair(seed)
}
_getDht () {
const swarm = this._core.swarm
if (!swarm || !swarm.dht) throw new Error('DHT not available (swarm not ready)')
return swarm.dht
}
async announce (roomId) {
const meta = await this._roomMeta.get(roomId)
if (!meta.discoverable) throw new Error('Room not discoverable')
const topics = meta.topics || []
if (!topics.length) throw new Error('No topics set')
const dht = this._getDht()
const keyPair = this._roomKeyPair(roomId)
// Get room config + stats
let title = '', description = ''
let memberCount = 0, creationTime = 0, lastActivity = 0, messageCount = 0, protocolVersion = 0
try {
const c = await this._backend.api.core.getConfig(roomId)
title = c?.title || ''
description = c?.description || ''
} catch {}
try {
const info = await this._backend.api.core.getRoomInfo(roomId)
if (info) {
memberCount = info.stats?.members?.total || 0
creationTime = info.digest?.creationTime || 0
lastActivity = info.stats?.chat?.timestamp || 0
messageCount = info.stats?.chat?.clock || 0
protocolVersion = info.digest?.abi?.version || 0
}
} catch {}
let inviteToken = ''
try {
const inv = await this._backend.api.core.createInvitation(roomId, {})
inviteToken = typeof inv === 'string' ? inv : (inv?.token || '')
} catch {}
// Store metadata as mutable DHT record (keyed by roomKeyPair.publicKey)
const value = Buffer.from(JSON.stringify({
roomId, title, description, topics, inviteToken, ts: Date.now(),
memberCount, creationTime, lastActivity, messageCount, protocolVersion
}))
await dht.mutablePut(keyPair, value, { seq: Math.floor(Date.now() / 1000) })
// Announce on each topic key so lookup() finds us
for (const t of topics) {
await dht.announce(this._topicKey(t), keyPair, []).finished()
}
this._announced.set(roomId, { keyPair, topics })
return { ok: true, topics }
}
async unannounce (roomId) {
const entry = this._announced.get(roomId)
if (!entry) return { ok: true }
const dht = this._getDht()
for (const t of entry.topics) {
await dht.unannounce(this._topicKey(t), entry.keyPair)
}
this._announced.delete(roomId)
return { ok: true }
}
async search (query) {
const dht = this._getDht()
const tk = this._topicKey(query)
const results = []
const seen = new Set()
for await (const data of dht.lookup(tk)) {
for (const peer of (data.peers || [])) {
const pkHex = peer.publicKey.toString('hex')
if (seen.has(pkHex)) continue
seen.add(pkHex)
try {
const record = await dht.mutableGet(peer.publicKey, { latest: true })
if (record && record.value) {
const meta = JSON.parse(record.value.toString())
if (meta.roomId) results.push(meta)
}
} catch {}
}
}
return results
}
async announceAll () {
const result = await this._backend.api.core.getRecentRooms()
const rooms = Array.isArray(result) ? result : (result?.rooms || [])
let count = 0
for (const room of rooms) {
const id = room.roomId || room.id || room.key
if (!id) continue
try {
const meta = await this._roomMeta.get(id)
if (meta.discoverable) {
await this.announce(id)
count++
}
} catch {}
}
return { announced: count }
}
}