@astro-community/astro-embed-bluesky
Version:
Component to embed a fully-styled Bluesky post with no client-side JavaScript in your Astro site
171 lines (154 loc) • 4.71 kB
text/typescript
import type {} from '@atcute/atproto';
import {
type AppBskyFeedDefs,
type AppBskyEmbedRecord,
AppBskyEmbedImages,
AppBskyEmbedExternal,
AppBskyEmbedVideo,
AppBskyEmbedRecordWithMedia,
AppBskyFeedPost,
} from '@atcute/bluesky';
import { segmentize } from '@atcute/bluesky-richtext-segmenter';
import { Client, simpleFetchHandler } from '@atcute/client';
import type { Post } from './types';
import { is } from '@atcute/lexicons';
const escapeMap: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
const escapeHTML = (str?: string) =>
str?.replace(/[&<>"']/g, (match) => escapeMap[match] || match) ?? '';
export function renderPostAsHtml(post?: AppBskyFeedDefs.PostView | Post) {
if (!post || !is(AppBskyFeedPost.mainSchema, post.record)) {
return '';
}
const segments = segmentize(post.record.text, post.record.facets);
let html = '';
for (const { text, features } of segments) {
const [feature] = features || [];
switch (feature?.$type) {
case 'app.bsky.richtext.facet#link':
html += `<a href="${escapeHTML(feature.uri)}">${escapeHTML(text)}</a>`;
break;
case 'app.bsky.richtext.facet#mention':
html += `<a href="https://bsky.app/profile/${escapeHTML(feature.did)}">${escapeHTML(text)}</a>`;
break;
case 'app.bsky.richtext.facet#tag':
html += `<a href="https://bsky.app/hashtag/${escapeHTML(feature.tag)}">#${escapeHTML(feature.tag)}</a>`;
break;
default:
html += escapeHTML(text);
break;
}
}
return html;
}
function viewRecordToPostView(
viewRecord: AppBskyEmbedRecord.ViewRecord
): AppBskyFeedDefs.PostView {
const { value, embeds, ...rest } = viewRecord;
return {
...rest,
$type: 'app.bsky.feed.defs#postView',
record: value,
embed: embeds?.[0],
} as AppBskyFeedDefs.PostView;
}
export function viewRecordToEmbed(
viewRecord: AppBskyEmbedRecord.ViewRecord,
allowNestedQuotes = false
) {
const { embed } = viewRecordToPostView(viewRecord);
if (allowNestedQuotes) {
return embed;
} else {
if (
is(AppBskyEmbedImages.viewSchema, embed) ||
is(AppBskyEmbedExternal.viewSchema, embed) ||
is(AppBskyEmbedVideo.viewSchema, embed)
) {
return embed;
} else if (
is(AppBskyEmbedRecordWithMedia.viewSchema, embed) &&
(is(AppBskyEmbedImages.viewSchema, embed.media) ||
is(AppBskyEmbedExternal.viewSchema, embed.media) ||
is(AppBskyEmbedVideo.viewSchema, embed.media))
) {
return embed.media;
}
}
return undefined;
}
const client = new Client({
handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }),
});
export async function resolvePost(
postUrl: string | Post | AppBskyFeedDefs.PostView
): Promise<Post | undefined> {
let atUri: `at://${string}.${string}`;
if (typeof postUrl === 'object') {
return postUrl as Post;
}
if (postUrl.startsWith('at:')) {
atUri = postUrl as `at://${string}.${string}`;
} else {
if (!postUrl.startsWith('https://bsky.app/')) {
return undefined;
}
const urlParts = new URL(postUrl).pathname.split('/');
let did = urlParts[2];
const postId = urlParts[4];
if (!did || !postId) {
return undefined;
}
if (!did.startsWith('did:')) {
try {
const handleResolution = await client.get(
'com.atproto.identity.resolveHandle',
{ params: { handle: did as `${string}.${string}` } }
);
if (!handleResolution.ok || !handleResolution.data.did) {
return undefined;
}
did = handleResolution.data.did;
} catch (e: any) {
console.error(
`[error] astro-embed` + '\n ' + (e?.message ?? e)
);
return undefined;
}
}
atUri = `at://${did}/app.bsky.feed.post/${postId}`;
}
try {
const hydratedPost = await client.get('app.bsky.feed.getPosts', {
params: { uris: [atUri] },
});
if (!hydratedPost.ok) {
throw new Error(hydratedPost.data.message || 'Failed to fetch post');
}
return hydratedPost.data.posts[0] as unknown as Post;
} catch (e: any) {
console.error(`[error] astro-embed` + '\n ' + (e?.message ?? e));
return undefined;
}
}
export function atUriToPostUri(atUri: string) {
const [, , did, , postId] = atUri.split('/');
return `https://bsky.app/profile/${did}/post/${postId}`;
}
export function atUriToStarterPackUri(atUri: string) {
const [, , did, , packId] = atUri.split('/');
return `https://bsky.app/starter-pack/${did}/${packId}`;
}
export function atUriToListUri(atUri: string) {
const [, , did, , listId] = atUri.split('/');
return `https://bsky.app/profile/${did}/lists/${listId}`;
}
export function starterPackOgImage(uri: string) {
const [, , did, , packId] = uri.split('/');
return `https://ogcard.cdn.bsky.app/start/${did}/${packId}`;
}