import { AtpAgent } from "@atproto/api"; export async function getTimeline(identifier, password) { const agent = new AtpAgent({ service: "https://melkat.blue" }); await agent.login({ identifier, password, }); const did = agent.assertDid; if (!did) { return; } return await agent.getTimeline({ limit: 100 }); } function filter(feed) { return feed.filter(({ reply, reason }) => { return !(reply || reason); }); } function parseFacets(facets) { const links = []; const tags = []; if (Array.isArray(facets)) { for (const facet of facets) { if (Array.isArray(facet.features)) { for (const feature of facet.features) { switch (feature?.$type) { case "app.bsky.richtext.facet#link": links.push(feature.uri); break; case "app.bsky.richtext.facet#tag": tags.push(feature.tag); break; default: break; } } } } } return { links, tags }; } function parseEmbed(embed) { const images = []; const video = {}; const quote = {}; switch (embed?.$type) { case "app.bsky.embed.images#view": if (embed.images) { for (const image of embed.images) { images.push({ fullsize: image?.fullsize, alt: image?.alt, }); } } break; case "app.bsky.embed.video#view": video.playlist = embed.playlist; video.thumbnail = embed.thumbnail; video.alt = embed.alt; break; case "app.bsky.embed.record#view": if (embed.record) { quote.handle = embed.record.author.handle; quote.displayName = embed.record.author.displayName; quote.avatar = embed.record.author.avatar; quote.url = `https://bsky.app/profile/${ quote.handle }/post/${embed.record.uri.split("/").at(-1)}`; const moreEmbeds = parseEmbed(embed.record?.embeds?.[0]); const { links, tags } = parseFacets(embed?.record?.value?.facets); quote.text = embed?.record?.value?.text; quote.images = moreEmbeds?.images; quote.video = moreEmbeds?.video; quote.links = links; quote.tags = tags; quote.stats = { likeCount: embed.record?.likeCount || 0, replyCount: embed.record?.replyCount || 0, repostCount: embed.record?.repostCount || 0, quoteCount: embed.record?.quoteCount || 0, }; } break; default: break; } return { images, video, quote, }; } function buildHtml({ images, links, text, video, stats, quote }) { const videoHtml = video?.thumbnail && video?.playlist ? `` : ""; const imagesHtml = images .map(({ alt, fullsize }) => `${alt}`) .join("
") || ""; const linksHtml = links?.length > 0 ? `` : ""; const quoteHtml = quote?.text ? `
${quote.displayName} (${ quote.handle })
${buildHtml(quote)}
` : ""; const statsHtml = ``; return `

${text}

${quoteHtml}${videoHtml}${imagesHtml}${linksHtml}${statsHtml}`; } function processFeed(feed) { const cleanFeed = filter(feed); return cleanFeed.map(({ post }) => { const text = post.record.text; const name = post.author.displayName || post.author.handle; const url = `https://bsky.app/profile/${post.author.handle}/post/${post.uri .split("/") .at(-1)}`; const { links, tags } = parseFacets(post.record.facets); const { images, video, quote } = parseEmbed(post.embed); const stats = { replyCount: post.replyCount, repostCount: post.repostCount, likeCount: post.likeCount, quoteCount: post.quoteCount, }; const content_html = buildHtml({ images, links, quote, stats, text, video, }); return { author: { name, url: `https://bsky.app/profile/${post.author.handle}`, avatar: post.author.avatar, }, date_published: post.indexedAt, external_url: links?.[0], id: url, image: images?.[0]?.fullsize, summary: post.record.text, tags, title: name, url, content_html, }; }); } export function buildJsonFeed(handle, feed) { const items = processFeed(feed.data.feed); return { version: "https://jsonfeed.org/version/1.1", title: `${handle}'s Timeline`, description: "A personalized Bluesky feed", home_page_url: `https://bsky.app/profile/${handle}`, items, }; }