OverviewFilesCommitsIssuesDocsReleasesPeers

keet-daemon / lib / topic-announce.js

151 lines · javascript ref:
/**
 * 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 }
  }
}