Statusphere Feed Generator

Compare changes

Choose any two refs to compare.

Changed files
+114 -22
scripts
src
+12 -15
bun.lock
··· 1 1 { 2 - "lockfileVersion": 1, 2 + "lockfileVersion": 0, 3 3 "workspaces": { 4 4 "": { 5 - "name": "minimal-bluesky-feed-typescript", 6 5 "dependencies": { 7 6 "@atproto/api": "^0.14.14", 8 7 "@atproto/identity": "^0.4.6", ··· 27 26 }, 28 27 }, 29 28 "packages": { 30 - "@atproto/api": ["@atproto/api@0.14.14", "", { "dependencies": { "@atproto/common-web": "^0.4.0", "@atproto/lexicon": "^0.4.9", "@atproto/syntax": "^0.4.0", "@atproto/xrpc": "^0.6.11", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-ryawcnmazVSWYfq11ujPHauY77GfkM3mF0rZOkqENN2Ptnl6BZXJvpA0zLA/sQ5YBLcHXSEWg5Xdq+8i1l+8gA=="], 29 + "@atproto/api": ["@atproto/api@0.14.19", "", { "dependencies": { "@atproto/common-web": "^0.4.1", "@atproto/lexicon": "^0.4.10", "@atproto/syntax": "^0.4.0", "@atproto/xrpc": "^0.6.12", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-YYTqM0K0qk2TP7PguktPzlAQGLTL1bEGz6PgY5kqKJNX4o1318kJYB22DzjJYqV2NUCq0JQ9Lb0oskLvTisEOg=="], 31 30 32 - "@atproto/common": ["@atproto/common@0.4.8", "", { "dependencies": { "@atproto/common-web": "^0.4.0", "@ipld/dag-cbor": "^7.0.3", "cbor-x": "^1.5.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-/etCtnWQGLcfiGhIPwxAWrzgzoGB22nMWMeQcU6xZgRT4Cqrfg3A08jAMIHqve/AQpL+6D82lHYp36CG7a5G0w=="], 31 + "@atproto/common": ["@atproto/common@0.4.10", "", { "dependencies": { "@atproto/common-web": "^0.4.1", "@ipld/dag-cbor": "^7.0.3", "cbor-x": "^1.5.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-/Yxnax3XOhf46jYpe8/6O3ORjTNMB4YCaxx3V1f+FKy6meTm3GNrJwo8d1CBs0UiTiheRiNATOV3u0s3C7Ydaw=="], 33 32 34 - "@atproto/common-web": ["@atproto/common-web@0.4.0", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-ZYL0P9myHybNgwh/hBY0HaBzqiLR1B5/ie5bJpLQAg0whRzNA28t8/nU2vh99tbsWcAF0LOD29M8++LyENJLNQ=="], 33 + "@atproto/common-web": ["@atproto/common-web@0.4.1", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-Ghh+djHYMAUCktLKwr2IuGgtjcwSWGudp+K7+N7KBA9pDDloOXUEY8Agjc5SHSo9B1QIEFkegClU5n+apn2e0w=="], 35 34 36 35 "@atproto/crypto": ["@atproto/crypto@0.4.4", "", { "dependencies": { "@noble/curves": "^1.7.0", "@noble/hashes": "^1.6.1", "uint8arrays": "3.0.0" } }, "sha512-Yq9+crJ7WQl7sxStVpHgie5Z51R05etaK9DLWYG/7bR5T4bhdcIgF6IfklLShtZwLYdVVj+K15s0BqW9a8PSDA=="], 37 36 38 - "@atproto/identity": ["@atproto/identity@0.4.6", "", { "dependencies": { "@atproto/common-web": "^0.4.0", "@atproto/crypto": "^0.4.4" } }, "sha512-fJq/cIp9MOgHxZfxuyki6mobk0QxRnbts53DstRixlvb5mOoxwttb9Gp6A8u9q49zBsfOmXNTHmP97I9iMHmTQ=="], 37 + "@atproto/identity": ["@atproto/identity@0.4.7", "", { "dependencies": { "@atproto/common-web": "^0.4.1", "@atproto/crypto": "^0.4.4" } }, "sha512-A61OT9yc74dEFi1elODt/tzQNSwV3ZGZCY5cRl6NYO9t/0AVdaD+fyt81yh3mRxyI8HeVOecvXl3cPX5knz9rQ=="], 39 38 40 - "@atproto/lexicon": ["@atproto/lexicon@0.4.9", "", { "dependencies": { "@atproto/common-web": "^0.4.0", "@atproto/syntax": "^0.4.0", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-/tmEuHQFr51V2V7EAVJzaA40sqJ7ylAZpR962VbOsPtmcdOHvezbjVHYEMXgfb927hS+xqbVyzBTbu5w9v8prA=="], 39 + "@atproto/lexicon": ["@atproto/lexicon@0.4.10", "", { "dependencies": { "@atproto/common-web": "^0.4.1", "@atproto/syntax": "^0.4.0", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-uDbP20vetBgtXPuxoyRcvOGBt2gNe1dFc9yYKcb6jWmXfseHiGTnIlORJOLBXIT2Pz15Eap4fLxAu6zFAykD5A=="], 41 40 42 - "@atproto/repo": ["@atproto/repo@0.7.2", "", { "dependencies": { "@atproto/common": "^0.4.8", "@atproto/common-web": "^0.4.0", "@atproto/crypto": "^0.4.4", "@atproto/lexicon": "^0.4.9", "@ipld/car": "^3.2.3", "@ipld/dag-cbor": "^7.0.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-PsAbkAl2+ITQIHFAaOIQzGD9QDUeV9aWKTU6crOpDT3JXWyEffAv8JWamQ9Zjh3k7CMKRDUqyCXO6uizmPPNgw=="], 41 + "@atproto/repo": ["@atproto/repo@0.8.0", "", { "dependencies": { "@atproto/common": "^0.4.10", "@atproto/common-web": "^0.4.1", "@atproto/crypto": "^0.4.4", "@atproto/lexicon": "^0.4.10", "@ipld/dag-cbor": "^7.0.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "varint": "^6.0.0", "zod": "^3.23.8" } }, "sha512-Er4Mpd8XWPwVLcUlKFxUpnyBC+J+oBxARoUGXMTLjdQyg0FmWtZzeYnse8FV/L36DeWV0+v/tqYYggJcOOe1HA=="], 43 42 44 - "@atproto/sync": ["@atproto/sync@0.1.18", "", { "dependencies": { "@atproto/common": "^0.4.8", "@atproto/identity": "^0.4.6", "@atproto/lexicon": "^0.4.9", "@atproto/repo": "^0.7.2", "@atproto/syntax": "^0.4.0", "@atproto/xrpc-server": "^0.7.13", "multiformats": "^9.9.0", "p-queue": "^6.6.2", "ws": "^8.12.0" } }, "sha512-RO9HqrEWEbItrOIHFV1Az6tPzvpQuzS8bWmrW49l0KdY+fAIN+x2JPkkjveBe7q460alDeTEkOPhjFt1IHQTyw=="], 43 + "@atproto/sync": ["@atproto/sync@0.1.20", "", { "dependencies": { "@atproto/common": "^0.4.10", "@atproto/identity": "^0.4.7", "@atproto/lexicon": "^0.4.10", "@atproto/repo": "^0.8.0", "@atproto/syntax": "^0.4.0", "@atproto/xrpc-server": "^0.7.15", "multiformats": "^9.9.0", "p-queue": "^6.6.2", "ws": "^8.12.0" } }, "sha512-/jx3Y3E7xPDih7aw7HcS+dDV34ZrflSqFfSY/h0NQv9UsjeW1WCmC35FuQKauXZj1OpIy4CLTawgOKqDmScbPQ=="], 45 44 46 45 "@atproto/syntax": ["@atproto/syntax@0.4.0", "", {}, "sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA=="], 47 46 48 - "@atproto/xrpc": ["@atproto/xrpc@0.6.11", "", { "dependencies": { "@atproto/lexicon": "^0.4.9", "zod": "^3.23.8" } }, "sha512-J2cZP8FjoDN0UkyTYBlCvKvxwBbDm4dld47u6FQK30RJy9YpSiUkdxJJ10NYqpi7JVny3M0qWQgpWJDV94+PdA=="], 47 + "@atproto/xrpc": ["@atproto/xrpc@0.6.12", "", { "dependencies": { "@atproto/lexicon": "^0.4.10", "zod": "^3.23.8" } }, "sha512-Ut3iISNLujlmY9Gu8sNU+SPDJDvqlVzWddU8qUr0Yae5oD4SguaUFjjhireMGhQ3M5E0KljQgDbTmnBo1kIZ3w=="], 49 48 50 - "@atproto/xrpc-server": ["@atproto/xrpc-server@0.7.13", "", { "dependencies": { "@atproto/common": "^0.4.8", "@atproto/crypto": "^0.4.4", "@atproto/lexicon": "^0.4.9", "@atproto/xrpc": "^0.6.11", "cbor-x": "^1.5.1", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "uint8arrays": "3.0.0", "ws": "^8.12.0", "zod": "^3.23.8" } }, "sha512-gaaAentq1lhAFBHwh2N0EhbNsb1mWrmEXoxmtuXp3uEV4Q7EMFaZYO25B9/Yos5oAsnpK54LIYUamHuJQVWGOA=="], 49 + "@atproto/xrpc-server": ["@atproto/xrpc-server@0.7.15", "", { "dependencies": { "@atproto/common": "^0.4.10", "@atproto/crypto": "^0.4.4", "@atproto/lexicon": "^0.4.10", "@atproto/xrpc": "^0.6.12", "cbor-x": "^1.5.1", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "uint8arrays": "3.0.0", "ws": "^8.12.0", "zod": "^3.23.8" } }, "sha512-9MjhQk3iaIL391j5dD2/lS908yCCwvbGken2wtZoLubSluCKTli2G53NXlfmGcPLEC5IN5iM1+BaUUzfV3Wt5g=="], 51 50 52 51 "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], 53 52 ··· 109 108 110 109 "@inquirer/type": ["@inquirer/type@3.0.5", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg=="], 111 110 112 - "@ipld/car": ["@ipld/car@3.2.4", "", { "dependencies": { "@ipld/dag-cbor": "^7.0.0", "multiformats": "^9.5.4", "varint": "^6.0.0" } }, "sha512-rezKd+jk8AsTGOoJKqzfjLJ3WVft7NZNH95f0pfPbicROvzTyvHCNy567HzSUd6gRXZ9im29z5ZEv9Hw49jSYw=="], 113 - 114 111 "@ipld/dag-cbor": ["@ipld/dag-cbor@7.0.3", "", { "dependencies": { "cborg": "^1.6.0", "multiformats": "^9.5.4" } }, "sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA=="], 115 112 116 113 "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], ··· 145 142 146 143 "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], 147 144 148 - "@types/node": ["@types/node@22.13.11", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-iEUCUJoU0i3VnrCmgoWCXttklWcvoCIx4jzcP22fioIVSdTmjgoEvmAO/QPw6TcS9k5FrNgn4w7q5lGOd1CT5g=="], 145 + "@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="], 149 146 150 147 "@types/qs": ["@types/qs@6.9.18", "", {}, "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA=="], 151 148 ··· 571 568 572 569 "uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="], 573 570 574 - "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], 571 + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 575 572 576 573 "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], 577 574
+15 -2
scripts/publishFeedGen.ts
··· 15 15 type: 'input', 16 16 name: 'handle', 17 17 message: 'bluesky handle:', 18 - required: true, 19 18 }, 20 19 { 21 20 type: 'password', ··· 68 67 service, 69 68 } = answers; 70 69 70 + // makes handle and app password optional, allowing it to be 71 + // fetched from the env 72 + const username = handle || process.env.USERNAME; 73 + 74 + if (!username) { 75 + throw new Error('Username/Handle Not Provided'); 76 + } 77 + 78 + const appPassword = password || process.env.APP_PASSWORD; 79 + 80 + if (!appPassword) { 81 + throw new Error('App Password Not Provided'); 82 + } 83 + 71 84 const feedGenDid = `did:web:${process.env.FEEDGEN_HOSTNAME}`; 72 85 73 86 // only update this if in a test environment 74 87 const agent = new AtpAgent({ 75 88 service: service ? service : 'https://bsky.social', 76 89 }); 77 - await agent.login({ identifier: handle, password }); 90 + await agent.login({ identifier: username, password: appPassword }); 78 91 79 92 let avatarRef: BlobRef | undefined; 80 93 if (avatar) {
+15 -2
scripts/unpublishFeedGen.ts
··· 10 10 type: 'input', 11 11 name: 'handle', 12 12 message: 'Enter your Bluesky handle', 13 - required: true, 14 13 }, 15 14 { 16 15 type: 'password', ··· 42 41 43 42 const { handle, password, recordName, service, confirm } = answers; 44 43 44 + // makes handle and app password optional, allowing it to be 45 + // fetched from the env 46 + const username = handle || process.env.USERNAME; 47 + 48 + if (!username) { 49 + throw new Error('Username/Handle Not Provided'); 50 + } 51 + 52 + const appPassword = password || process.env.APP_PASSWORD; 53 + 54 + if (!appPassword) { 55 + throw new Error('App Password Not Provided'); 56 + } 57 + 45 58 if (!confirm) { 46 59 console.log('Aborting...'); 47 60 return; ··· 51 64 const agent = new AtpAgent({ 52 65 service: service ? service : 'https://bsky.social', 53 66 }); 54 - await agent.login({ identifier: handle, password }); 67 + await agent.login({ identifier: username, password: appPassword }); 55 68 56 69 await agent.com.atproto.repo.deleteRecord({ 57 70 repo: agent.session?.did ?? '',
+72 -3
src/index.ts
··· 8 8 dotenv.config(); 9 9 10 10 const logger = Logger.create('FEED'); 11 + logger.level = process.env.LOG_LEVEL || 'info'; 12 + 11 13 const app = express(); 12 14 const port = process.env.PORT || 3000; 13 15 ··· 47 49 '/xrpc/app.bsky.feed.getFeedSkeleton', 48 50 async (req: Request, res: Response) => { 49 51 try { 52 + // cursor pagination separato 53 + // cursor format -> <actual cursor value>::<did to make it unique> 54 + const separator = '::'; 55 + 56 + // used to uniquely indentify the cursor from other feeds 57 + const did = 58 + req.query.feed 59 + ?.toString() 60 + .replace('at%3S%2F', '') 61 + .replaceAll('%2F', '/') 62 + .split('/') 63 + .at(0) || ''; 64 + 65 + logger.debug({ did }); 66 + 67 + logger.debug({ 68 + headers: req.headers, 69 + body: req.body, 70 + originalUrl: req.originalUrl, 71 + searchParams: req.query, 72 + }); 73 + 74 + let limit = Number(req.query.limit) || 30; 75 + limit = limit <= 0 ? 1 : limit; 76 + limit = limit > 100 ? 100 : limit; 77 + 78 + let cursor = 79 + Number(req.query.cursor?.toString().split(separator).at(0)) || 0; 80 + 81 + cursor = cursor < 0 ? 0 : cursor; 82 + 83 + logger.debug({ cursor }); 84 + 85 + const totalPosts = 86 + Number( 87 + ( 88 + await db 89 + .selectFrom('post') 90 + .select(db.fn.count<number>('post.uri').as('totalPosts')) 91 + .executeTakeFirst() 92 + )?.totalPosts 93 + ) || 0; 94 + 95 + logger.debug({ totalPosts }); 96 + 97 + if (totalPosts === 0) { 98 + res.json({ feed: [] }); 99 + return; 100 + } 101 + 50 102 // Fetch the most recent posts from the database 51 - const posts = await db 103 + let query = db 52 104 .selectFrom('post') 53 105 .select(['uri']) 54 106 .orderBy('createdAt', 'desc') 55 - .limit(30) 56 - .execute(); 107 + .limit(limit); 108 + 109 + if (!Number.isNaN(cursor)) { 110 + query = query.offset(cursor); 111 + } 112 + 113 + const posts = await query.execute(); 114 + 115 + let nextCursor = cursor.toString(); 116 + 117 + if (cursor + limit < totalPosts) { 118 + nextCursor = (cursor + limit).toString().concat(separator).concat(did); 119 + } else { 120 + // @ts-ignore : ignored to allow undfined type excluding the property from the response 121 + nextCursor = undefined; 122 + } 123 + 124 + logger.debug({ nextCursor }); 57 125 58 126 // Transform the posts into the expected format 59 127 const feed = posts.map((post) => ({ ··· 63 131 64 132 res.json({ 65 133 feed, 134 + cursor: nextCursor, 66 135 }); 67 136 } catch (error) { 68 137 logger.error('Error fetching feed:', error);