/**
* 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
}
}