+12
-15
bun.lock
+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
-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
+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
+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);