tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
27
pulls
pipelines
add feed service
awarm.space
9 months ago
42a37014
9b64c7f4
+113
6 changed files
expand all
collapse all
unified
split
.gitignore
disco.json
feeds
Dockerfile
index.ts
package-lock.json
package.json
+1
.gitignore
···
5
6
# Appview
7
appview/dist
0
8
9
.next
10
node_modules
···
5
6
# Appview
7
appview/dist
8
+
feeds/dist
9
10
.next
11
node_modules
+7
disco.json
···
1
{
2
"version": "1.0",
3
"services": {
0
0
0
0
4
"worker": {
5
"image": "appview",
6
"volumes": [
···
12
}
13
},
14
"images": {
0
0
0
15
"appview": {
16
"dockerfile": "appview/Dockerfile"
17
}
···
1
{
2
"version": "1.0",
3
"services": {
4
+
"web": {
5
+
"image": "feed-service",
6
+
"port": 3000
7
+
},
8
"worker": {
9
"image": "appview",
10
"volumes": [
···
16
}
17
},
18
"images": {
19
+
"feed-service": {
20
+
"dockerfile": "feeds/Dockerfile"
21
+
},
22
"appview": {
23
"dockerfile": "appview/Dockerfile"
24
}
+12
feeds/Dockerfile
···
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
FROM node:22
2
+
WORKDIR /code
3
+
4
+
# start with dependencies to enjoy caching
5
+
COPY ./package.json /code/package.json
6
+
COPY ./package-lock.json /code/package-lock.json
7
+
RUN npm ci
8
+
9
+
# copy rest and build
10
+
COPY . /code/.
11
+
RUN npm run build-feed-service
12
+
CMD ["node", "/code/feeds/dist/index.js"]
+67
feeds/index.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { Hono, HonoRequest } from "hono";
2
+
import { serve } from "@hono/node-server";
3
+
import { DidResolver } from "@atproto/identity";
4
+
import { parseReqNsid, verifyJwt } from "@atproto/xrpc-server";
5
+
import { supabaseServerClient } from "supabase/serverClient";
6
+
import { PubLeafletDocument } from "lexicons/api";
7
+
8
+
const app = new Hono();
9
+
10
+
const domain = "feeds.leaflet.pub";
11
+
const serviceDid = `did:web:${domain}`;
12
+
13
+
app.get("/.well-known/did.json", (c) => {
14
+
return c.json({
15
+
"@context": ["https://www.w3.org/ns/did/v1"],
16
+
id: serviceDid,
17
+
service: [
18
+
{
19
+
id: "#bsky_fg",
20
+
type: "BskyFeedGenerator",
21
+
serviceEndpoint: `https://${domain}`,
22
+
},
23
+
],
24
+
});
25
+
});
26
+
27
+
app.get("/xrpc/app.bsky.feed.getFeedSkeleton", async (c) => {
28
+
let auth = await validateAuth(c.req, serviceDid);
29
+
if (!auth) return c.json({ feed: [] });
30
+
31
+
let { data: publications } = await supabaseServerClient
32
+
.from("publication_subscriptions")
33
+
.select(`publications(documents_in_publications(documents(*)))`)
34
+
.eq("identity", auth);
35
+
const feed = (publications || []).flatMap((pub) => {
36
+
let posts = pub.publications?.documents_in_publications || [];
37
+
return posts.flatMap((p) => {
38
+
if (!p.documents?.data) return [];
39
+
let record = p.documents.data as PubLeafletDocument.Record;
40
+
if (!record.postRef) return [];
41
+
return { post: record.postRef.uri };
42
+
});
43
+
});
44
+
45
+
return c.json({
46
+
feed,
47
+
});
48
+
});
49
+
50
+
const didResolver = new DidResolver({});
51
+
const validateAuth = async (
52
+
req: HonoRequest,
53
+
serviceDid: string,
54
+
): Promise<string | null> => {
55
+
const authorization = req.header("authorization");
56
+
if (!authorization?.startsWith("Bearer ")) {
57
+
return null;
58
+
}
59
+
const jwt = authorization.replace("Bearer ", "").trim();
60
+
const nsid = parseReqNsid(req);
61
+
const parsed = await verifyJwt(jwt, serviceDid, nsid, async (did: string) => {
62
+
return didResolver.resolveAtprotoKey(did);
63
+
});
64
+
return parsed.iss;
65
+
};
66
+
67
+
serve(app);
+23
package-lock.json
···
16
"@atproto/sync": "^0.1.23",
17
"@atproto/syntax": "^0.3.3",
18
"@atproto/xrpc": "^0.6.9",
0
19
"@mdx-js/loader": "^3.1.0",
20
"@mdx-js/react": "^3.1.0",
21
"@next/bundle-analyzer": "^15.3.2",
···
38
"drizzle-orm": "^0.30.10",
39
"feed": "^5.0.1",
40
"fractional-indexing": "^3.2.0",
0
41
"linkifyjs": "^4.2.0",
42
"multiformats": "^13.3.2",
43
"next": "15.3.2",
···
1839
"license": "MIT",
1840
"dependencies": {
1841
"tslib": "^2.8.0"
0
0
0
0
0
0
0
0
0
0
0
0
1842
}
1843
},
1844
"node_modules/@humanwhocodes/config-array": {
···
10159
"resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz",
10160
"integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==",
10161
"dev": true
0
0
0
0
0
0
0
0
0
10162
},
10163
"node_modules/html-escaper": {
10164
"version": "2.0.2",
···
16
"@atproto/sync": "^0.1.23",
17
"@atproto/syntax": "^0.3.3",
18
"@atproto/xrpc": "^0.6.9",
19
+
"@hono/node-server": "^1.14.3",
20
"@mdx-js/loader": "^3.1.0",
21
"@mdx-js/react": "^3.1.0",
22
"@next/bundle-analyzer": "^15.3.2",
···
39
"drizzle-orm": "^0.30.10",
40
"feed": "^5.0.1",
41
"fractional-indexing": "^3.2.0",
42
+
"hono": "^4.7.11",
43
"linkifyjs": "^4.2.0",
44
"multiformats": "^13.3.2",
45
"next": "15.3.2",
···
1841
"license": "MIT",
1842
"dependencies": {
1843
"tslib": "^2.8.0"
1844
+
}
1845
+
},
1846
+
"node_modules/@hono/node-server": {
1847
+
"version": "1.14.3",
1848
+
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.3.tgz",
1849
+
"integrity": "sha512-KuDMwwghtFYSmIpr4WrKs1VpelTrptvJ+6x6mbUcZnFcc213cumTF5BdqfHyW93B19TNI4Vaev14vOI2a0Ie3w==",
1850
+
"license": "MIT",
1851
+
"engines": {
1852
+
"node": ">=18.14.1"
1853
+
},
1854
+
"peerDependencies": {
1855
+
"hono": "^4"
1856
}
1857
},
1858
"node_modules/@humanwhocodes/config-array": {
···
10173
"resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz",
10174
"integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==",
10175
"dev": true
10176
+
},
10177
+
"node_modules/hono": {
10178
+
"version": "4.7.11",
10179
+
"resolved": "https://registry.npmjs.org/hono/-/hono-4.7.11.tgz",
10180
+
"integrity": "sha512-rv0JMwC0KALbbmwJDEnxvQCeJh+xbS3KEWW5PC9cMJ08Ur9xgatI0HmtgYZfOdOSOeYsp5LO2cOhdI8cLEbDEQ==",
10181
+
"license": "MIT",
10182
+
"engines": {
10183
+
"node": ">=16.9.0"
10184
+
}
10185
},
10186
"node_modules/html-escaper": {
10187
"version": "2.0.2",
+3
package.json
···
10
"lexgen": "tsx ./lexicons/build.ts && lex gen-api ./lexicons/api ./lexicons/pub/leaflet/* ./lexicons/pub/leaflet/*/* ./lexicons/com/atproto/*/* --yes && find './lexicons/api' -type f -exec sed -i 's/\\.js'/'/g' {} \\;",
11
"wrangler-dev": "wrangler dev",
12
"build-appview": "esbuild appview/index.ts --outfile=appview/dist/index.js --bundle --platform=node",
0
13
"start-appview-dev": "tsx --env-file='./.env.local' --watch appview/index.ts",
14
"start-appview-prod": "npm run build-appview && node appview/dist/index.js"
15
},
···
24
"@atproto/sync": "^0.1.23",
25
"@atproto/syntax": "^0.3.3",
26
"@atproto/xrpc": "^0.6.9",
0
27
"@mdx-js/loader": "^3.1.0",
28
"@mdx-js/react": "^3.1.0",
29
"@next/bundle-analyzer": "^15.3.2",
···
46
"drizzle-orm": "^0.30.10",
47
"feed": "^5.0.1",
48
"fractional-indexing": "^3.2.0",
0
49
"linkifyjs": "^4.2.0",
50
"multiformats": "^13.3.2",
51
"next": "15.3.2",
···
10
"lexgen": "tsx ./lexicons/build.ts && lex gen-api ./lexicons/api ./lexicons/pub/leaflet/* ./lexicons/pub/leaflet/*/* ./lexicons/com/atproto/*/* --yes && find './lexicons/api' -type f -exec sed -i 's/\\.js'/'/g' {} \\;",
11
"wrangler-dev": "wrangler dev",
12
"build-appview": "esbuild appview/index.ts --outfile=appview/dist/index.js --bundle --platform=node",
13
+
"build-feed-service": "esbuild feeds/index.ts --outfile=feeds/dist/index.js --bundle --platform=node",
14
"start-appview-dev": "tsx --env-file='./.env.local' --watch appview/index.ts",
15
"start-appview-prod": "npm run build-appview && node appview/dist/index.js"
16
},
···
25
"@atproto/sync": "^0.1.23",
26
"@atproto/syntax": "^0.3.3",
27
"@atproto/xrpc": "^0.6.9",
28
+
"@hono/node-server": "^1.14.3",
29
"@mdx-js/loader": "^3.1.0",
30
"@mdx-js/react": "^3.1.0",
31
"@next/bundle-analyzer": "^15.3.2",
···
48
"drizzle-orm": "^0.30.10",
49
"feed": "^5.0.1",
50
"fractional-indexing": "^3.2.0",
51
+
"hono": "^4.7.11",
52
"linkifyjs": "^4.2.0",
53
"multiformats": "^13.3.2",
54
"next": "15.3.2",