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";
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 });