mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

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

at ft/monetization 318 lines 9.0 kB view raw
1#!/usr/bin/env bun 2 3import { parseArgs } from "util"; 4 5const MICROCOSM_XRPC_BASE = "https://constellation.microcosm.blue/xrpc"; 6const PUBLIC_BSKY_XRPC_BASE = "https://public.api.bsky.app/xrpc"; 7const BLOCK_SOURCE = "app.bsky.graph.block:subject"; 8const BACKLINK_LIMIT = 16; 9const PROFILE_BATCH_SIZE = 25; 10 11type JsonMap = Record<string, unknown>; 12 13type FetchResult = { 14 status: number; 15 ok: boolean; 16 text: string; 17 data: JsonMap | null; 18}; 19 20type OrderedEntry = 21 | { 22 did: string; 23 status: "resolved"; 24 handle: string; 25 displayName: string | null; 26 } 27 | { 28 did: string; 29 status: "unavailable"; 30 reason: string; 31 }; 32 33function buildXrpcUrl(base: string, method: string, params: Record<string, string | number | null | undefined>): URL { 34 const url = new URL(`${base}/${method}`); 35 for (const [key, value] of Object.entries(params)) { 36 if (value !== null && value !== undefined) { 37 url.searchParams.set(key, String(value)); 38 } 39 } 40 return url; 41} 42 43async function fetchJson(url: URL): Promise<FetchResult> { 44 const response = await fetch(url, { 45 headers: { 46 Accept: "application/json", 47 "User-Agent": "lazurite", 48 }, 49 }); 50 const text = await response.text(); 51 52 let data: JsonMap | null = null; 53 try { 54 data = text.length > 0 ? (JSON.parse(text) as JsonMap) : null; 55 } catch { 56 data = null; 57 } 58 59 return { 60 status: response.status, 61 ok: response.ok, 62 text, 63 data, 64 }; 65} 66 67function getListFieldAny(data: JsonMap | null, keys: string[]): unknown[] { 68 if (!data) return []; 69 70 for (const key of keys) { 71 const value = data[key]; 72 if (Array.isArray(value)) { 73 return value; 74 } 75 } 76 77 return []; 78} 79 80function asString(value: unknown): string | null { 81 return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; 82} 83 84function classifyProfileFailure(result: FetchResult): string { 85 const error = asString(result.data?.error); 86 const message = asString(result.data?.message) ?? result.text; 87 const combined = `${error ?? ""} ${message}`.toLowerCase(); 88 89 if (combined.includes("accounttakedown") || combined.includes("suspended")) { 90 return "Suspended account"; 91 } 92 if (result.status === 404 || combined.includes("not found")) { 93 return "Profile unavailable"; 94 } 95 return "Public profile lookup failed"; 96} 97 98function chunk<T>(items: T[], size: number): T[][] { 99 const output: T[][] = []; 100 for (let index = 0; index < items.length; index += size) { 101 output.push(items.slice(index, index + size)); 102 } 103 return output; 104} 105 106async function fetchBlockedByCount(did: string): Promise<number> { 107 const url = buildXrpcUrl(MICROCOSM_XRPC_BASE, "blue.microcosm.links.getBacklinksCount", { 108 subject: did, 109 source: BLOCK_SOURCE, 110 }); 111 const result = await fetchJson(url); 112 113 if (!result.ok || typeof result.data?.total !== "number") { 114 throw new Error(`getBacklinksCount failed (${result.status}): ${result.text}`); 115 } 116 117 return result.data.total as number; 118} 119 120async function fetchDistinct(did: string): Promise<FetchResult> { 121 const url = buildXrpcUrl(MICROCOSM_XRPC_BASE, "blue.microcosm.links.getDistinct", { 122 subject: did, 123 source: BLOCK_SOURCE, 124 limit: BACKLINK_LIMIT, 125 }); 126 return fetchJson(url); 127} 128 129async function fetchAllBacklinkDids( 130 did: string, 131): Promise<{ orderedDids: string[]; pages: Array<{ page: number; cursorIn: string | null; cursorOut: string | null; recordCount: number }> }> { 132 const orderedDids: string[] = []; 133 const seen = new Set<string>(); 134 const pages: Array<{ page: number; cursorIn: string | null; cursorOut: string | null; recordCount: number }> = []; 135 136 let cursor: string | null = null; 137 let page = 1; 138 139 do { 140 const url = buildXrpcUrl(MICROCOSM_XRPC_BASE, "blue.microcosm.links.getBacklinks", { 141 subject: did, 142 source: BLOCK_SOURCE, 143 limit: BACKLINK_LIMIT, 144 cursor, 145 }); 146 const result = await fetchJson(url); 147 if (!result.ok) { 148 throw new Error(`getBacklinks failed (${result.status}) on page ${page}: ${result.text}`); 149 } 150 151 const records = getListFieldAny(result.data, ["records", "linking_records"]); 152 for (const record of records) { 153 if (record && typeof record === "object") { 154 const blockerDid = asString((record as JsonMap).did); 155 if (blockerDid && !seen.has(blockerDid)) { 156 seen.add(blockerDid); 157 orderedDids.push(blockerDid); 158 } 159 } 160 } 161 162 const nextCursor = asString(result.data?.cursor); 163 pages.push({ 164 page, 165 cursorIn: cursor, 166 cursorOut: nextCursor, 167 recordCount: records.length, 168 }); 169 cursor = nextCursor; 170 page += 1; 171 } while (cursor !== null); 172 173 return { orderedDids, pages }; 174} 175 176async function fetchProfilesBatch(dids: string[]): Promise<Map<string, OrderedEntry>> { 177 const resolved = new Map<string, OrderedEntry>(); 178 179 for (const batch of chunk(dids, PROFILE_BATCH_SIZE)) { 180 const url = new URL(`${PUBLIC_BSKY_XRPC_BASE}/app.bsky.actor.getProfiles`); 181 for (const did of batch) { 182 url.searchParams.append("actors", did); 183 } 184 185 const batchResult = await fetchJson(url); 186 const batchProfiles = batchResult.ok ? getListFieldAny(batchResult.data, ["profiles"]) : []; 187 const batchResolved = new Map<string, OrderedEntry>(); 188 189 for (const profile of batchProfiles) { 190 if (!profile || typeof profile !== "object") continue; 191 const record = profile as JsonMap; 192 const did = asString(record.did); 193 const handle = asString(record.handle); 194 if (!did || !handle) continue; 195 196 batchResolved.set(did, { 197 did, 198 status: "resolved", 199 handle, 200 displayName: asString(record.displayName), 201 }); 202 } 203 204 for (const did of batch) { 205 const existing = batchResolved.get(did); 206 if (existing) { 207 resolved.set(did, existing); 208 continue; 209 } 210 211 const singleUrl = buildXrpcUrl(PUBLIC_BSKY_XRPC_BASE, "app.bsky.actor.getProfile", { 212 actor: did, 213 }); 214 const singleResult = await fetchJson(singleUrl); 215 216 if (singleResult.ok) { 217 const handle = asString(singleResult.data?.handle); 218 if (handle) { 219 resolved.set(did, { 220 did, 221 status: "resolved", 222 handle, 223 displayName: asString(singleResult.data?.displayName), 224 }); 225 continue; 226 } 227 } 228 229 resolved.set(did, { 230 did, 231 status: "unavailable", 232 reason: classifyProfileFailure(singleResult), 233 }); 234 } 235 } 236 237 return resolved; 238} 239 240function printUsage(): void { 241 console.error("Usage: bun run profile-context.ts --did <did:plc:...>"); 242} 243 244async function main(): Promise<void> { 245 const { values } = parseArgs({ 246 args: Bun.argv.slice(2), 247 options: { 248 did: { 249 type: "string", 250 }, 251 help: { 252 type: "boolean", 253 short: "h", 254 default: false, 255 }, 256 }, 257 strict: true, 258 allowPositionals: false, 259 }); 260 261 if (values.help) { 262 printUsage(); 263 process.exit(0); 264 } 265 266 const did = asString(values.did); 267 if (!did) { 268 printUsage(); 269 process.exit(1); 270 } 271 272 const count = await fetchBlockedByCount(did); 273 const distinct = await fetchDistinct(did); 274 const backlinks = await fetchAllBacklinkDids(did); 275 const hydrated = await fetchProfilesBatch(backlinks.orderedDids); 276 const orderedEntries = backlinks.orderedDids.map((blockerDid) => { 277 return ( 278 hydrated.get(blockerDid) ?? { 279 did: blockerDid, 280 status: "unavailable" as const, 281 reason: "Public profile lookup failed", 282 } 283 ); 284 }); 285 286 const resolvedCount = orderedEntries.filter((entry) => entry.status === "resolved").length; 287 const unavailableEntries = orderedEntries.filter((entry) => entry.status === "unavailable"); 288 289 console.log(`Target DID: ${did}`); 290 console.log(`Microcosm blocked-by count: ${count}`); 291 console.log( 292 `Microcosm getDistinct: HTTP ${distinct.status}${distinct.ok ? "" : ` ${distinct.text.trim()}`}`, 293 ); 294 console.log(""); 295 console.log("Backlink pages:"); 296 for (const page of backlinks.pages) { 297 console.log( 298 ` page ${page.page}: cursorIn=${page.cursorIn ?? "null"} records=${page.recordCount} cursorOut=${page.cursorOut ?? "null"}`, 299 ); 300 } 301 console.log(""); 302 console.log(`Collected blocker DIDs: ${backlinks.orderedDids.length}`); 303 console.log(`Resolved public profiles: ${resolvedCount}`); 304 console.log(`Unavailable public profiles: ${unavailableEntries.length}`); 305 console.log(""); 306 console.log("Ordered results:"); 307 orderedEntries.forEach((entry, index) => { 308 if (entry.status === "resolved") { 309 const display = entry.displayName ? ` (${entry.displayName})` : ""; 310 console.log(`${String(index + 1).padStart(2, "0")}. RESOLVED ${entry.did} -> @${entry.handle}${display}`); 311 return; 312 } 313 314 console.log(`${String(index + 1).padStart(2, "0")}. UNAVAILABLE ${entry.did} -> ${entry.reason}`); 315 }); 316} 317 318await main();