OverviewFilesCommitsIssuesDocsReleasesPeers

keet-daemon / lib / registry-room.js

145 lines · javascript ref:
/**
 * registry-room.js — _hs:room-listing messages in registry rooms
 *
 * A "registry room" is a normal keet room with _hs:room-meta type "registry".
 * Rooms are listed/unlisted via structured messages, replacing Autobase registries.
 *
 * Usage:
 *   const reg = new RegistryRoom(backend)
 *   await reg.add(registryRoomId, { roomId, title, ... })
 *   await reg.remove(registryRoomId, { roomId })
 *   await reg.list(registryRoomId)    → [{ roomId, title, ... }]
 *   await reg.rooms()                 → [{ roomId, title }]  (all registry-type rooms)
 */

import { createHsMessage, parseHsMessage } from './room-meta.js'

export class RegistryRoom {
  constructor (backend, roomMeta) {
    this.backend = backend
    this._roomMeta = roomMeta
  }

  /**
   * List a room in a registry. Posts an _hs:room-listing message.
   */
  async add (registryRoomId, { roomId, title, description, topics, inviteToken, listedBy }) {
    if (!registryRoomId) throw new Error('Missing registryRoomId')
    if (!roomId) throw new Error('Missing roomId')

    const payload = createHsMessage('room-listing', {
      roomId,
      title: title || '',
      description: description || '',
      topics: topics || [],
      inviteToken: inviteToken || '',
      listedBy: listedBy || '',
      listedAt: Date.now()
    })

    const result = await this.backend.api.core.addChatMessage(registryRoomId, payload, {})
    return result || { ok: true }
  }

  /**
   * Unlist a room from a registry. Posts an _hs:room-unlisting tombstone.
   */
  async remove (registryRoomId, { roomId, unlistedBy }) {
    if (!registryRoomId) throw new Error('Missing registryRoomId')
    if (!roomId) throw new Error('Missing roomId')

    const payload = createHsMessage('room-unlisting', {
      roomId,
      unlistedBy: unlistedBy || '',
      unlistedAt: Date.now()
    })

    await this.backend.api.core.addChatMessage(registryRoomId, payload, {})
    return { ok: true }
  }

  /**
   * List all rooms in a registry. Reads messages, dedupes, applies tombstones.
   * Optional query string filters by title/description/topics.
   */
  async list (registryRoomId, query) {
    if (!registryRoomId) throw new Error('Missing registryRoomId')

    const result = await this.backend.api.core.getChatMessages(registryRoomId, {
      limit: 500,
      reverse: true
    })
    const messages = Array.isArray(result) ? result : (result?.messages || result?.data || [])

    const listings = new Map() // roomId -> listing data
    const unlisted = new Set() // roomId set of tombstoned rooms

    // Process newest-first: first occurrence wins (most recent state)
    for (const msg of messages) {
      if (msg.deleted) continue
      const text = msg.chat?.text || msg.text || msg.message?.text || ''
      const parsed = parseHsMessage(text)
      if (!parsed) continue

      if (parsed._hs === 'room-unlisting' && parsed.roomId) {
        if (!listings.has(parsed.roomId)) {
          unlisted.add(parsed.roomId)
        }
      } else if (parsed._hs === 'room-listing' && parsed.roomId) {
        if (!listings.has(parsed.roomId) && !unlisted.has(parsed.roomId)) {
          listings.set(parsed.roomId, {
            roomId: parsed.roomId,
            title: parsed.title || '',
            description: parsed.description || '',
            topics: parsed.topics || [],
            inviteToken: parsed.inviteToken || '',
            listedBy: parsed.listedBy || '',
            listedAt: parsed.listedAt || null,
            messageId: msg.id || null
          })
        }
      }
    }

    let rooms = [...listings.values()]

    // Optional query filter
    if (query && query.trim()) {
      const q = query.toLowerCase()
      rooms = rooms.filter(r =>
        r.title.toLowerCase().includes(q) ||
        r.description.toLowerCase().includes(q) ||
        r.topics.some(t => t.toLowerCase().includes(q))
      )
    }

    return rooms
  }

  /**
   * Find all rooms the user has joined that are registry-type rooms.
   * Checks _hs:room-meta for type === "registry".
   */
  async rooms () {
    const result = await this.backend.api.core.getRecentRooms()
    const allRooms = Array.isArray(result) ? result : (result?.rooms || result?.data || [])

    const registries = []
    await Promise.all(allRooms.map(async (room) => {
      const roomId = room.roomId || room.id || room.key
      if (!roomId) return
      try {
        const meta = await this._roomMeta.get(roomId)
        if (meta.type === 'registry') {
          registries.push({
            roomId,
            title: room.title || room.name || '(untitled)'
          })
        }
      } catch {}
    }))

    return registries
  }
}