a tool for shared writing and social publishing

add feed service

+113
+1
.gitignore
··· 5 5 6 6 # Appview 7 7 appview/dist 8 + feeds/dist 8 9 9 10 .next 10 11 node_modules
+7
disco.json
··· 1 1 { 2 2 "version": "1.0", 3 3 "services": { 4 + "web": { 5 + "image": "feed-service", 6 + "port": 3000 7 + }, 4 8 "worker": { 5 9 "image": "appview", 6 10 "volumes": [ ··· 12 16 } 13 17 }, 14 18 "images": { 19 + "feed-service": { 20 + "dockerfile": "feeds/Dockerfile" 21 + }, 15 22 "appview": { 16 23 "dockerfile": "appview/Dockerfile" 17 24 }
+12
feeds/Dockerfile
··· 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
··· 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 16 "@atproto/sync": "^0.1.23", 17 17 "@atproto/syntax": "^0.3.3", 18 18 "@atproto/xrpc": "^0.6.9", 19 + "@hono/node-server": "^1.14.3", 19 20 "@mdx-js/loader": "^3.1.0", 20 21 "@mdx-js/react": "^3.1.0", 21 22 "@next/bundle-analyzer": "^15.3.2", ··· 38 39 "drizzle-orm": "^0.30.10", 39 40 "feed": "^5.0.1", 40 41 "fractional-indexing": "^3.2.0", 42 + "hono": "^4.7.11", 41 43 "linkifyjs": "^4.2.0", 42 44 "multiformats": "^13.3.2", 43 45 "next": "15.3.2", ··· 1839 1841 "license": "MIT", 1840 1842 "dependencies": { 1841 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" 1842 1856 } 1843 1857 }, 1844 1858 "node_modules/@humanwhocodes/config-array": { ··· 10159 10173 "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", 10160 10174 "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", 10161 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 + } 10162 10185 }, 10163 10186 "node_modules/html-escaper": { 10164 10187 "version": "2.0.2",
+3
package.json
··· 10 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 11 "wrangler-dev": "wrangler dev", 12 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", 13 14 "start-appview-dev": "tsx --env-file='./.env.local' --watch appview/index.ts", 14 15 "start-appview-prod": "npm run build-appview && node appview/dist/index.js" 15 16 }, ··· 24 25 "@atproto/sync": "^0.1.23", 25 26 "@atproto/syntax": "^0.3.3", 26 27 "@atproto/xrpc": "^0.6.9", 28 + "@hono/node-server": "^1.14.3", 27 29 "@mdx-js/loader": "^3.1.0", 28 30 "@mdx-js/react": "^3.1.0", 29 31 "@next/bundle-analyzer": "^15.3.2", ··· 46 48 "drizzle-orm": "^0.30.10", 47 49 "feed": "^5.0.1", 48 50 "fractional-indexing": "^3.2.0", 51 + "hono": "^4.7.11", 49 52 "linkifyjs": "^4.2.0", 50 53 "multiformats": "^13.3.2", 51 54 "next": "15.3.2",