Something went wrong. Try again.
A Bsky-like frontend using the atprotocol natively.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333import express from 'express';import { resolveHandle, resolvePds, getRecord, getProfileRecord, getProfileCounts, formatCount, getFeedGenerator, getFeedSkeleton, getHydratedPosts } from './lib/resolver.js';import type { PostRecord, PostEmbed, PostEmbedImage, HydratedPost } from './lib/resolver.js';import { renderOgHtml } from './lib/render.js';import type { OgImage, OgVideo } from './lib/render.js';import { generateProfileImage } from './lib/composite.js';
const app = express();const PORT = Number(process.env.PORT ?? 3001);const BASE_URL = (process.env.BASE_URL ?? `http://localhost`).replace(/\/$/, '');
const compositeCache = new Map<string, { png: Buffer; ts: number }>();const COMPOSITE_TTL = 300_000;
app.get('/profile-img/:actor', async (req, res) => { try { const actor = req.params.actor; const did = actor.startsWith('did:') ? actor : await resolveHandle(actor); const pds = await resolvePds(did);
// Serve from cache when fresh const cached = compositeCache.get(did); if (cached && Date.now() - cached.ts < COMPOSITE_TTL) { res.setHeader('Content-Type', 'image/png'); res.setHeader('Cache-Control', 'public, max-age=300'); res.send(cached.png); return; }
const [{ value: profile }, counts] = await Promise.all([ getProfileRecord(pds, did), getProfileCounts(pds, did), ]);
const actorHandle = actor.startsWith('did:') ? actor : actor; const png = await generateProfileImage(pds, did, profile, { displayName: profile.displayName, handle: actorHandle, pronouns: counts.pronouns, });
compositeCache.set(did, { png, ts: Date.now() });
res.setHeader('Content-Type', 'image/png'); res.setHeader('Cache-Control', 'public, max-age=300'); res.send(png); } catch (err: any) { console.error('[profile-img]', err.message); res.status(502).send('Failed to render profile image'); }});
app.get('/profile/:actor', async (req, res) => { try { const actor = req.params.actor; const did = actor.startsWith('did:') ? actor : await resolveHandle(actor); const pds = await resolvePds(did);
const [{ value: profile }, counts] = await Promise.all([ getProfileRecord(pds, did), getProfileCounts(pds, did), ]);
const displayName = profile.displayName || actor; const pageUrl = `${BASE_URL}/profile/${encodeURIComponent(actor)}`; const imageUrl = `${BASE_URL}/profile-img/${encodeURIComponent(actor)}`;
const bio = (profile.description || `@${actor} on Bluesky`).slice(0, 300); const statsParts: string[] = []; if (counts.followersCount != null) statsParts.push(`${formatCount(counts.followersCount)} followers`); if (counts.followsCount != null) statsParts.push(`${formatCount(counts.followsCount)} following`); if (counts.postsCount != null) statsParts.push(`${formatCount(counts.postsCount)} posts`); const statsLine = statsParts.length ? statsParts.join(' · ') : ''; const description = statsLine ? `${bio}\n${statsLine}` : bio;
const followerTag = counts.followersCount != null ? ` (${formatCount(counts.followersCount)} followers)` : '';
const html = renderOgHtml({ url: pageUrl, title: `${displayName} (@${actor})${followerTag}`, description, image: imageUrl, imageAlt: `${displayName}'s profile`, type: 'profile', });
res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('Cache-Control', 'public, max-age=300'); res.send(html); } catch (err: any) { console.error('[profile]', err.message); res.status(502).send('Failed to generate OG tags'); }});
app.get('/post/:actor/:rkey', async (req, res) => { await handlePostOg(req, res, req.params.actor, req.params.rkey);});
app.get('/profile/:actor/post/:rkey', async (req, res) => { await handlePostOg(req, res, req.params.actor, req.params.rkey);});
async function handlePostOg(_req: express.Request, res: express.Response, actor: string, rkey: string) { try { const did = actor.startsWith('did:') ? actor : await resolveHandle(actor); const pds = await resolvePds(did);
const [{ value: post }, { value: profile }, counts] = await Promise.all([ getRecord<PostRecord>(pds, did, 'app.bsky.feed.post', rkey), getProfileRecord(pds, did), getProfileCounts(pds, did), ]);
const displayName = profile.displayName || actor; const followerTag = counts.followersCount != null ? ` (${formatCount(counts.followersCount)} followers)` : ''; const postText = (post.text || '').slice(0, 600); const pageUrl = `${BASE_URL}/post/${encodeURIComponent(actor)}/${rkey}`;
const media = extractEmbedMedia(pds, did, post.embed);
const descriptionParts: string[] = [postText];
const extLink = extractExternalLink(post.embed); if (extLink) { const linkPart = [extLink.title, extLink.description].filter(Boolean).join(' — '); if (linkPart) descriptionParts.push(`🔗 ${linkPart}`); }
if (hasQuotePost(post.embed)) { descriptionParts.push('↗ Quoted post'); }
const description = descriptionParts.join('\n').slice(0, 800);
const primaryImage = media.images[0]?.url ?? media.video?.thumbnail;
const html = renderOgHtml({ url: pageUrl, title: `${displayName}${followerTag} on Bluesky`, description, image: primaryImage, imageAlt: postText.slice(0, 200), images: media.images, video: media.video, type: media.video?.isGif ? 'video.other' : 'article', });
res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('Cache-Control', 'public, max-age=300'); res.send(html); } catch (err: any) { console.error('[post]', err.message); res.status(502).send('Failed to generate OG tags'); }}
interface ExtractedMedia { images: OgImage[]; video?: OgVideo & { thumbnail?: string; isGif?: boolean };}
function extractEmbedMedia(pds: string, did: string, embed?: PostEmbed): ExtractedMedia { if (!embed) return { images: [] };
const blobUrl = (cid: string) => `${pds}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`;
if (embed.$type === 'app.bsky.embed.images') { const imgs = (embed as { images: PostEmbedImage[] }).images; const images: OgImage[] = (imgs ?? []).map((img) => ({ url: img.image?.ref?.$link ? blobUrl(img.image.ref.$link) : '', alt: img.alt, width: img.aspectRatio?.width, height: img.aspectRatio?.height, })).filter((img) => img.url);
const firstImg = imgs?.[0]; if (firstImg?.image?.mimeType === 'image/gif' && images.length > 0) { const gifUrl = images[0].url; return { images, video: { url: gifUrl, mimeType: 'video/gif', width: images[0].width, height: images[0].height, isGif: true, }, }; }
return { images }; }
if (embed.$type === 'app.bsky.embed.video') { const e = embed as { video: { ref: { $link: string }; mimeType: string }; thumbnail?: { ref: { $link: string }; mimeType: string }; alt?: string; aspectRatio?: { width: number; height: number }; presentation?: 'default' | 'gif' | string; }; const videoUrl = e.video?.ref?.$link ? blobUrl(e.video.ref.$link) : undefined; const thumbnailUrl = e.thumbnail?.ref?.$link ? blobUrl(e.thumbnail.ref.$link) : undefined; const isGif = e.presentation === 'gif'; if (videoUrl) { return { images: thumbnailUrl ? [{ url: thumbnailUrl, alt: e.alt, width: e.aspectRatio?.width, height: e.aspectRatio?.height }] : [], video: { url: videoUrl, mimeType: e.video.mimeType, width: e.aspectRatio?.width, height: e.aspectRatio?.height, thumbnail: thumbnailUrl, isGif, }, }; } return { images: [] }; }
if (embed.$type === 'app.bsky.embed.external') { const e = embed as { external: { uri: string; title: string; description: string; thumb?: { ref: { $link: string }; mimeType: string } } }; const thumbUrl = e.external?.thumb?.ref?.$link ? blobUrl(e.external.thumb.ref.$link) : undefined; const images = thumbUrl ? [{ url: thumbUrl, alt: e.external.title }] : []; return { images }; }
if (embed.$type === 'app.bsky.embed.recordWithMedia') { const e = embed as { media: PostEmbed; record: { uri: string; cid: string } }; const media = extractEmbedMedia(pds, did, e.media); return media; }
return { images: [] };}
function extractExternalLink(embed?: PostEmbed): { uri: string; title: string; description: string } | undefined { if (!embed) return undefined;
if (embed.$type === 'app.bsky.embed.external') { const e = embed as { external: { uri: string; title: string; description: string } }; return e.external; }
if (embed.$type === 'app.bsky.embed.recordWithMedia') { const e = embed as { media: PostEmbed }; return extractExternalLink(e.media); }
return undefined;}
function hasQuotePost(embed?: PostEmbed): boolean { if (!embed) return false; if (embed.$type === 'app.bsky.embed.record') return true; if (embed.$type === 'app.bsky.embed.recordWithMedia') return true; return false;}
app.get('/feed/:did/:feedname', async (req, res) => { try { const { did, feedname } = req.params;
if (!did.startsWith('did:')) { res.status(400).send('Invalid DID format'); return; }
const feedUri = `at://${did}/app.bsky.feed.generator/${feedname}`; const pageUrl = `${BASE_URL}/feed/${encodeURIComponent(did)}/${encodeURIComponent(feedname)}`;
const [feedGen, skeleton] = await Promise.all([ getFeedGenerator(feedUri), getFeedSkeleton(feedUri, 5), ]);
const postUris = skeleton.map((item) => item.post); const hydratedPosts = await getHydratedPosts(postUris);
const feedName = feedGen.displayName || feedname; const feedDescription = feedGen.description || ''; const creator = feedGen.creator;
const descriptionParts: string[] = []; if (feedDescription) { descriptionParts.push(feedDescription.slice(0, 300)); }
const previewLines = hydratedPosts .slice(0, 3) .map((post) => { const handle = post.author?.handle ?? 'unknown'; const text = (post.record?.text ?? '').slice(0, 120).replace(/\n/g, ' '); return `· ${handle}: ${text}`; }); if (previewLines.length > 0) { descriptionParts.push('Recent posts:'); descriptionParts.push(...previewLines); }
const description = descriptionParts.join('\n').slice(0, 800);
const imageUrl = feedGen.avatar || creator?.avatar || undefined;
const creatorTag = creator?.handle ? ` by @${creator.handle}` : ''; const likeTag = feedGen.likeCount != null ? ` (${formatCount(feedGen.likeCount)} likes)` : ''; const onlineTag = feedGen.isOnline === false ? ' ⚠ offline' : '';
const html = renderOgHtml({ url: pageUrl, title: `${feedName}${creatorTag}${likeTag}${onlineTag}`, description, image: imageUrl, imageAlt: `${feedName} feed on Bluesky`, type: 'website', });
res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('Cache-Control', 'public, max-age=300'); res.send(html); } catch (err: any) { console.error('[feed]', err.message); res.status(502).send('Failed to generate feed OG tags'); }});
app.listen(PORT, () => { console.log(`Foxsky OG server listening on port ${PORT}`); console.log(` BASE_URL = ${BASE_URL}`);});