GET /xrpc/app.bsky.actor.searchActorsTypeahead typeahead.waow.tech
16
fork

Configure Feed

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

auto-enrich bsky-banned accounts via PDS fallback

three changes:

1. handleRequestIndexing now persists PDS from slingshot response

2. enrichment phase 1b: backfill PDS via slingshot for actors that
have handles but no PDS (100/run, 24h backoff)

3. enrichment phase 2 + refreshModeration: for DIDs not returned by
getProfiles, probe getProfile for AccountTakedown. if confirmed
and PDS is available, fetch profile directly — no show override
needed. just protocol-level data from their own PDS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+120 -41
+34 -19
src/cron.ts
··· 81 81 changed += batchResults.filter((r) => r.meta.changes > 0).length; 82 82 } 83 83 84 - // PDS fallback for unreturned DIDs with show overrides 84 + // PDS fallback for unreturned DIDs — check if bsky banned them 85 85 const returnedDids = new Set(profiles.map((p: any) => p.did)); 86 86 const pdsStmts: Stmt[] = []; 87 87 for (const r of batch) { 88 88 if (returnedDids.has(r.did)) continue; 89 - if (overrides.get(r.did) === 'show' && r.pds) { 90 - const pdsProfile = await fetchProfileFromPds(r.did, r.pds); 91 - if (pdsProfile) { 92 - const f = extractProfileFields(pdsProfile, 'show'); 93 - const cur = current.get(r.did); 94 - if (cur && cur.hidden === f.hidden && cur.labels === f.labels) continue; 95 - pdsStmts.push( 96 - db.prepare( 97 - `UPDATE actors SET hidden = ?1, 98 - display_name = COALESCE(NULLIF(?3, ''), display_name), 99 - avatar_url = COALESCE(NULLIF(?4, ''), avatar_url), 100 - labels = ?5, 101 - created_at = COALESCE(NULLIF(?6, ''), created_at), 102 - associated = COALESCE(NULLIF(?7, '{}'), associated) 103 - WHERE did = ?2` 104 - ).bind(f.hidden, r.did, f.displayName, f.avatarCid, f.labels, f.createdAt, f.associated) 105 - ); 106 - returnedDids.add(r.did); // don't delete this actor 89 + const override = overrides.get(r.did) ?? null; 90 + if (r.pds && (override === 'show' || !override)) { 91 + let isTakedown = override === 'show'; 92 + if (!isTakedown) { 93 + try { 94 + const probe = await fetch( 95 + `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(r.did)}` 96 + ); 97 + if (!probe.ok) { 98 + const err: any = await probe.json().catch(() => ({})); 99 + isTakedown = err.error === 'AccountTakedown'; 100 + } 101 + } catch {} 102 + } 103 + if (isTakedown) { 104 + const pdsProfile = await fetchProfileFromPds(r.did, r.pds); 105 + if (pdsProfile) { 106 + const f = extractProfileFields(pdsProfile, override); 107 + const cur = current.get(r.did); 108 + if (cur && cur.hidden === f.hidden && cur.labels === f.labels) continue; 109 + pdsStmts.push( 110 + db.prepare( 111 + `UPDATE actors SET hidden = ?1, 112 + display_name = COALESCE(NULLIF(?3, ''), display_name), 113 + avatar_url = COALESCE(NULLIF(?4, ''), avatar_url), 114 + labels = ?5, 115 + created_at = COALESCE(NULLIF(?6, ''), created_at), 116 + associated = COALESCE(NULLIF(?7, '{}'), associated) 117 + WHERE did = ?2` 118 + ).bind(f.hidden, r.did, f.displayName, f.avatarCid, f.labels, f.createdAt, f.associated) 119 + ); 120 + returnedDids.add(r.did); // don't delete this actor 121 + } 107 122 } 108 123 } 109 124 }
+82 -19
src/enrichment.ts
··· 97 97 } 98 98 } 99 99 100 + // phase 1b: backfill PDS for actors that have handles but no PDS 101 + const { results: pdsRows } = await db.prepare( 102 + `SELECT did FROM actors 103 + WHERE handle != '' AND pds = '' 104 + AND identity_checked_at < unixepoch() - 86400 105 + ORDER BY identity_checked_at ASC LIMIT 100` 106 + ).all<{ did: string }>(); 107 + 108 + if (pdsRows && pdsRows.length > 0) { 109 + let filled = 0; 110 + const BATCH = 20; 111 + for (let i = 0; i < pdsRows.length; i += BATCH) { 112 + const batch = pdsRows.slice(i, i + BATCH); 113 + await Promise.all(batch.map(async ({ did }) => { 114 + try { 115 + const res = await fetch( 116 + `${SLINGSHOT_URL}?identifier=${encodeURIComponent(did)}` 117 + ); 118 + if (!res.ok) { 119 + await db.prepare( 120 + "UPDATE actors SET identity_checked_at = unixepoch() WHERE did = ?1" 121 + ).bind(did).run(); 122 + return; 123 + } 124 + const identity: SlingshotResponse = await res.json(); 125 + if (identity.pds) { 126 + await db.prepare( 127 + "UPDATE actors SET pds = ?1, identity_checked_at = unixepoch() WHERE did = ?2" 128 + ).bind(identity.pds, did).run(); 129 + filled++; 130 + } else { 131 + await db.prepare( 132 + "UPDATE actors SET identity_checked_at = unixepoch() WHERE did = ?1" 133 + ).bind(did).run(); 134 + } 135 + } catch { 136 + await db.prepare( 137 + "UPDATE actors SET identity_checked_at = unixepoch() WHERE did = ?1" 138 + ).bind(did).run().catch(() => {}); 139 + } 140 + })); 141 + } 142 + if (filled > 0) { 143 + console.log(JSON.stringify({ event: "enrich_pds", filled, checked: pdsRows.length })); 144 + } 145 + } 146 + 100 147 // phase 2: profile + labels enrichment via getProfiles batch 101 148 // targets actors missing avatar OR labels — one API call per 25 actors 102 149 const { results: profileRows } = await db.prepare( ··· 149 196 ); 150 197 if (f.avatarCid) enriched++; 151 198 } 152 - // PDS fallback for unreturned DIDs with show overrides 199 + // PDS fallback for unreturned DIDs — check if bsky banned them 153 200 for (const r of batch) { 154 201 if (returned.has(r.did)) continue; 155 - if (overrides.get(r.did) === 'show' && r.pds) { 156 - const pdsProfile = await fetchProfileFromPds(r.did, r.pds); 157 - if (pdsProfile) { 158 - const f = extractProfileFields(pdsProfile, 'show'); 159 - stmts.push( 160 - db.prepare( 161 - `UPDATE actors SET 162 - display_name = COALESCE(NULLIF(?2, ''), display_name), 163 - avatar_url = COALESCE(NULLIF(?3, ''), avatar_url), 164 - labels = ?4, hidden = ?5, 165 - created_at = COALESCE(NULLIF(?6, ''), created_at), 166 - associated = COALESCE(NULLIF(?7, '{}'), associated), 167 - profile_checked_at = unixepoch() 168 - WHERE did = ?1` 169 - ).bind(r.did, f.displayName, f.avatarCid, f.labels, f.hidden, f.createdAt, f.associated) 170 - ); 171 - if (f.avatarCid) enriched++; 172 - continue; 202 + const override = overrides.get(r.did) ?? null; 203 + // if we have a PDS, check if this is a takedown (or has show override) 204 + if (r.pds && (override === 'show' || !override)) { 205 + let isTakedown = override === 'show'; // trust override 206 + if (!isTakedown) { 207 + try { 208 + const probe = await fetch( 209 + `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(r.did)}` 210 + ); 211 + if (!probe.ok) { 212 + const err: any = await probe.json().catch(() => ({})); 213 + isTakedown = err.error === 'AccountTakedown'; 214 + } 215 + } catch {} 216 + } 217 + if (isTakedown) { 218 + const pdsProfile = await fetchProfileFromPds(r.did, r.pds); 219 + if (pdsProfile) { 220 + const f = extractProfileFields(pdsProfile, override); 221 + stmts.push( 222 + db.prepare( 223 + `UPDATE actors SET 224 + display_name = COALESCE(NULLIF(?2, ''), display_name), 225 + avatar_url = COALESCE(NULLIF(?3, ''), avatar_url), 226 + labels = ?4, hidden = ?5, 227 + created_at = COALESCE(NULLIF(?6, ''), created_at), 228 + associated = COALESCE(NULLIF(?7, '{}'), associated), 229 + profile_checked_at = unixepoch() 230 + WHERE did = ?1` 231 + ).bind(r.did, f.displayName, f.avatarCid, f.labels, f.hidden, f.createdAt, f.associated) 232 + ); 233 + if (f.avatarCid) enriched++; 234 + continue; 235 + } 173 236 } 174 237 } 175 238 stmts.push(
+4 -3
src/handlers/admin.ts
··· 124 124 } 125 125 126 126 await db.prepare( 127 - `INSERT INTO actors (did, handle, display_name, avatar_url, hidden, labels, created_at, associated, updated_at) 128 - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, unixepoch()) 127 + `INSERT INTO actors (did, handle, display_name, avatar_url, hidden, labels, created_at, associated, pds, updated_at) 128 + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, unixepoch()) 129 129 ON CONFLICT(did) DO UPDATE SET 130 130 handle = ?2, 131 131 display_name = COALESCE(NULLIF(?3, ''), actors.display_name), ··· 134 134 labels = ?6, 135 135 created_at = COALESCE(NULLIF(?7, ''), actors.created_at), 136 136 associated = COALESCE(NULLIF(?8, '{}'), actors.associated), 137 + pds = COALESCE(NULLIF(?9, ''), actors.pds), 137 138 updated_at = unixepoch()` 138 139 ) 139 - .bind(identity.did, identity.handle, f.displayName, f.avatarCid, f.hidden, f.labels, f.createdAt, f.associated) 140 + .bind(identity.did, identity.handle, f.displayName, f.avatarCid, f.hidden, f.labels, f.createdAt, f.associated, identity.pds || '') 140 141 .run(); 141 142 142 143 return json({