Mass Block [bsky] Reposts [and more]
0
fork

Configure Feed

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

feat: add fetch progress counts and user-agent header

spinners for data fetching and social graph now show a running record
count as constellation pages accumulate. adds USER_AGENT to all
constellation and slingshot requests per their api etiquette guidelines.

bumps version to 1.1.0.

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

Winter e530590a c94c5878

+43 -30
+42 -29
index.js
··· 15 15 const IS_BUN = typeof Bun !== "undefined"; 16 16 17 17 // ── config ────────────────────────────────────────────────────────── 18 + const USER_AGENT = "block-reposters/1.1 (did:plc:7exy3k53z33dvghn6edyayxt; winter@madoka.systems)"; 18 19 const CONSTELLATION_BASE = "https://constellation.microcosm.blue"; 19 20 const PORT = 22891; 20 21 const REDIRECT_URI = `http://127.0.0.1:${PORT}/callback`; ··· 77 78 78 79 async function resolveMiniDoc(identifier) { 79 80 const res = await fetch( 80 - `https://slingshot.microcosm.blue/xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=${encodeURIComponent(identifier)}` 81 + `https://slingshot.microcosm.blue/xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=${encodeURIComponent(identifier)}`, 82 + { headers: { "user-agent": USER_AGENT } } 81 83 ); 82 84 if (!res.ok) throw new Error(`slingshot identity error for ${identifier}: ${res.status}`); 83 85 const doc = await res.json(); ··· 154 156 } 155 157 156 158 // ── constellation ─────────────────────────────────────────────────── 157 - async function fetchConstellationDids(target, collection, path) { 159 + async function fetchConstellationDids(target, collection, path, onProgress) { 158 160 const allDids = []; 159 161 let cursor; 160 162 ··· 166 168 url.searchParams.set("limit", String(PAGE_SIZE)); 167 169 if (cursor) url.searchParams.set("cursor", cursor); 168 170 169 - const res = await fetch(url); 171 + const res = await fetch(url, { headers: { "user-agent": USER_AGENT } }); 170 172 if (!res.ok) throw new Error(`constellation error: ${res.status}`); 171 173 const data = await res.json(); 172 174 173 175 allDids.push(...data.linking_dids); 176 + if (onProgress) onProgress(data.linking_dids.length); 174 177 if (!data.cursor || data.linking_dids.length === 0) break; 175 178 cursor = data.cursor; 176 179 } ··· 178 181 return allDids; 179 182 } 180 183 181 - const fetchReposters = (atUri) => 182 - fetchConstellationDids(atUri, "app.bsky.feed.repost", ".subject.uri"); 184 + const fetchReposters = (atUri, onProgress) => 185 + fetchConstellationDids(atUri, "app.bsky.feed.repost", ".subject.uri", onProgress); 183 186 184 - const fetchLikers = (atUri) => 185 - fetchConstellationDids(atUri, "app.bsky.feed.like", ".subject.uri"); 187 + const fetchLikers = (atUri, onProgress) => 188 + fetchConstellationDids(atUri, "app.bsky.feed.like", ".subject.uri", onProgress); 186 189 187 - const fetchRepliers = (atUri) => 188 - fetchConstellationDids(atUri, "app.bsky.feed.post", ".reply.parent.uri"); 190 + const fetchRepliers = (atUri, onProgress) => 191 + fetchConstellationDids(atUri, "app.bsky.feed.post", ".reply.parent.uri", onProgress); 189 192 190 - const fetchQuotePosters = (atUri) => 191 - fetchConstellationDids(atUri, "app.bsky.feed.post", ".embed.record.uri"); 193 + const fetchQuotePosters = (atUri, onProgress) => 194 + fetchConstellationDids(atUri, "app.bsky.feed.post", ".embed.record.uri", onProgress); 192 195 193 196 function extractAuthorDid(atUri) { 194 197 return atUri.replace("at://", "").split("/")[0]; ··· 201 204 return doc.pds; 202 205 } 203 206 204 - async function fetchFollowing(did, pdsUrl) { 207 + async function fetchFollowing(did, pdsUrl, onProgress) { 205 208 const allDids = []; 206 209 let cursor; 207 210 ··· 217 220 const data = await res.json(); 218 221 219 222 for (const rec of data.records) allDids.push(rec.value.subject); 223 + if (onProgress) onProgress(data.records.length); 220 224 if (!data.cursor || data.records.length === 0) break; 221 225 cursor = data.cursor; 222 226 } ··· 224 228 return new Set(allDids); 225 229 } 226 230 227 - async function fetchFollowers(did) { 228 - const dids = await fetchConstellationDids(did, "app.bsky.graph.follow", ".subject"); 231 + async function fetchFollowers(did, onProgress) { 232 + const dids = await fetchConstellationDids(did, "app.bsky.graph.follow", ".subject", onProgress); 229 233 return new Set(dids); 230 234 } 231 235 232 - async function fetchExistingBlocks(did, pdsUrl) { 236 + async function fetchExistingBlocks(did, pdsUrl, onProgress) { 233 237 const allDids = []; 234 238 let cursor; 235 239 ··· 245 249 const data = await res.json(); 246 250 247 251 for (const rec of data.records) allDids.push(rec.value.subject); 252 + if (onProgress) onProgress(data.records.length); 248 253 if (!data.cursor || data.records.length === 0) break; 249 254 cursor = data.cursor; 250 255 } ··· 252 257 return new Set(allDids); 253 258 } 254 259 255 - async function fetchSocialGraph(did) { 260 + async function fetchSocialGraph(did, onProgress) { 256 261 const pdsUrl = await resolvePds(did); 262 + let total = 0; 263 + const tick = onProgress ? (delta) => { total += delta; onProgress(total); } : undefined; 257 264 const [follows, followers, existingBlocks] = await Promise.all([ 258 - fetchFollowing(did, pdsUrl), 259 - fetchFollowers(did), 260 - fetchExistingBlocks(did, pdsUrl), 265 + fetchFollowing(did, pdsUrl, tick), 266 + fetchFollowers(did, tick), 267 + fetchExistingBlocks(did, pdsUrl, tick), 261 268 ]); 262 269 return { follows, followers, existingBlocks }; 263 270 } ··· 536 543 } 537 544 538 545 // ── data fetching ─────────────────────────────────────────────────── 539 - async function fetchEngagementData(target, categories, mode) { 546 + async function fetchEngagementData(target, categories, mode, onProgress) { 540 547 const results = { reposts: [], likes: [], replies: [], followers: [], following: [], quotePosters: [] }; 541 548 const fetchers = []; 549 + let total = 0; 550 + const tick = onProgress ? (delta) => { total += delta; onProgress(total); } : undefined; 542 551 543 552 if (mode === "profile") { 544 553 // target is a DID 545 554 if (categories.has("followers")) { 546 - fetchers.push(fetchFollowers(target).then((d) => { results.followers = [...d]; })); 555 + fetchers.push(fetchFollowers(target, tick).then((d) => { results.followers = [...d]; })); 547 556 } 548 557 if (categories.has("following")) { 549 558 fetchers.push( 550 559 resolvePds(target).then((pdsUrl) => 551 - fetchFollowing(target, pdsUrl).then((d) => { results.following = [...d]; }) 560 + fetchFollowing(target, pdsUrl, tick).then((d) => { results.following = [...d]; }) 552 561 ) 553 562 ); 554 563 } 555 564 } else { 556 565 // post mode: target is an at:// URI 557 566 if (categories.has("reposts")) { 558 - fetchers.push(fetchReposters(target).then((d) => { results.reposts = d; })); 567 + fetchers.push(fetchReposters(target, tick).then((d) => { results.reposts = d; })); 559 568 } 560 569 if (categories.has("likes")) { 561 - fetchers.push(fetchLikers(target).then((d) => { results.likes = d; })); 570 + fetchers.push(fetchLikers(target, tick).then((d) => { results.likes = d; })); 562 571 } 563 572 if (categories.has("replies")) { 564 - fetchers.push(fetchRepliers(target).then((d) => { results.replies = d; })); 573 + fetchers.push(fetchRepliers(target, tick).then((d) => { results.replies = d; })); 565 574 } 566 575 if (categories.has("followers")) { 567 576 const authorDid = extractAuthorDid(target); 568 - fetchers.push(fetchFollowers(authorDid).then((d) => { results.followers = [...d]; })); 577 + fetchers.push(fetchFollowers(authorDid, tick).then((d) => { results.followers = [...d]; })); 569 578 } 570 - fetchers.push(fetchQuotePosters(target).then((d) => { results.quotePosters = d; })); 579 + fetchers.push(fetchQuotePosters(target, tick).then((d) => { results.quotePosters = d; })); 571 580 } 572 581 573 582 await Promise.all(fetchers); ··· 837 846 const s2 = p.spinner(); 838 847 s2.start("fetching data & resolving identity..."); 839 848 const [results, did] = await Promise.all([ 840 - fetchEngagementData(target, config.categories, config.mode), 849 + fetchEngagementData(target, config.categories, config.mode, (n) => { 850 + s2.message(`fetching data & resolving identity... (${n.toLocaleString()} records)`); 851 + }), 841 852 resolveHandle(config.handle), 842 853 ]); 843 854 s2.stop("data fetched"); 844 855 845 856 const s3 = p.spinner(); 846 857 s3.start("fetching your social graph & existing blocks..."); 847 - const { follows, followers, existingBlocks } = await fetchSocialGraph(did); 858 + const { follows, followers, existingBlocks } = await fetchSocialGraph(did, (n) => { 859 + s3.message(`fetching your social graph & existing blocks... (${n.toLocaleString()} records)`); 860 + }); 848 861 s3.stop(`following: ${follows.size}, followers: ${followers.size}, existing blocks: ${existingBlocks.size}`); 849 862 850 863 const filterResult = filterCandidates({
+1 -1
package.json
··· 1 1 { 2 2 "name": "block-reposters", 3 - "version": "1.0.0", 3 + "version": "1.1.0", 4 4 "type": "module", 5 5 "scripts": { 6 6 "start": "node index.js"