GET /xrpc/app.bsky.actor.searchActorsTypeahead typeahead.waow.tech
16
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 5080912ca9c3d7d4301e4dce83a5b804de56d00a 110 lines 3.7 kB view raw
1import { CORS_HEADERS } from "./types"; 2import { shouldHide } 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): string { 17 if (cidOrUrl.startsWith("https://")) return cidOrUrl; 18 return `https://cdn.bsky.app/img/avatar/plain/${did}/${cidOrUrl}`; 19} 20 21export function extractAvatarCid(url: string): string { 22 const match = url.match(/\/([^/]+?)(?:@[a-z]+)?$/); 23 return match?.[1] ?? ''; 24} 25 26/** fetch profile directly from an actor's PDS — fallback for banned/suspended accounts */ 27export async function fetchProfileFromPds(did: string, pds: string): Promise<any | null> { 28 try { 29 const res = await fetch( 30 `${pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=app.bsky.actor.profile&rkey=self` 31 ); 32 if (!res.ok) return null; 33 const data: any = await res.json(); 34 const val = data?.value; 35 if (!val) return null; 36 37 let avatar = ''; 38 const cid = val.avatar?.ref?.$link || val.avatar?.ref?.['$link']; 39 if (cid) { 40 avatar = `${pds}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`; 41 } 42 43 return { 44 did, 45 handle: '', 46 displayName: val.displayName || '', 47 avatar, 48 labels: [], 49 createdAt: val.createdAt || '', 50 associated: {}, 51 }; 52 } catch { 53 return null; 54 } 55} 56 57/** strip zero/false fields from associated object to match bsky's typeahead shape */ 58export function cleanAssociated(assoc: any): Record<string, unknown> { 59 if (!assoc || typeof assoc !== 'object') return {}; 60 const clean: Record<string, unknown> = {}; 61 for (const [k, v] of Object.entries(assoc)) { 62 if (v === 0 || v === false || v === null || v === undefined) continue; 63 clean[k] = v; 64 } 65 return clean; 66} 67 68/** extract the fields we store from a bsky profile response (getProfiles/getProfile/typeahead) */ 69export interface ProfileFields { 70 handle: string; 71 displayName: string; 72 avatarCid: string; 73 labels: string; 74 hidden: number; 75 createdAt: string; 76 associated: string; 77} 78 79export function extractProfileFields(profile: any, override?: 'show' | 'hide' | null): ProfileFields { 80 let hidden = shouldHide(profile.labels) ? 1 : 0; 81 if (override === 'show') hidden = 0; 82 if (override === 'hide') hidden = 1; 83 const raw = profile.avatar || ''; 84 // PDS blob URLs are already full URLs — store as-is; CDN URLs get CID extracted 85 const avatarCid = raw.startsWith('https://') && !raw.includes('cdn.bsky.app') 86 ? raw 87 : extractAvatarCid(raw); 88 return { 89 handle: profile.handle || '', 90 displayName: profile.displayName || '', 91 avatarCid, 92 labels: JSON.stringify(profile.labels || []), 93 hidden, 94 createdAt: profile.createdAt || '', 95 associated: JSON.stringify(cleanAssociated(profile.associated)), 96 }; 97} 98 99/** strip anything that could break FTS5 syntax, preserving unicode letters/digits */ 100export function sanitize(q: string): string { 101 return q.replace(/[^\p{L}\p{N}\s.-]/gu, "").trim(); 102} 103 104export function html(body: string, extra?: Record<string, string> | number, status = 200): Response { 105 if (typeof extra === "number") { status = extra; extra = undefined; } 106 return new Response(body, { 107 status, 108 headers: { "Content-Type": "text/html; charset=utf-8", ...CORS_HEADERS, ...extra }, 109 }); 110}