@astro-community/astro-embed-bluesky
Version:
Component to embed a fully-styled Bluesky post with no client-side JavaScript in your Astro site
160 lines (143 loc) • 3.98 kB
text/typescript
import {
AppBskyEmbedExternal,
AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyEmbedVideo,
AppBskyFeedDefs,
AtpAgent,
RichText,
} from '@atproto/api';
import type { Post } from './types';
const escapeMap: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
export const escapeHTML = (str?: string) =>
str?.replace(/[&<>"']/g, (match) => escapeMap[match] || match) ?? '';
export function renderPostAsHtml(post?: AppBskyFeedDefs.PostView | Post) {
if (!post) {
return '';
}
const rt = new RichText(post.record as any);
let html = '';
for (const segment of rt.segments()) {
if (segment.isLink()) {
html += `<a href="${escapeHTML(segment.link?.uri)}">${escapeHTML(
segment.text
)}</a>`;
} else if (segment.isMention()) {
html += `<a href="https://bsky.app/profile/${escapeHTML(
segment.mention?.did
)}">${escapeHTML(segment.text)}</a>`;
} else if (segment.isTag()) {
html += `<a href="https://bsky.app/hashtag/${escapeHTML(
segment.tag?.tag
)}">#${escapeHTML(segment.tag?.tag)}</a>`;
} else {
html += escapeHTML(segment.text);
}
}
return html;
}
export 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 (
AppBskyEmbedImages.isView(embed) ||
AppBskyEmbedExternal.isView(embed) ||
AppBskyEmbedVideo.isView(embed)
) {
return embed;
} else if (
AppBskyEmbedRecordWithMedia.isView(embed) &&
(AppBskyEmbedImages.isView(embed.media) ||
AppBskyEmbedExternal.isView(embed.media) ||
AppBskyEmbedVideo.isView(embed.media))
) {
return embed.media;
}
}
return undefined;
}
const agent = new AtpAgent({
service: 'https://public.api.bsky.app',
});
export async function resolvePost(
postUrl: string | Post | AppBskyFeedDefs.PostView
): Promise<Post | undefined> {
let atUri;
if (typeof postUrl === 'object') {
return postUrl as Post;
}
if (postUrl.startsWith('at:')) {
atUri = postUrl;
} 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 agent.resolveHandle({ handle: did });
if (!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 agent.getPosts({ uris: [atUri] });
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}`;
}