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 += `
');
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 += ``; break; } case 'app.bsky.embed.record#viewNotFound': case 'app.bsky.embed.record#viewBlocked': case 'app.bsky.embed.record#viewDetached': { html += `
`; html += richtextToHtml(record.text, record.facets); 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