UNPKG

@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
import { AppBskyEmbedExternal, AppBskyEmbedImages, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, AppBskyEmbedVideo, AppBskyFeedDefs, AtpAgent, RichText, } from '@atproto/api'; import type { Post } from './types'; const escapeMap: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', }; 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}`; }