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

Configure Feed

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

chore: make things actually work lol

Winter 04415a81 ad7bec62

+92 -9
+3
bun.lock
··· 8 8 "@atcute/oauth-node-client": "^1.1.0", 9 9 "@clack/core": "^1.2.0", 10 10 "@clack/prompts": "^1.2.0", 11 + "picocolors": "^1.1.1", 11 12 }, 12 13 }, 13 14 }, ··· 53 54 "fast-wrap-ansi": ["fast-wrap-ansi@0.1.6", "", { "dependencies": { "fast-string-width": "^1.1.0" } }, "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w=="], 54 55 55 56 "nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="], 57 + 58 + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 56 59 57 60 "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], 58 61
+87 -8
index.js
··· 1 + import { mkdir, readFile, writeFile } from "node:fs/promises"; 2 + import { homedir } from "node:os"; 3 + import { dirname, join } from "node:path"; 1 4 import { fileURLToPath } from "node:url"; 2 5 import { parseArgs as nodeParseArgs } from "node:util"; 3 6 import { OAuthClient, MemoryStore } from "@atcute/oauth-node-client"; ··· 473 476 return (await Promise.all(batches)).flat(); 474 477 } 475 478 479 + // ── oauth scope ───────────────────────────────────────────────────── 480 + function getOAuthScope(unblock) { 481 + const action = unblock ? "delete" : "create"; 482 + return `atproto repo:app.bsky.graph.block?action=${action}`; 483 + } 484 + 485 + function scopeCovers(granted, required) { 486 + if (!granted) return false; 487 + const g = new Set(granted.trim().split(/\s+/)); 488 + return required.trim().split(/\s+/).every((tok) => g.has(tok)); 489 + } 490 + 491 + // ── persistent session store ──────────────────────────────────────── 492 + function getSessionStorePath() { 493 + const base = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"); 494 + return join(base, "mbr", "sessions.json"); 495 + } 496 + 497 + class FileStore { 498 + #path; 499 + #cache; 500 + 501 + constructor(path) { 502 + this.#path = path; 503 + } 504 + 505 + async #load() { 506 + if (this.#cache) return this.#cache; 507 + try { 508 + const data = await readFile(this.#path, "utf8"); 509 + this.#cache = new Map(Object.entries(JSON.parse(data))); 510 + } catch (err) { 511 + if (err.code !== "ENOENT") throw err; 512 + this.#cache = new Map(); 513 + } 514 + return this.#cache; 515 + } 516 + 517 + async #flush() { 518 + await mkdir(dirname(this.#path), { recursive: true }); 519 + await writeFile(this.#path, JSON.stringify(Object.fromEntries(this.#cache)), { mode: 0o600 }); 520 + } 521 + 522 + async get(key) { 523 + return (await this.#load()).get(key); 524 + } 525 + 526 + async set(key, value) { 527 + (await this.#load()).set(key, value); 528 + await this.#flush(); 529 + } 530 + 531 + async delete(key) { 532 + (await this.#load()).delete(key); 533 + await this.#flush(); 534 + } 535 + 536 + async clear() { 537 + this.#cache = new Map(); 538 + await this.#flush(); 539 + } 540 + } 541 + 542 + const sessionStore = new FileStore(getSessionStorePath()); 543 + 476 544 // ── callback server ───────────────────────────────────────────────── 477 545 async function startCallbackServer() { 478 546 let resolveSession, rejectSession; ··· 526 594 } 527 595 528 596 // ── oauth ─────────────────────────────────────────────────────────── 529 - async function authenticate(handle, scope) { 597 + async function authenticate({ handle, did, scope }) { 530 598 const oauthClient = new OAuthClient({ 531 599 metadata: { 532 600 redirect_uris: [REDIRECT_URI], ··· 539 607 }, 540 608 }, 541 609 stores: { 542 - sessions: new MemoryStore(), 610 + sessions: sessionStore, 543 611 states: new MemoryStore({ ttl: 600_000 }), 544 612 }, 545 613 }); 546 614 615 + if (did) { 616 + const stored = await sessionStore.get(did); 617 + if (stored && scopeCovers(stored.tokenSet?.scope, scope)) { 618 + try { 619 + const session = await oauthClient.restore(did); 620 + p.log.info(`restored cached session for ${session.did}`); 621 + return session; 622 + } catch { 623 + // cached session unusable (expired/revoked) — fall through to full flow 624 + } 625 + } 626 + } 627 + 547 628 const { sessionPromise, close, ctx } = await startCallbackServer(); 548 629 ctx.oauthClient = oauthClient; 549 630 ··· 639 720 640 721 const prompt = new Prompt({ 641 722 validate(value) { 642 - if (value.size === 0) return "select at least one option"; 723 + if (!value || value.size === 0) return "select at least one option"; 643 724 }, 644 725 render() { 645 726 const prefix = color.gray("│"); ··· 682 763 } 683 764 }); 684 765 685 - prompt.on("submit", () => { 686 - prompt.value = selected; 687 - }); 766 + prompt.value = selected; 688 767 689 768 return prompt.prompt(); 690 769 } ··· 934 1013 935 1014 const authSpinner = p.spinner(); 936 1015 authSpinner.start("authenticating via oauth..."); 937 - const session = await authenticate(handle, "atproto repo:app.bsky.graph.block?action=create"); 1016 + const session = await authenticate({ handle, did, scope: getOAuthScope(false) }); 938 1017 authSpinner.stop(`authenticated as ${session.did}`); 939 1018 940 1019 const blockSpinner = p.spinner(); ··· 1051 1130 1052 1131 const authSpinner = p.spinner(); 1053 1132 authSpinner.start("authenticating via oauth..."); 1054 - const session = await authenticate(handle, "atproto repo:app.bsky.graph.block?action=delete"); 1133 + const session = await authenticate({ handle, did, scope: getOAuthScope(true) }); 1055 1134 authSpinner.stop(`authenticated as ${session.did}`); 1056 1135 1057 1136 const unblockSpinner = p.spinner();
+2 -1
package.json
··· 8 8 "dependencies": { 9 9 "@atcute/oauth-node-client": "^1.1.0", 10 10 "@clack/core": "^1.2.0", 11 - "@clack/prompts": "^1.2.0" 11 + "@clack/prompts": "^1.2.0", 12 + "picocolors": "^1.1.1" 12 13 } 13 14 }