import {renderCachedEmbed} from './embed-cache.ts' export type ProfileView = { avatar?: string banner?: string description?: string did: string displayName?: string handle: string } // based on https://github.com/Janpot/escape-html-template-tag/blob/master/src/index.ts const ENTITIES: { [key: string]: string } = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/', '`': '`', '=': '=', } const ENT_REGEX = new RegExp(Object.keys(ENTITIES).join('|'), 'g') function escapehtml(unsafe: Sub): string { if (Array.isArray(unsafe)) { return unsafe.map(escapehtml).join('') } if (unsafe instanceof HtmlSafeString) { return unsafe.toString() } return String(unsafe).replace(ENT_REGEX, char => ENTITIES[char]) } type Sub = HtmlSafeString | string | (HtmlSafeString | string)[] export class HtmlSafeString { private _parts: readonly string[] private _subs: readonly Sub[] constructor(parts: readonly string[], subs: readonly Sub[]) { this._parts = parts this._subs = subs } toString(): string { let result = this._parts[0] for (let i = 1; i < this._parts.length; i++) { result += escapehtml(this._subs[i - 1]) + this._parts[i] } return result } } export function html(parts: TemplateStringsArray, ...subs: Sub[]) { return new HtmlSafeString(parts, subs) } export const renderHandleString = (profile: ProfileView) => profile.displayName ? `${profile.displayName} (@${profile.handle})` : `@${profile.handle}` class HeadHandler { profile: ProfileView url: string constructor(profile: ProfileView, url: string) { this.profile = profile this.url = url } async element(element) { const view = this.profile const description = view.description ? html` ` : '' const img = view.banner ? html` ` : view.avatar ? html`` : '' element.append( html` ${description} ${img} `, {html: true}, ) } } class TitleHandler { profile: ProfileView constructor(profile: ProfileView) { this.profile = profile } async element(element) { element.setInnerContent(renderHandleString(this.profile)) } } class NoscriptHandler { profile: ProfileView constructor(profile: ProfileView) { this.profile = profile } async element(element) { const view = this.profile element.append( html`

Profile

${view.displayName ?? ''}

${view.handle}

${view.did}

${view.description ?? ''}

`, {html: true}, ) } } export async function handleProfileEmbed(context) { const {request, env} = context return renderCachedEmbed({ request, executionContext: context, async render(publicUrl) { const origin = new URL(publicUrl).origin const base = env.ASSETS.fetch(new URL('/', origin)) try { const apiUrl = new URL( 'https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile', ) apiUrl.searchParams.set('actor', context.params.handleOrDID) const apiResponse = await fetch(apiUrl, { headers: {Accept: 'application/json'}, }) if (!apiResponse.ok) { throw new Error(`getProfile returned ${apiResponse.status}`) } const profile = (await apiResponse.json()) as ProfileView return new HTMLRewriter() .on(`head`, new HeadHandler(profile, publicUrl)) .on(`title`, new TitleHandler(profile)) .on(`noscript`, new NoscriptHandler(profile)) .transform(await base) } catch (error) { console.error(error) const baseResponse = await base const response = new Response(baseResponse.body, baseResponse) response.headers.set('Cache-Control', 'no-store') return response } }, }) }