(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
98
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 110 lines 3.4 kB view raw
1import type { UserProfile } from "../types"; 2 3const API_URL = 4 process.env.API_URL || `http://localhost:${process.env.API_PORT || 8081}`; 5 6function serverFetch(path: string, cookie?: string): Promise<Response> { 7 const headers: Record<string, string> = { 8 "Content-Type": "application/json", 9 }; 10 if (cookie) headers["Cookie"] = cookie; 11 return fetch(`${API_URL}${path}`, { headers }); 12} 13 14const sessionCache = new Map<string, { user: UserProfile; expires: number }>(); 15 16export function clearSessionCacheForCookie(cookie: string) { 17 const cacheKey = cookie.match(/margin_session=([^;]+)/)?.[1] || ""; 18 if (cacheKey) sessionCache.delete(cacheKey); 19} 20 21export async function getSession(cookie: string): Promise<UserProfile | null> { 22 try { 23 const cacheKey = cookie.match(/margin_session=([^;]+)/)?.[1] || ""; 24 const cached = sessionCache.get(cacheKey); 25 if (cached && Date.now() < cached.expires) { 26 return cached.user; 27 } 28 29 const res = await serverFetch("/auth/session", cookie); 30 if (!res.ok) return null; 31 const data = await res.json(); 32 if (!data.authenticated && !data.did) return null; 33 34 const profile: UserProfile = { 35 did: data.did, 36 handle: data.handle, 37 displayName: data.displayName, 38 avatar: data.avatar, 39 description: data.description, 40 website: data.website, 41 links: data.links, 42 followersCount: data.followersCount, 43 followsCount: data.followsCount, 44 postsCount: data.postsCount, 45 }; 46 47 if (cacheKey) { 48 sessionCache.set(cacheKey, { 49 user: profile, 50 expires: Date.now() + 5 * 60_000, 51 }); 52 if (sessionCache.size > 100) { 53 const now = Date.now(); 54 for (const [k, v] of sessionCache) { 55 if (now > v.expires) sessionCache.delete(k); 56 } 57 } 58 } 59 60 const controller = new AbortController(); 61 const timeout = setTimeout(() => controller.abort(), 3000); 62 63 Promise.allSettled([ 64 fetch( 65 `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(data.did)}`, 66 { signal: controller.signal }, 67 ), 68 serverFetch(`/api/profile/${data.did}`, cookie), 69 ]) 70 .then(([bskyRes, marginRes]) => { 71 clearTimeout(timeout); 72 73 if (bskyRes.status === "fulfilled" && bskyRes.value.ok) { 74 bskyRes.value 75 .json() 76 .then((bsky) => { 77 if (bsky.avatar) profile.avatar = bsky.avatar; 78 if (bsky.displayName) profile.displayName = bsky.displayName; 79 }) 80 .catch(() => { 81 /* ignore */ 82 }); 83 } 84 85 if (marginRes.status === "fulfilled" && marginRes.value.ok) { 86 marginRes.value 87 .json() 88 .then((mp) => { 89 if (mp?.description) profile.description = mp.description; 90 if (mp?.followersCount) 91 profile.followersCount = mp.followersCount; 92 if (mp?.followsCount) profile.followsCount = mp.followsCount; 93 if (mp?.postsCount) profile.postsCount = mp.postsCount; 94 if (mp?.website) profile.website = mp.website; 95 if (mp?.links) profile.links = mp.links; 96 }) 97 .catch(() => { 98 /* ignore */ 99 }); 100 } 101 }) 102 .catch(() => { 103 clearTimeout(timeout); 104 }); 105 106 return profile; 107 } catch { 108 return null; 109 } 110}