a tool for shared writing and social publishing
at update/reader 163 lines 5.0 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 { 7 normalizeDocumentRecord, 8 type NormalizedDocument, 9} from "src/utils/normalizeRecords"; 10import { inngest } from "app/api/inngest/client"; 11import { AtUri } from "@atproto/api"; 12 13const app = new Hono(); 14 15const domain = process.env.FEED_SERVICE_URL || "feeds.leaflet.pub"; 16const serviceDid = `did:web:${domain}`; 17 18app.get("/.well-known/did.json", (c) => { 19 return c.json({ 20 "@context": ["https://www.w3.org/ns/did/v1"], 21 id: serviceDid, 22 service: [ 23 { 24 id: "#bsky_fg", 25 type: "BskyFeedGenerator", 26 serviceEndpoint: `https://${domain}`, 27 }, 28 ], 29 }); 30}); 31//Cursor format ts::uri 32 33app.get("/xrpc/app.bsky.feed.getFeedSkeleton", async (c) => { 34 let auth = await validateAuth(c.req, serviceDid); 35 let feed = c.req.query("feed"); 36 if (!auth || !feed) return c.json({ feed: [] }); 37 let cursor = c.req.query("cursor"); 38 let parsedCursor; 39 if (cursor) { 40 let date = cursor.split("::")[0]; 41 let uri = cursor.split("::")[1]; 42 parsedCursor = { date, uri }; 43 } 44 let limit = parseInt(c.req.query("limit") || "10"); 45 let feedAtURI = new AtUri(feed); 46 let posts; 47 let query; 48 if (feedAtURI.rkey == "bsky-leaflet-quotes") { 49 let query = supabaseServerClient 50 .from("document_mentions_in_bsky") 51 .select("*") 52 .order("indexed_at", { ascending: false }) 53 .order("uri", { ascending: false }) 54 .limit(25); 55 if (parsedCursor) 56 query = query.or( 57 `indexed_at.lt.${parsedCursor.date},and(indexed_at.eq.${parsedCursor.date},uri.lt.${parsedCursor.uri})`, 58 ); 59 60 let { data, error } = await query; 61 let posts = data || []; 62 63 let lastPost = posts[posts.length - 1]; 64 let newCursor = lastPost ? `${lastPost.indexed_at}::${lastPost.uri}` : null; 65 return c.json({ 66 cursor: newCursor || cursor, 67 feed: posts.flatMap((p) => { 68 return { post: p.uri }; 69 }), 70 }); 71 } 72 if (feedAtURI.rkey === "bsky-follows-leaflets") { 73 if (!cursor) { 74 console.log("Sending event"); 75 await inngest.send({ name: "feeds/index-follows", data: { did: auth } }); 76 } 77 query = supabaseServerClient 78 .from("documents") 79 .select( 80 `*, 81 documents_in_publications!inner( 82 publications!inner(*, 83 identities!publications_identity_did_fkey!inner( 84 bsky_follows!bsky_follows_follows_fkey!inner(*) 85 ) 86 ) 87 )`, 88 ) 89 .eq( 90 "documents_in_publications.publications.identities.bsky_follows.identity", 91 auth, 92 ); 93 } else if (feedAtURI.rkey === "all-leaflets") { 94 query = supabaseServerClient 95 .from("documents") 96 .select( 97 `*, 98 documents_in_publications(publications(*))`, 99 ) 100 .or( 101 "record->preferences->showInDiscover.is.null,record->preferences->>showInDiscover.eq.true", 102 { referencedTable: "documents_in_publications.publications" }, 103 ); 104 } else { 105 //the default subscription feed 106 query = supabaseServerClient 107 .from("documents") 108 .select( 109 `*, 110 documents_in_publications!inner(publications!inner(*, publication_subscriptions!inner(*)))`, 111 ) 112 .eq( 113 "documents_in_publications.publications.publication_subscriptions.identity", 114 auth, 115 ); 116 } 117 query = query 118 .or("data->postRef.not.is.null,data->bskyPostRef.not.is.null") 119 .order("sort_date", { ascending: false }) 120 .order("uri", { ascending: false }) 121 .limit(25); 122 if (parsedCursor) 123 query = query.or( 124 `sort_date.lt.${parsedCursor.date},and(sort_date.eq.${parsedCursor.date},uri.lt.${parsedCursor.uri})`, 125 ); 126 127 let { data, error } = await query; 128 console.log(error); 129 posts = data; 130 131 posts = posts || []; 132 133 let lastPost = posts[posts.length - 1]; 134 let newCursor = lastPost ? `${lastPost.sort_date}::${lastPost.uri}` : null; 135 return c.json({ 136 cursor: newCursor || cursor, 137 feed: posts.flatMap((p) => { 138 if (!p.data) return []; 139 const normalizedDoc = normalizeDocumentRecord(p.data, p.uri); 140 if (!normalizedDoc?.bskyPostRef) return []; 141 return { post: normalizedDoc.bskyPostRef.uri }; 142 }), 143 }); 144}); 145 146const didResolver = new DidResolver({}); 147const validateAuth = async ( 148 req: HonoRequest, 149 serviceDid: string, 150): Promise<string | null> => { 151 const authorization = req.header("authorization"); 152 if (!authorization?.startsWith("Bearer ")) { 153 return null; 154 } 155 const jwt = authorization.replace("Bearer ", "").trim(); 156 const nsid = parseReqNsid({ url: req.path }); 157 const parsed = await verifyJwt(jwt, serviceDid, nsid, async (did: string) => { 158 return didResolver.resolveAtprotoKey(did); 159 }); 160 return parsed.iss; 161}; 162 163serve({ fetch: app.fetch, port: 3030 });