OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / tests / auto-populate-meta.mjs

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