feat: init

+3
.envrc
··· 1 + if has nix; then 2 + use nix 3 + fi
+2
.gitignore
··· 1 + dist/ 2 + .env
+23
package.json
··· 1 + { 2 + "name": "atproto-goofin", 3 + "version": "1.0.0", 4 + "description": "", 5 + "scripts": { 6 + "toplikes": "node src/toplikes.ts", 7 + "whodoilike": "node src/whodoilike.ts" 8 + }, 9 + "keywords": [], 10 + "author": "isabel roses <isabel@isabelroses.com>", 11 + "license": "MIT", 12 + "packageManager": "pnpm@10.14.0", 13 + "dependencies": { 14 + "@atcute/bluesky": "^3.2.0", 15 + "@atcute/client": "^4.0.3", 16 + "@atcute/lexicons": "^1.1.0", 17 + "dotenv": "^17.2.1" 18 + }, 19 + "type": "module", 20 + "devDependencies": { 21 + "@types/node": "^24.3.0" 22 + } 23 + }
+97
pnpm-lock.yaml
··· 1 + lockfileVersion: '9.0' 2 + 3 + settings: 4 + autoInstallPeers: true 5 + excludeLinksFromLockfile: false 6 + 7 + importers: 8 + 9 + .: 10 + dependencies: 11 + '@atcute/bluesky': 12 + specifier: ^3.2.0 13 + version: 3.2.0 14 + '@atcute/client': 15 + specifier: ^4.0.3 16 + version: 4.0.3 17 + '@atcute/lexicons': 18 + specifier: ^1.1.0 19 + version: 1.1.0 20 + dotenv: 21 + specifier: ^17.2.1 22 + version: 17.2.1 23 + devDependencies: 24 + '@types/node': 25 + specifier: ^24.3.0 26 + version: 24.3.0 27 + 28 + packages: 29 + 30 + '@atcute/atproto@3.1.1': 31 + resolution: {integrity: sha512-D+RLTIPF0xLu7BPZY8KSewAPemJFh+3n3zeQ3ROsLxbTtCHbrTDMAmAFexaVRAPGcPYrwXaBUlv7yZjScJolMg==} 32 + 33 + '@atcute/bluesky@3.2.0': 34 + resolution: {integrity: sha512-OqPLqUNjXcgQ25MaPdU7H0QcWmZrx6QQk7d5B22A5U4xy+hZJ954kQ5mSAn24Bt0DEm4j/isq1WZovr3vaPTUA==} 35 + 36 + '@atcute/client@4.0.3': 37 + resolution: {integrity: sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==} 38 + 39 + '@atcute/identity@1.0.3': 40 + resolution: {integrity: sha512-mNMxbKHFGys03A8JXKk0KfMBzdd0vrYMzZZWjpw1nYTs0+ea6bo5S1hwqVUZxHdo1gFHSe/t63jxQIF4yL9aKw==} 41 + 42 + '@atcute/lexicons@1.1.0': 43 + resolution: {integrity: sha512-LFqwnria78xLYb62Ri/+WwQpUTgZp2DuyolNGIIOV1dpiKhFFFh//nscHMA6IExFLQRqWDs3tTjy7zv0h3sf1Q==} 44 + 45 + '@badrap/valita@0.4.6': 46 + resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==} 47 + engines: {node: '>= 18'} 48 + 49 + '@types/node@24.3.0': 50 + resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} 51 + 52 + dotenv@17.2.1: 53 + resolution: {integrity: sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==} 54 + engines: {node: '>=12'} 55 + 56 + esm-env@1.2.2: 57 + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 58 + 59 + undici-types@7.10.0: 60 + resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} 61 + 62 + snapshots: 63 + 64 + '@atcute/atproto@3.1.1': 65 + dependencies: 66 + '@atcute/lexicons': 1.1.0 67 + 68 + '@atcute/bluesky@3.2.0': 69 + dependencies: 70 + '@atcute/atproto': 3.1.1 71 + '@atcute/lexicons': 1.1.0 72 + 73 + '@atcute/client@4.0.3': 74 + dependencies: 75 + '@atcute/identity': 1.0.3 76 + '@atcute/lexicons': 1.1.0 77 + 78 + '@atcute/identity@1.0.3': 79 + dependencies: 80 + '@atcute/lexicons': 1.1.0 81 + '@badrap/valita': 0.4.6 82 + 83 + '@atcute/lexicons@1.1.0': 84 + dependencies: 85 + esm-env: 1.2.2 86 + 87 + '@badrap/valita@0.4.6': {} 88 + 89 + '@types/node@24.3.0': 90 + dependencies: 91 + undici-types: 7.10.0 92 + 93 + dotenv@17.2.1: {} 94 + 95 + esm-env@1.2.2: {} 96 + 97 + undici-types@7.10.0: {}
+11
shell.nix
··· 1 + # girl who's shell isn't reproducible 2 + { 3 + pkgs ? import <nixpkgs> { }, 4 + }: 5 + pkgs.mkShell { 6 + packages = with pkgs; [ 7 + pnpm 8 + nodejs-slim_24 9 + typescript-language-server 10 + ]; 11 + }
+55
src/toplikes.ts
··· 1 + import { Client, simpleFetchHandler } from '@atcute/client'; 2 + import { AppBskyFeedPost } from '@atcute/bluesky'; 3 + import { parseResourceUri } from '@atcute/lexicons'; 4 + import fs from 'fs'; 5 + import 'dotenv/config'; 6 + 7 + const client = new Client({ 8 + handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }), 9 + }); 10 + 11 + async function getLikedPosts(cursor: string | undefined = undefined, posts: AppBskyFeedPost.Main[] = []): Promise<AppBskyFeedPost.Main[]> { 12 + const response = await client.get('app.bsky.feed.getAuthorFeed', { 13 + params: { 14 + actor: process.env.DID, 15 + filter: "posts_no_replies", 16 + cursor 17 + } 18 + }); 19 + 20 + if (!response.ok) { 21 + console.log(response) 22 + return []; 23 + } 24 + 25 + for (const feedItem of response.data.feed) { 26 + if (feedItem.post.author.did != process.env.DID) continue; 27 + 28 + posts.push(feedItem); 29 + } 30 + 31 + if (response.data.cursor) { 32 + await getLikedPosts(response.data.cursor, posts) 33 + } 34 + 35 + return posts; 36 + } 37 + 38 + async function main() { 39 + const posts = await getLikedPosts(); 40 + 41 + posts.sort((a, b) => b.post.likeCount - a.post.likeCount) 42 + 43 + fs.writeFileSync('dist/toplikes.md', 'Post | likes'); 44 + fs.appendFileSync('dist/toplikes.md', '\n-----|------'); 45 + 46 + for (const post of posts) { 47 + const uri = parseResourceUri(post.post.uri); 48 + 49 + if (!uri.ok) continue; 50 + 51 + fs.appendFileSync('dist/toplikes.md', `\nhttps://bsky.app/profile/${uri.value.repo}/post/${uri.value.rkey} | ${post.post.likeCount}`); 52 + } 53 + } 54 + 55 + main()
+65
src/whodoilike.ts
··· 1 + import { Client, CredentialManager } from '@atcute/client'; 2 + import { AppBskyFeedPost } from '@atcute/bluesky'; 3 + import fs from 'fs'; 4 + import 'dotenv/config'; 5 + 6 + async function getLikedPosts(cursor: string | undefined = undefined, posts: AppBskyFeedPost.Main[] = []): Promise<AppBskyFeedPost.Main[]> { 7 + const response = await rpc.get('app.bsky.feed.getActorLikes', { 8 + params: { 9 + actor: process.env.DID, 10 + cursor 11 + } 12 + }); 13 + 14 + if (!response.ok) { 15 + console.log(response) 16 + return []; 17 + } 18 + 19 + for (const feedItem of response.data.feed) { 20 + // ewwww, whos liking their own posts 21 + if (feedItem.post.author.did == process.env.DID) continue; 22 + 23 + posts.push(feedItem); 24 + } 25 + 26 + if (response.data.cursor) { 27 + return getLikedPosts(response.data.cursor, posts) 28 + } 29 + 30 + return posts; 31 + } 32 + 33 + async function main() { 34 + const posts = await getLikedPosts(); 35 + 36 + fs.writeFileSync('dist/whodoilike.md', 'DID | handle | times likes'); 37 + fs.appendFileSync('dist/whodoilike.md', '\n----|------|-----------'); 38 + 39 + const didToLikes = new Map(); 40 + for (const post of posts) { 41 + const { handle, did } = post.post; 42 + 43 + if (didToLikes.has(did)) { 44 + let didsLikes = didToLikes.get(did); 45 + didToLikes.set(did, { handle, likes: didsLikes + 1 }); 46 + } else { 47 + didToLikes.set(did, { handle, likes: 1 }) 48 + } 49 + } 50 + 51 + var descDidToLikes = new Map([...didToLikes.entries()].sort((a, b) => b[1].likes - a[1].likes)); 52 + 53 + for (const [did, likers] of descDidToLikes) { 54 + fs.appendFileSync('dist/whodoilike.md', `\n${did} | ${likers.handle} | ${likers.likes}`); 55 + } 56 + } 57 + 58 + // hoist me pls 59 + const manager = new CredentialManager({ service: process.env.PDS }); 60 + const rpc = new Client({ handler: manager }); 61 + await manager.login({ identifier: process.env.DID, password: process.env.PASSWORD }); 62 + 63 + const mydid = ""; 64 + 65 + main()
+23
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "types": ["@atcute/bluesky", "@types/node"], 4 + "outDir": "dist/", 5 + "esModuleInterop": true, 6 + "skipLibCheck": true, 7 + "target": "ESNext", 8 + "allowJs": true, 9 + "resolveJsonModule": true, 10 + "moduleDetection": "force", 11 + "isolatedModules": true, 12 + "verbatimModuleSyntax": true, 13 + "strict": true, 14 + "noImplicitOverride": true, 15 + "noUnusedLocals": true, 16 + "noUnusedParameters": true, 17 + "useDefineForClassFields": false, 18 + "noFallthroughCasesInSwitch": true, 19 + "module": "NodeNext", 20 + "sourceMap": true, 21 + "declaration": true 22 + } 23 + }