import { unwrapEmbed, type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyRichtextFacet, } from '@atcute/bluesky'; import { segmentize } from '@atcute/bluesky-richtext-segmenter'; import { PUBLIC_APP_URL } from '$env/static/public'; import { findLabel, FlagsBlurMedia } from './moderation'; import { assertCanonicalResourceUri } from './types/at-uri'; import { getQuoteEmbed } from './utils/bluesky/embeds'; import { assertNever } from './utils/invariant'; import type { UnwrapArray } from './utils/types'; export const escapeContent = (str: string) => { return str.replace(/[&<]/g, (c) => `&#${c.charCodeAt(0)};`); }; export const escapeAttribute = (str: string) => { return str.replace(/[&"]/g, (c) => `&#${c.charCodeAt(0)};`); }; export interface FeedItem { id?: string; url: string; title?: string; description?: string | { html: string }; images?: Array<{ src: string; thumbnailSrc?: string; adult?: boolean; alt?: string; }>; video?: { playerUrl: string; thumbnailSrc?: string; adult?: boolean; alt?: string; }; date?: Date; } export interface FeedOptions { meta: { title: string; description: string; pageUrl: string; rssUrl?: string; image?: { src: string; }; }; items: FeedItem[]; } export const createRssFeed = (options: FeedOptions) => { const { meta: { title, description, pageUrl, rssUrl, image }, items, } = options; let rss = ``; rss += ``; rss += ``; { rss += `${escapeContent(title)}`; rss += `${escapeContent(pageUrl)}`; rss += `${escapeContent(escapeContent(description))}`; if (rssUrl !== undefined) { rss += ``; } if (image !== undefined) { rss += ``; rss += `${escapeContent(image.src)}`; rss += `${escapeContent(title)}`; rss += `${escapeContent(pageUrl)}`; rss += ``; } } for (const { id, url, title, description, images, video, date } of items) { rss += ``; if (id !== undefined) { rss += `${escapeContent(id)}`; } rss += `${escapeContent(url)}`; if (date !== undefined) { rss += `${date.toUTCString()}`; } if (title !== undefined) { rss += `${escapeContent(title)}`; } if (description !== undefined) { const value = typeof description === 'string' ? escapeContent(description) : description.html; rss += `${escapeContent(value)}`; } if (images !== undefined) { for (const { src: url, adult, thumbnailSrc: thumbnail, alt } of images) { rss += ``; rss += `${adult ? 'adult' : 'nonadult'}`; if (alt !== undefined) { rss += `${escapeContent(alt)}`; } if (thumbnail !== undefined) { rss += ``; } rss += ``; } } if (video !== undefined) { const { playerUrl, thumbnailSrc, adult, alt } = video; rss += ``; rss += ``; rss += `${adult ? 'adult' : 'nonadult'}`; if (thumbnailSrc !== undefined) { rss += ``; } if (alt !== undefined) { rss += `${escapeContent(alt)}`; } rss += ``; } rss += ``; } rss += ``; rss += ``; return rss; }; export const richtextToHtml = (text: string, facets: AppBskyRichtextFacet.Main[] | undefined) => { let html = ''; for (const segment of segmentize(text, facets)) { const feature = grabFirstSupported(segment.features); const subtext = escapeContent(segment.text).replace(/\n/g, '
'); switch (feature?.$type) { case undefined: { html += subtext; break; } case 'app.bsky.richtext.facet#link': { html += `${subtext}`; break; } case 'app.bsky.richtext.facet#mention': { const href = `${PUBLIC_APP_URL}/${feature.did}`; html += `${subtext}`; break; } case 'app.bsky.richtext.facet#tag': { const href = `${PUBLIC_APP_URL}/search/posts?q=${encodeURIComponent('#' + feature.tag)}`; html += `${subtext}`; break; } default: { assertNever(feature); } } } return html; }; export const feedPostToFeedItem = (item: AppBskyFeedDefs.FeedViewPost): FeedItem => { const post = item.post; const author = post.author; const record = post.record as AppBskyFeedPost.Main; const { media, record: recordEmbed } = unwrapEmbed(post.embed); const quote = getQuoteEmbed(recordEmbed); const shouldBlurMedia = !!findLabel(post.labels, author.did, FlagsBlurMedia); let html = richtextToHtml(record.text, record.facets); if (quote) { html += `

`; switch (quote.$type) { case 'app.bsky.embed.record#viewRecord': { const author = quote.author; const record = quote.value as AppBskyFeedPost.Main; const uri = assertCanonicalResourceUri(quote.uri); const postUrl = `${PUBLIC_APP_URL}/${author.did}/${uri.rkey}`; html += `
`; html += ``; if (author.displayName?.trim()) { html += `${escapeContent(author.displayName.trim())} (@${escapeContent(author.handle)})`; } else { html += `@${escapeContent(author.handle)}`; } html += `
`; html += richtextToHtml(record.text, record.facets); html += `
`; break; } case 'app.bsky.embed.record#viewNotFound': case 'app.bsky.embed.record#viewBlocked': case 'app.bsky.embed.record#viewDetached': { html += `
Post not found
`; break; } } } return { id: `${post.uri}|${post.cid}`, url: `${PUBLIC_APP_URL}/${author.did}/${assertCanonicalResourceUri(post.uri).rkey}`, date: new Date(post.indexedAt), description: { html }, images: media?.$type === 'app.bsky.embed.images#view' ? media.images.map((image) => ({ src: image.fullsize, thumbnailSrc: image.thumb, adult: shouldBlurMedia, alt: image.alt, })) : undefined, video: media?.$type === 'app.bsky.embed.video#view' ? { playerUrl: getVideoUrl(media.playlist), thumbnailSrc: media.thumbnail, adult: shouldBlurMedia, alt: media.alt, } : undefined, }; }; const getVideoUrl = (playlistUrl: string) => { const MATCH_RE = /\/(did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])\/(bafkrei[2-7a-z]{52})\//; const match = MATCH_RE.exec(decodeURIComponent(playlistUrl)); if (!match) { return ''; } return `${PUBLIC_APP_URL}/watch/${match[1]}/${match[2]}`; }; type FacetFeature = UnwrapArray; const grabFirstSupported = (features: FacetFeature[] | undefined): FacetFeature | undefined => { return features?.find( (feature) => feature.$type === 'app.bsky.richtext.facet#link' || feature.$type === 'app.bsky.richtext.facet#mention' || feature.$type === 'app.bsky.richtext.facet#tag', ); };