Something went wrong. Try again.
leaflet fork for copy edit / bug fixes forked from leaflet.pub/leaflet
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241import { Hono, HonoRequest } from "hono";import { serve } from "@hono/node-server";import { DidResolver, MemoryCache } from "@atproto/identity";import { parseReqNsid, verifyJwt } from "@atproto/xrpc-server";import { supabaseServerClient } from "supabase/serverClient";import { inngest } from "app/api/inngest/client";import { AtUri } from "@atproto/api";import { wikipedia } from "../mentions/services/wikipedia";import { pokemon } from "../mentions/services/pokemon";
const app = new Hono();
const domain = process.env.FEED_SERVICE_URL || "feeds.leaflet.pub";const serviceDid = `did:web:${domain}`;
app.get("/.well-known/did.json", (c) => { return c.json({ "@context": ["https://www.w3.org/ns/did/v1"], id: serviceDid, service: [ { id: "#bsky_fg", type: "BskyFeedGenerator", serviceEndpoint: `https://${domain}`, }, { id: "#mention_search", type: "MentionSearchService", serviceEndpoint: `https://${domain}`, }, ], });});
// Mention search services, keyed by rkeyconst mentionServices: Record< string, (search: string, limit: number) => Promise<{ uri: string; name: string; href?: string; icon?: string; embed?: { src: string; width?: number; height?: number } }[]>> = { wikipedia, pokemon,};
app.get("/xrpc/parts.page.mention.search", async (c) => { const serviceUri = c.req.query("service"); const search = c.req.query("search"); const limit = Math.min( Math.max(parseInt(c.req.query("limit") || "20"), 1), 50, );
if (!serviceUri || !search) { return c.json({ error: "missing required parameters: service, search" }, 400); }
let rkey: string; try { const parsed = new AtUri(serviceUri); rkey = parsed.rkey; } catch { return c.json({ error: "invalid service AT URI" }, 400); }
const handler = mentionServices[rkey]; if (!handler) { return c.json({ error: `unknown service: ${rkey}` }, 404); }
const results = await handler(search, limit); return c.json({ results });});
//Cursor format ts::uri
// Everything the skeleton response needs from a document. postRef is the// pub.leaflet field, bskyPostRef the site.standard one; publishedAt gates// pub.leaflet records the same way normalizeDocument does.const SKELETON_COLUMNS = `uri, sort_date, postRef:data->postRef, bskyPostRef:data->bskyPostRef, publishedAt:data->>publishedAt`;
app.get("/xrpc/app.bsky.feed.getFeedSkeleton", async (c) => { let feed = c.req.query("feed"); if (!feed) return c.json({ feed: [] }); let cursor = c.req.query("cursor"); let parsedCursor; if (cursor) { let date = cursor.split("::")[0]; let uri = cursor.split("::")[1]; parsedCursor = { date, uri }; } let limit = parseInt(c.req.query("limit") || "10"); let feedAtURI = new AtUri(feed); let posts; let query; // The quotes feed isn't personalized, so serve it without auth — the bsky // appview omits the viewer JWT for logged-out requests, feed previews, and // crawlers, and those should still see the feed. if (feedAtURI.rkey == "bsky-leaflet-quotes") { let query = supabaseServerClient .from("document_mentions_in_bsky") .select("uri, indexed_at") .order("indexed_at", { ascending: false }) .order("uri", { ascending: false }) .limit(25); if (parsedCursor) query = query.or( `indexed_at.lt.${parsedCursor.date},and(indexed_at.eq.${parsedCursor.date},uri.lt.${parsedCursor.uri})`, );
let { data, error } = await query; if (error) console.error("bsky-leaflet-quotes skeleton query error:", error); let posts = data || [];
let lastPost = posts[posts.length - 1]; let newCursor = lastPost ? `${lastPost.indexed_at}::${lastPost.uri}` : null; return c.json({ cursor: newCursor || cursor, feed: posts.flatMap((p) => { return { post: p.uri }; }), }); } let auth = await validateAuth(c.req, serviceDid); if (!auth) return c.json({ feed: [] }); if (feedAtURI.rkey === "bsky-follows-leaflets") { if (!cursor) { console.log("Sending event"); await inngest.send({ name: "feeds/index-follows", data: { did: auth } }); } // plan-checked: KNOWN DEBT — the follow filter lives in !inner embeds, so // the newest-first limit walks documents_sort_date_idx probing the embeds // per document, scanning the whole table for users whose follows have few // or old posts. Needs a fenced SQL function like get_reader_feed. query = supabaseServerClient .from("documents") // The skeleton only needs the bsky post ref out of each document, so // project just the refs instead of the full record jsonb; the embedded // tables exist purely to filter and select as little as possible. .select( `${SKELETON_COLUMNS}, documents_in_publications!inner( publications!inner(uri, identities!publications_identity_did_fkey!inner( bsky_follows!bsky_follows_follows_fkey!inner(identity) ) ) )`, ) .eq( "documents_in_publications.publications.identities.bsky_follows.identity", auth, ); } else if (feedAtURI.rkey === "all-leaflets") { query = supabaseServerClient .from("documents") .select( `${SKELETON_COLUMNS}, documents_in_publications(publications(uri))`, ) .or( "record->preferences->showInDiscover.is.null,record->preferences->>showInDiscover.eq.true", { referencedTable: "documents_in_publications.publications" }, ); } else { //the default subscription feed // plan-checked: KNOWN DEBT — same shape as bsky-follows-leaflets above: // subscribers with few or quiet subscriptions scan the whole documents // table. get_reader_feed already fences this exact join for the reader // UI; this skeleton needs the same treatment. query = supabaseServerClient .from("documents") .select( `${SKELETON_COLUMNS}, documents_in_publications!inner(publications!inner(uri, publication_subscriptions!inner(identity)))`, ) .eq( "documents_in_publications.publications.publication_subscriptions.identity", auth, ); } query = query .eq("indexed", true) .or("data->postRef.not.is.null,data->bskyPostRef.not.is.null") .order("sort_date", { ascending: false }) .order("uri", { ascending: false }) .limit(25); if (parsedCursor) query = query.or( `sort_date.lt.${parsedCursor.date},and(sort_date.eq.${parsedCursor.date},uri.lt.${parsedCursor.uri})`, );
let { data, error } = await query; console.log(error); posts = data;
posts = posts || [];
let lastPost = posts[posts.length - 1]; let newCursor = lastPost ? `${lastPost.sort_date}::${lastPost.uri}` : null; return c.json({ cursor: newCursor || cursor, feed: posts.flatMap((p) => { let ref = (p.bskyPostRef ?? (p.publishedAt ? p.postRef : null)) as { uri?: string; } | null; if (!ref?.uri) return []; return { post: ref.uri }; }), });});
const didResolver = new DidResolver({ didCache: new MemoryCache() });const validateAuth = async ( req: HonoRequest, serviceDid: string,): Promise<string | null> => { const authorization = req.header("authorization"); if (!authorization?.startsWith("Bearer ")) { return null; } const jwt = authorization.replace("Bearer ", "").trim(); const nsid = parseReqNsid({ url: req.path }); // verifyJwt throws on a bad/expired token; treat that as unauthenticated // rather than letting the whole skeleton request 500. try { const parsed = await verifyJwt( jwt, serviceDid, nsid, async (did: string) => { return didResolver.resolveAtprotoKey(did); }, ); return parsed.iss; } catch (err) { console.error("feed skeleton auth failed:", err); return null; }};
serve({ fetch: app.fetch, port: 3030 });