| … | … | @@ -0,0 +1,248 @@ |
| 1 | /** |
| 2 | * auto-populate-meta.mjs — Auto-populate metadata for all rooms |
| 3 | * |
| 4 | * For each room: |
| 5 | * - Fetches title, type, member count from keet-core |
| 6 | * - Sets discoverable, topics, language, category based on room info |
| 7 | * - Skips DMs and special rooms (_test-suite, admin channel) |
| 8 | * - Merges with existing metadata (doesn't overwrite unless --force) |
| 9 | * |
| 10 | * Usage: |
| 11 | * bare tests/auto-populate-meta.mjs # dry run (preview changes) |
| 12 | * bare tests/auto-populate-meta.mjs --apply # apply changes |
| 13 | * bare tests/auto-populate-meta.mjs --force --apply # overwrite existing |
| 14 | */ |
| 15 | |
| 16 | import Pipe from 'bare-pipe' |
| 17 | import FramedStream from 'framed-stream' |
| 18 | import b4a from 'b4a' |
| 19 | |
| 20 | const SOCKET = '/tmp/keet-daemon.sock' |
| 21 | const ADMIN_CHANNEL = '98q5xy39rsibzjaxt6bjau5y9qfcjzk6phird399tpbq377uzkio' |
| 22 | const TEST_SUITE = '_test-suite' |
| 23 | const APPLY = Bare.argv.includes('--apply') |
| 24 | const FORCE = Bare.argv.includes('--force') // overwrite existing metadata |
| 25 | let rpcId = 0 |
| 26 | |
| 27 | function connect () { |
| 28 | const pipe = new Pipe(SOCKET) |
| 29 | const framed = new FramedStream(pipe) |
| 30 | const pending = new Map() |
| 31 | |
| 32 | framed.on('data', (buf) => { |
| 33 | try { |
| 34 | const msg = JSON.parse(b4a.toString(buf)) |
| 35 | const cb = pending.get(msg.id) |
| 36 | if (cb) { |
| 37 | pending.delete(msg.id) |
| 38 | if (msg.ok !== undefined && msg.data !== undefined) msg.result = msg.data |
| 39 | cb(msg) |
| 40 | } |
| 41 | } catch {} |
| 42 | }) |
| 43 | |
| 44 | function call (method, params = {}) { |
| 45 | return new Promise((resolve, reject) => { |
| 46 | const id = ++rpcId |
| 47 | const timeout = setTimeout(() => { |
| 48 | pending.delete(id) |
| 49 | reject(new Error(`RPC timeout: ${method}`)) |
| 50 | }, 15000) |
| 51 | pending.set(id, (msg) => { |
| 52 | clearTimeout(timeout) |
| 53 | if (msg.error) reject(new Error(msg.error.message || msg.error)) |
| 54 | else resolve(msg.result) |
| 55 | }) |
| 56 | const payload = JSON.stringify({ jsonrpc: '2.0', id, method, params }) |
| 57 | framed.write(b4a.from(payload)) |
| 58 | }) |
| 59 | } |
| 60 | |
| 61 | function close () { framed.destroy(); pipe.destroy() } |
| 62 | return { call, close } |
| 63 | } |
| 64 | |
| 65 | // Infer topics from room title + member count |
| 66 | function inferTopics (title, roomType, memberCount) { |
| 67 | if (!title || title === '(untitled)') { |
| 68 | // For untitled rooms, use member count to differentiate |
| 69 | if (memberCount <= 2) return ['private'] |
| 70 | if (memberCount <= 5) return ['small-group'] |
| 71 | return ['community'] |
| 72 | } |
| 73 | |
| 74 | const lower = title.toLowerCase() |
| 75 | const topics = [] |
| 76 | |
| 77 | // Category detection — order by specificity |
| 78 | if (lower.includes('test') || lower.includes('_test')) topics.push('testing') |
| 79 | if (lower.includes('lobby') || lower.includes('general') || lower.includes('chat')) topics.push('social') |
| 80 | if (lower.includes('ai') || lower.includes('bot') || lower.includes('clode') || lower.includes('claude')) topics.push('ai') |
| 81 | if (lower.includes('invite') || lower.includes('welcome') || lower.includes('onboard')) topics.push('onboarding') |
| 82 | if (lower.includes('dev') || lower.includes('code') || lower.includes('hack') || lower.includes('build')) topics.push('development') |
| 83 | if (lower.includes('game') || lower.includes('godot') || lower.includes('play')) topics.push('gaming') |
| 84 | if (lower.includes('p2p') || lower.includes('hyper') || lower.includes('pear') || lower.includes('keet')) topics.push('p2p') |
| 85 | if (lower.includes('music') || lower.includes('art') || lower.includes('media')) topics.push('media') |
| 86 | if (lower.includes('wallet') || lower.includes('crypto') || lower.includes('bitcoin') || lower.includes('btc')) topics.push('crypto') |
| 87 | |
| 88 | if (topics.length === 0) { |
| 89 | if (memberCount <= 2) topics.push('private') |
| 90 | else if (memberCount <= 5) topics.push('small-group') |
| 91 | else topics.push('community') |
| 92 | } |
| 93 | return topics |
| 94 | } |
| 95 | |
| 96 | // Infer category from topics |
| 97 | function inferCategory (topics) { |
| 98 | if (topics.includes('testing')) return 'testing' |
| 99 | if (topics.includes('onboarding')) return 'onboarding' |
| 100 | if (topics.includes('development')) return 'development' |
| 101 | if (topics.includes('gaming')) return 'gaming' |
| 102 | if (topics.includes('ai')) return 'ai' |
| 103 | if (topics.includes('p2p')) return 'p2p' |
| 104 | if (topics.includes('crypto')) return 'crypto' |
| 105 | if (topics.includes('media')) return 'media' |
| 106 | if (topics.includes('social')) return 'social' |
| 107 | if (topics.includes('private')) return 'private' |
| 108 | if (topics.includes('small-group')) return 'small-group' |
| 109 | if (topics.includes('community')) return 'community' |
| 110 | return 'general' |
| 111 | } |
| 112 | |
| 113 | // Normalize stale category values |
| 114 | function normalizeCategory (cat) { |
| 115 | if (cat === 'test') return 'testing' // fix inconsistency |
| 116 | return cat |
| 117 | } |
| 118 | |
| 119 | async function run () { |
| 120 | console.log(`\n=== Auto-Populate Room Metadata ${APPLY ? '(APPLYING)' : '(DRY RUN)'}${FORCE ? ' [FORCE]' : ''} ===\n`) |
| 121 | |
| 122 | const client = connect() |
| 123 | |
| 124 | const rooms = await client.call('rooms') |
| 125 | let updated = 0 |
| 126 | let skipped = 0 |
| 127 | |
| 128 | for (const room of rooms) { |
| 129 | const id = room.roomId || room.id |
| 130 | const title = room.title || '(untitled)' |
| 131 | |
| 132 | // Get room details |
| 133 | let info |
| 134 | try { |
| 135 | info = await client.call('room', { roomId: id }) |
| 136 | } catch { |
| 137 | console.log(` SKIP: "${title}" — cannot get room info`) |
| 138 | skipped++ |
| 139 | continue |
| 140 | } |
| 141 | |
| 142 | const roomType = info.digest?.settings?.roomType |
| 143 | |
| 144 | // Skip DMs |
| 145 | if (roomType === 3) { |
| 146 | console.log(` SKIP: "${title}" — DM room`) |
| 147 | skipped++ |
| 148 | continue |
| 149 | } |
| 150 | |
| 151 | // Skip test suite room |
| 152 | if (title === TEST_SUITE) { |
| 153 | console.log(` SKIP: "${title}" — test suite room`) |
| 154 | skipped++ |
| 155 | continue |
| 156 | } |
| 157 | |
| 158 | // Skip admin channel (but DO set its metadata) |
| 159 | const isAdmin = id === ADMIN_CHANNEL |
| 160 | |
| 161 | // Get member count for better inference |
| 162 | let memberCount = 0 |
| 163 | try { |
| 164 | const mResult = await client.call('members', { roomId: id }) |
| 165 | const members = Array.isArray(mResult) ? mResult : (mResult?.members || mResult?.data || []) |
| 166 | memberCount = members.length |
| 167 | } catch {} |
| 168 | |
| 169 | // Get existing metadata |
| 170 | let existing = {} |
| 171 | try { |
| 172 | existing = await client.call('meta.get', { roomId: id }) |
| 173 | } catch {} |
| 174 | |
| 175 | // Build metadata update |
| 176 | const update = {} |
| 177 | let changes = [] |
| 178 | |
| 179 | // Topics: set if empty/missing OR force |
| 180 | if (FORCE || !existing.topics || existing.topics.length === 0) { |
| 181 | if (isAdmin) { |
| 182 | update.topics = ['admin', 'system'] |
| 183 | } else { |
| 184 | update.topics = inferTopics(title, roomType, memberCount) |
| 185 | } |
| 186 | changes.push(`topics=[${update.topics.join(', ')}]`) |
| 187 | } |
| 188 | |
| 189 | // Discoverable: set if missing OR force |
| 190 | if (FORCE || existing.discoverable === undefined || existing.discoverable === null) { |
| 191 | update.discoverable = !isAdmin |
| 192 | changes.push(`discoverable=${update.discoverable}`) |
| 193 | } |
| 194 | |
| 195 | // Language: set if missing OR force |
| 196 | if (FORCE || !existing.language) { |
| 197 | update.language = 'en' |
| 198 | changes.push('language=en') |
| 199 | } |
| 200 | |
| 201 | // Category: normalize stale values, infer from topics |
| 202 | const existingCat = existing.category ? normalizeCategory(existing.category) : null |
| 203 | if (FORCE || !existingCat) { |
| 204 | const topics = update.topics || existing.topics || ['general'] |
| 205 | update.category = isAdmin ? 'system' : inferCategory(topics) |
| 206 | changes.push(`category=${update.category}`) |
| 207 | } else if (existingCat !== existing.category) { |
| 208 | // Category was normalized (e.g. "test" → "testing") |
| 209 | update.category = existingCat |
| 210 | changes.push(`category=${existingCat} (normalized)`) |
| 211 | } |
| 212 | |
| 213 | if (changes.length === 0) { |
| 214 | console.log(` OK: "${title}" — already populated`) |
| 215 | skipped++ |
| 216 | continue |
| 217 | } |
| 218 | |
| 219 | console.log(` ${APPLY ? 'SET' : 'WOULD SET'}: "${title}" (${id.slice(0, 12)}...) [${memberCount} members]`) |
| 220 | console.log(` ${changes.join(', ')}`) |
| 221 | |
| 222 | if (APPLY) { |
| 223 | try { |
| 224 | await client.call('meta.set', { roomId: id, metadata: update }) |
| 225 | updated++ |
| 226 | } catch (err) { |
| 227 | console.log(` ERROR: ${err.message}`) |
| 228 | } |
| 229 | } else { |
| 230 | updated++ |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | console.log(`\n=== RESULT ===`) |
| 235 | console.log(` ${APPLY ? 'Updated' : 'Would update'}: ${updated}`) |
| 236 | console.log(` Skipped: ${skipped}`) |
| 237 | console.log(` Total rooms: ${rooms.length}`) |
| 238 | |
| 239 | if (!APPLY && updated > 0) { |
| 240 | console.log(`\nRun with --apply to execute: bare tests/auto-populate-meta.mjs --apply`) |
| 241 | console.log(`Add --force to overwrite existing: bare tests/auto-populate-meta.mjs --force --apply`) |
| 242 | } |
| 243 | |
| 244 | client.close() |
| 245 | setTimeout(() => Bare.exit(0), 500) |
| 246 | } |
| 247 | |
| 248 | run().catch(err => { console.error('FATAL:', err.message); Bare.exit(1) }) |