/**
* auto-populate-meta.mjs — Auto-populate metadata for all rooms
*
* For each room:
* - Fetches title, type, member count from keet-core
* - Sets discoverable, topics, language, category based on room info
* - Skips DMs and special rooms (_test-suite, admin channel)
* - Merges with existing metadata (doesn't overwrite unless --force)
*
* Usage:
* bare tests/auto-populate-meta.mjs # dry run (preview changes)
* bare tests/auto-populate-meta.mjs --apply # apply changes
* bare tests/auto-populate-meta.mjs --force --apply # overwrite existing
*/
import Pipe from 'bare-pipe'
import FramedStream from 'framed-stream'
import b4a from 'b4a'
const SOCKET = '/tmp/keet-daemon.sock'
const ADMIN_CHANNEL = '98q5xy39rsibzjaxt6bjau5y9qfcjzk6phird399tpbq377uzkio'
const TEST_SUITE = '_test-suite'
const APPLY = Bare.argv.includes('--apply')
const FORCE = Bare.argv.includes('--force') // overwrite existing metadata
let rpcId = 0
function connect () {
const pipe = new Pipe(SOCKET)
const framed = new FramedStream(pipe)
const pending = new Map()
framed.on('data', (buf) => {
try {
const msg = JSON.parse(b4a.toString(buf))
const cb = pending.get(msg.id)
if (cb) {
pending.delete(msg.id)
if (msg.ok !== undefined && msg.data !== undefined) msg.result = msg.data
cb(msg)
}
} catch {}
})
function call (method, params = {}) {
return new Promise((resolve, reject) => {
const id = ++rpcId
const timeout = setTimeout(() => {
pending.delete(id)
reject(new Error(`RPC timeout: ${method}`))
}, 15000)
pending.set(id, (msg) => {
clearTimeout(timeout)
if (msg.error) reject(new Error(msg.error.message || msg.error))
else resolve(msg.result)
})
const payload = JSON.stringify({ jsonrpc: '2.0', id, method, params })
framed.write(b4a.from(payload))
})
}
function close () { framed.destroy(); pipe.destroy() }
return { call, close }
}
// Infer topics from room title + member count
function inferTopics (title, roomType, memberCount) {
if (!title || title === '(untitled)') {
// For untitled rooms, use member count to differentiate
if (memberCount <= 2) return ['private']
if (memberCount <= 5) return ['small-group']
return ['community']
}
const lower = title.toLowerCase()
const topics = []
// Category detection — order by specificity
if (lower.includes('test') || lower.includes('_test')) topics.push('testing')
if (lower.includes('lobby') || lower.includes('general') || lower.includes('chat')) topics.push('social')
if (lower.includes('ai') || lower.includes('bot') || lower.includes('clode') || lower.includes('claude')) topics.push('ai')
if (lower.includes('invite') || lower.includes('welcome') || lower.includes('onboard')) topics.push('onboarding')
if (lower.includes('dev') || lower.includes('code') || lower.includes('hack') || lower.includes('build')) topics.push('development')
if (lower.includes('game') || lower.includes('godot') || lower.includes('play')) topics.push('gaming')
if (lower.includes('p2p') || lower.includes('hyper') || lower.includes('pear') || lower.includes('keet')) topics.push('p2p')
if (lower.includes('music') || lower.includes('art') || lower.includes('media')) topics.push('media')
if (lower.includes('wallet') || lower.includes('crypto') || lower.includes('bitcoin') || lower.includes('btc')) topics.push('crypto')
if (topics.length === 0) {
if (memberCount <= 2) topics.push('private')
else if (memberCount <= 5) topics.push('small-group')
else topics.push('community')
}
return topics
}
// Infer category from topics
function inferCategory (topics) {
if (topics.includes('testing')) return 'testing'
if (topics.includes('onboarding')) return 'onboarding'
if (topics.includes('development')) return 'development'
if (topics.includes('gaming')) return 'gaming'
if (topics.includes('ai')) return 'ai'
if (topics.includes('p2p')) return 'p2p'
if (topics.includes('crypto')) return 'crypto'
if (topics.includes('media')) return 'media'
if (topics.includes('social')) return 'social'
if (topics.includes('private')) return 'private'
if (topics.includes('small-group')) return 'small-group'
if (topics.includes('community')) return 'community'
return 'general'
}
// Normalize stale category values
function normalizeCategory (cat) {
if (cat === 'test') return 'testing' // fix inconsistency
return cat
}
async function run () {
console.log(`\n=== Auto-Populate Room Metadata ${APPLY ? '(APPLYING)' : '(DRY RUN)'}${FORCE ? ' [FORCE]' : ''} ===\n`)
const client = connect()
const rooms = await client.call('rooms')
let updated = 0
let skipped = 0
for (const room of rooms) {
const id = room.roomId || room.id
const title = room.title || '(untitled)'
// Get room details
let info
try {
info = await client.call('room', { roomId: id })
} catch {
console.log(` SKIP: "${title}" — cannot get room info`)
skipped++
continue
}
const roomType = info.digest?.settings?.roomType
// Skip DMs
if (roomType === 3) {
console.log(` SKIP: "${title}" — DM room`)
skipped++
continue
}
// Skip test suite room
if (title === TEST_SUITE) {
console.log(` SKIP: "${title}" — test suite room`)
skipped++
continue
}
// Skip admin channel (but DO set its metadata)
const isAdmin = id === ADMIN_CHANNEL
// Get member count for better inference
let memberCount = 0
try {
const mResult = await client.call('members', { roomId: id })
const members = Array.isArray(mResult) ? mResult : (mResult?.members || mResult?.data || [])
memberCount = members.length
} catch {}
// Get existing metadata
let existing = {}
try {
existing = await client.call('meta.get', { roomId: id })
} catch {}
// Build metadata update
const update = {}
let changes = []
// Topics: set if empty/missing OR force
if (FORCE || !existing.topics || existing.topics.length === 0) {
if (isAdmin) {
update.topics = ['admin', 'system']
} else {
update.topics = inferTopics(title, roomType, memberCount)
}
changes.push(`topics=[${update.topics.join(', ')}]`)
}
// Discoverable: set if missing OR force
if (FORCE || existing.discoverable === undefined || existing.discoverable === null) {
update.discoverable = !isAdmin
changes.push(`discoverable=${update.discoverable}`)
}
// Language: set if missing OR force
if (FORCE || !existing.language) {
update.language = 'en'
changes.push('language=en')
}
// Category: normalize stale values, infer from topics
const existingCat = existing.category ? normalizeCategory(existing.category) : null
if (FORCE || !existingCat) {
const topics = update.topics || existing.topics || ['general']
update.category = isAdmin ? 'system' : inferCategory(topics)
changes.push(`category=${update.category}`)
} else if (existingCat !== existing.category) {
// Category was normalized (e.g. "test" → "testing")
update.category = existingCat
changes.push(`category=${existingCat} (normalized)`)
}
if (changes.length === 0) {
console.log(` OK: "${title}" — already populated`)
skipped++
continue
}
console.log(` ${APPLY ? 'SET' : 'WOULD SET'}: "${title}" (${id.slice(0, 12)}...) [${memberCount} members]`)
console.log(` ${changes.join(', ')}`)
if (APPLY) {
try {
await client.call('meta.set', { roomId: id, metadata: update })
updated++
} catch (err) {
console.log(` ERROR: ${err.message}`)
}
} else {
updated++
}
}
console.log(`\n=== RESULT ===`)
console.log(` ${APPLY ? 'Updated' : 'Would update'}: ${updated}`)
console.log(` Skipped: ${skipped}`)
console.log(` Total rooms: ${rooms.length}`)
if (!APPLY && updated > 0) {
console.log(`\nRun with --apply to execute: bare tests/auto-populate-meta.mjs --apply`)
console.log(`Add --force to overwrite existing: bare tests/auto-populate-meta.mjs --force --apply`)
}
client.close()
setTimeout(() => Bare.exit(0), 500)
}
run().catch(err => { console.error('FATAL:', err.message); Bare.exit(1) })