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