Ionosphere.tv
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: backfill profiles for mention authors

Fetches display names and avatars from public Bluesky API for all
447 unique DIDs in the mentions table. 100% success rate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+82
+82
scripts/backfill-mention-profiles.mjs
··· 1 + /** 2 + * Backfill profiles for all DIDs found in the mentions table 3 + * that are not yet cached in the profiles table. 4 + */ 5 + 6 + import { createRequire } from 'module'; 7 + const require = createRequire( 8 + new URL('../apps/ionosphere-appview/package.json', import.meta.url).pathname 9 + ); 10 + const Database = require('better-sqlite3'); 11 + 12 + import { fileURLToPath } from 'url'; 13 + import { dirname, join } from 'path'; 14 + 15 + const __dirname = dirname(fileURLToPath(import.meta.url)); 16 + const DB_PATH = join(__dirname, '..', 'apps', 'data', 'ionosphere.sqlite'); 17 + const PUBLIC_API = 'https://public.api.bsky.app'; 18 + 19 + function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } 20 + 21 + async function main() { 22 + const db = new Database(DB_PATH); 23 + 24 + const missing = db.prepare(` 25 + SELECT DISTINCT m.author_did 26 + FROM mentions m 27 + LEFT JOIN profiles p ON m.author_did = p.did 28 + WHERE p.did IS NULL 29 + `).all(); 30 + 31 + console.log(`${missing.length} profiles to fetch\n`); 32 + 33 + const upsert = db.prepare(` 34 + INSERT OR REPLACE INTO profiles (did, handle, display_name, avatar_url, fetched_at) 35 + VALUES (?, ?, ?, ?, ?) 36 + `); 37 + 38 + let fetched = 0; 39 + let failed = 0; 40 + 41 + for (const { author_did: did } of missing) { 42 + try { 43 + const res = await fetch( 44 + `${PUBLIC_API}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}` 45 + ); 46 + if (res.ok) { 47 + const data = await res.json(); 48 + upsert.run( 49 + did, 50 + data.handle || null, 51 + data.displayName || null, 52 + data.avatar || null, 53 + new Date().toISOString() 54 + ); 55 + fetched++; 56 + if (fetched % 50 === 0) { 57 + console.log(` ${fetched}/${missing.length} fetched...`); 58 + } 59 + } else { 60 + failed++; 61 + } 62 + } catch { 63 + failed++; 64 + } 65 + await sleep(100); 66 + } 67 + 68 + console.log(`\nDone: ${fetched} profiles cached, ${failed} failed`); 69 + 70 + // Verify 71 + const still = db.prepare(` 72 + SELECT COUNT(DISTINCT m.author_did) as c 73 + FROM mentions m 74 + LEFT JOIN profiles p ON m.author_did = p.did 75 + WHERE p.did IS NULL 76 + `).get(); 77 + console.log(`Remaining uncached: ${still.c}`); 78 + 79 + db.close(); 80 + } 81 + 82 + main().catch(console.error);