import { getPdsUrlForDid, resolvePdsForDids, setPdsForDid } from './pdsResolver'; import { getImageProxySettings } from '../settings/storage'; let activeProxyUrl: string | null = null; export function setActiveProxyUrl(url: string | null): void { activeProxyUrl = url; } export function getActiveProxyUrl(): string | null { return activeProxyUrl; } const CDN_IMG_RE = /^https:\/\/cdn\.bsky\.app\/img\/[^/]+\/plain\/(did:[^/]+)\/([a-zA-Z0-9]+)$/; const CDN_VIDEO_THUMB_RE = /^https:\/\/video\.cdn\.bsky\.app\/hls\/(did:[^/]+)\/([a-zA-Z0-9]+)\/thumbnail\.jpg$/; const CDN_VIDEO_WATCH_THUMB_RE = /^https:\/\/video\.bsky\.app\/watch\/(did:[^/]+)\/([a-zA-Z0-9]+)\/thumbnail\.jpg$/; export function parseCdnUrl(url: string): { did: string; cid: string } | null { let decoded: string; try { decoded = decodeURIComponent(url); } catch { decoded = url; } let m = CDN_IMG_RE.exec(decoded); if (m) return { did: m[1], cid: m[2] }; m = CDN_VIDEO_THUMB_RE.exec(decoded); if (m) return { did: m[1], cid: m[2] }; m = CDN_VIDEO_WATCH_THUMB_RE.exec(decoded); if (m) return { did: m[1], cid: m[2] }; return null; } let fallbackPdsUrl: string | null = null; export function setBlobRewritePdsUrl(pdsUrl: string): void { fallbackPdsUrl = pdsUrl.replace(/\/$/, ''); } export function getBlobRewritePdsUrl(): string | null { return fallbackPdsUrl; } export function initBlobRewrite(userDid: string, pdsUrl: string): void { const normalized = pdsUrl.replace(/\/$/, ''); fallbackPdsUrl = normalized; setPdsForDid(userDid, normalized); } function collectDidsFromObject(data: unknown, seen = new Set()): Set { if (typeof data === 'string') { const parsed = parseCdnUrl(data); if (parsed) seen.add(parsed.did); return seen; } if (!data || typeof data !== 'object') return seen; if (Array.isArray(data)) { for (const item of data) collectDidsFromObject(item, seen); return seen; } for (const val of Object.values(data as Record)) { collectDidsFromObject(val, seen); } return seen; } export function rewriteBlobUrl(url: string | undefined | null): string | undefined { if (!url) return undefined; const parsed = parseCdnUrl(url); if (!parsed) return url; const settings = getImageProxySettings(); if (settings.mode !== 'disabled' && activeProxyUrl) { if (activeProxyUrl === 'cdn.bsky.app' || activeProxyUrl === 'https://cdn.bsky.app') { return url; } const isThumb = url.includes('/plain/') || url.includes('/thumbnail.jpg'); if (settings.mode === 'all' || (settings.mode === 'thumbnails' && isThumb)) { const original = new URL(url); return `${activeProxyUrl.replace(/\/$/, '')}${original.pathname}`; } } const pds = getPdsUrlForDid(parsed.did); if (!pds) return url; return `${pds}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(parsed.did)}&cid=${encodeURIComponent(parsed.cid)}`; } export async function rewriteBlobUrls(data: T): Promise { if (!data || typeof data !== 'object') return data; const dids = collectDidsFromObject(data); if (dids.size > 0) { await resolvePdsForDids([...dids]).catch(() => {}); } if (typeof data === 'string') { return (rewriteBlobUrl(data) ?? data) as T; } if (Array.isArray(data)) { for (let i = 0; i < data.length; i++) { rewriteBlobUrlsInPlace(data, i); } return data; } const obj = data as Record; for (const key of Object.keys(obj)) { rewriteBlobUrlsInPlace(obj, key); } return data; } function rewriteBlobUrlsInPlace(container: object, key: string | number): void { const obj = container as Record; const val = obj[key]; if (typeof val === 'string') { const rewritten = rewriteBlobUrl(val); if (rewritten !== val) { obj[key] = rewritten; } } else if (val && typeof val === 'object') { rewriteBlobUrls(val); } }