forked from
leaflet.pub/leaflet
a tool for shared writing and social publishing
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 });