GET /xrpc/app.bsky.actor.searchActorsTypeahead typeahead.waow.tech
at main 117 lines 4.2 kB view raw
1import { CORS_HEADERS } from "./types"; 2import { shouldHide, hasExplicitSlur } from "./moderation"; 3 4export function clientIP(request: Request): string { 5 return request.headers.get("CF-Connecting-IP") || "unknown"; 6} 7 8export function escHtml(s: string): string { 9 return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); 10} 11 12export function json(data: unknown, status = 200): Response { 13 return Response.json(data, { status, headers: CORS_HEADERS }); 14} 15 16export function avatarUrl(did: string, cidOrUrl: string, pds?: string): string { 17 if (cidOrUrl.startsWith("https://")) return cidOrUrl; 18 if (pds && !pds.includes('.bsky.network')) { 19 return `${pds}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cidOrUrl)}`; 20 } 21 return `https://cdn.bsky.app/img/avatar/plain/${did}/${cidOrUrl}`; 22} 23 24export function extractAvatarCid(urlOrCid: string): string { 25 if (!urlOrCid) return ''; 26 // bare CID (from PDS fallback) — no slashes 27 if (!urlOrCid.includes('/')) return urlOrCid; 28 // PDS getBlob URL — extract cid param 29 const cidParam = urlOrCid.match(/[?&]cid=([^&]+)/); 30 if (cidParam) return decodeURIComponent(cidParam[1]); 31 // bsky CDN URL — extract last path segment 32 const match = urlOrCid.match(/\/([^/]+?)(?:@[a-z]+)?$/); 33 return match?.[1] ?? ''; 34} 35 36/** fetch profile directly from an actor's PDS — tries bsky profile first, then tangled. 37 * returns bare CID for avatar (not full URL) — caller reconstructs at query time via pds column. */ 38export async function fetchProfileFromPds(did: string, pds: string): Promise<any | null> { 39 const collections = ['app.bsky.actor.profile', 'sh.tangled.actor.profile']; 40 for (const collection of collections) { 41 try { 42 const res = await fetch( 43 `${pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=${collection}&rkey=self` 44 ); 45 if (!res.ok) continue; 46 const data: any = await res.json(); 47 const val = data?.value; 48 if (!val) continue; 49 50 return { 51 did, 52 handle: '', 53 displayName: val.displayName || '', 54 avatar: val.avatar?.ref?.$link || val.avatar?.ref?.['$link'] || '', 55 labels: [], 56 createdAt: val.createdAt || '', 57 associated: {}, 58 }; 59 } catch { 60 continue; 61 } 62 } 63 return null; 64} 65 66/** strip zero/false fields from associated object to match bsky's typeahead shape */ 67export function cleanAssociated(assoc: any): Record<string, unknown> { 68 if (!assoc || typeof assoc !== 'object') return {}; 69 const clean: Record<string, unknown> = {}; 70 for (const [k, v] of Object.entries(assoc)) { 71 if (v === 0 || v === false || v === null || v === undefined) continue; 72 clean[k] = v; 73 } 74 return clean; 75} 76 77/** extract the fields we store from a bsky profile response (getProfiles/getProfile/typeahead) */ 78export interface ProfileFields { 79 handle: string; 80 displayName: string; 81 avatarCid: string; 82 labels: string; 83 hidden: number; 84 createdAt: string; 85 associated: string; 86} 87 88export function extractProfileFields(profile: any, override?: 'show' | 'hide' | null): ProfileFields { 89 let hidden = shouldHide(profile.labels) ? 1 : 0; 90 if (hasExplicitSlur(profile.handle || '')) hidden = 1; 91 if (override === 'show') hidden = 0; 92 if (override === 'hide') hidden = 1; 93 const raw = profile.avatar || ''; 94 const avatarCid = extractAvatarCid(raw); 95 return { 96 handle: profile.handle || '', 97 displayName: profile.displayName || '', 98 avatarCid, 99 labels: JSON.stringify(profile.labels || []), 100 hidden, 101 createdAt: profile.createdAt || '', 102 associated: JSON.stringify(cleanAssociated(profile.associated)), 103 }; 104} 105 106/** strip anything that could break FTS5 syntax, preserving unicode letters/digits */ 107export function sanitize(q: string): string { 108 return q.replace(/[^\p{L}\p{N}\s.-]/gu, "").trim(); 109} 110 111export function html(body: string, extra?: Record<string, string> | number, status = 200): Response { 112 if (typeof extra === "number") { status = extra; extra = undefined; } 113 return new Response(body, { 114 status, 115 headers: { "Content-Type": "text/html; charset=utf-8", ...CORS_HEADERS, ...extra }, 116 }); 117}