a tool for shared writing and social publishing
at feature/reader 104 lines 3.4 kB view raw
1import { Hono, HonoRequest } from "hono"; 2import { serve } from "@hono/node-server"; 3import { DidResolver } from "@atproto/identity"; 4import { parseReqNsid, verifyJwt } from "@atproto/xrpc-server"; 5import { supabaseServerClient } from "supabase/serverClient"; 6import { PubLeafletDocument } from "lexicons/api"; 7 8const app = new Hono(); 9 10const domain = process.env.FEED_SERVICE_URL || "feeds.leaflet.pub"; 11const serviceDid = `did:web:${domain}`; 12 13app.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//Cursor format ts::uri 27 28app.get("/xrpc/app.bsky.feed.getFeedSkeleton", async (c) => { 29 let auth = await validateAuth(c.req, serviceDid); 30 if (!auth) return c.json({ feed: [] }); 31 let cursor = c.req.query("cursor"); 32 let limit = parseInt(c.req.query("limit") || "10"); 33 34 let { data: publications } = await supabaseServerClient 35 .from("publication_subscriptions") 36 .select(`publications(*, documents_in_publications(documents(*)))`) 37 .eq("identity", auth); 38 39 const allPosts = (publications || []) 40 .flatMap((pub) => { 41 let posts = pub.publications?.documents_in_publications || []; 42 return posts; 43 }) 44 .sort((a, b) => { 45 let aRecord = a.documents?.data! as PubLeafletDocument.Record; 46 let bRecord = b.documents?.data! as PubLeafletDocument.Record; 47 const aDate = aRecord.publishedAt 48 ? new Date(aRecord.publishedAt) 49 : new Date(0); 50 const bDate = bRecord.publishedAt 51 ? new Date(bRecord.publishedAt) 52 : new Date(0); 53 return bDate.getTime() - aDate.getTime(); // Sort by most recent first 54 }); 55 let posts; 56 if (!cursor) { 57 posts = allPosts.slice(0, 25); 58 } else { 59 let date = cursor.split("::")[0]; 60 let uri = cursor.split("::")[1]; 61 posts = allPosts 62 .filter((p) => { 63 if (!p.documents?.data) return false; 64 let record = p.documents.data as PubLeafletDocument.Record; 65 if (!record.publishedAt) return false; 66 return record.publishedAt <= date && uri !== p.documents?.uri; 67 }) 68 .slice(0, 25); 69 } 70 71 let lastPost = posts[posts.length - 1]; 72 let lastRecord = lastPost?.documents?.data! as PubLeafletDocument.Record; 73 let newCursor = lastRecord 74 ? `${lastRecord.publishedAt}::${lastPost.documents?.uri}` 75 : null; 76 return c.json({ 77 cursor: newCursor || cursor, 78 feed: posts.flatMap((p) => { 79 if (!p.documents?.data) return []; 80 let record = p.documents.data as PubLeafletDocument.Record; 81 if (!record.postRef) return []; 82 return { post: record.postRef.uri }; 83 }), 84 }); 85}); 86 87const didResolver = new DidResolver({}); 88const validateAuth = async ( 89 req: HonoRequest, 90 serviceDid: string, 91): Promise<string | null> => { 92 const authorization = req.header("authorization"); 93 if (!authorization?.startsWith("Bearer ")) { 94 return null; 95 } 96 const jwt = authorization.replace("Bearer ", "").trim(); 97 const nsid = parseReqNsid({ url: req.path }); 98 const parsed = await verifyJwt(jwt, serviceDid, nsid, async (did: string) => { 99 return didResolver.resolveAtprotoKey(did); 100 }); 101 return parsed.iss; 102}; 103 104serve({ fetch: app.fetch, port: 3030 });