mobile bluesky app made with flutter
lazurite.stormlightlabs.org/
mobile
bluesky
flutter
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();