Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289import { LocalActorResolver, CompositeHandleResolver, DohJsonHandleResolver, WellKnownHandleResolver, CompositeDidDocumentResolver, PlcDidDocumentResolver, WebDidDocumentResolver,} from "@atcute/identity-resolver";
// Initialize resolvers for Cloudflare Workersconst handleResolver = new CompositeHandleResolver({ strategy: "race", methods: { dns: new DohJsonHandleResolver({ dohUrl: "https://cloudflare-dns.com/dns-query", }), http: new WellKnownHandleResolver(), },});
const didDocumentResolver = new CompositeDidDocumentResolver({ methods: { plc: new PlcDidDocumentResolver(), web: new WebDidDocumentResolver(), },});
const actorResolver = new LocalActorResolver({ handleResolver, didDocumentResolver,});
export default { async fetch(request, env) { // Helper function to generate a color from a string const stringToColor = (str) => { let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } let color = "#"; for (let i = 0; i < 3; i++) { const value = (hash >> (i * 8)) & 0xff; color += ("00" + value.toString(16)).substr(-2); } return color; };
// Helper function to fetch Tangled profile from PDS const getTangledAvatarFromPDS = async (actor) => { try { // Resolve the identity const identity = await actorResolver.resolve(actor); if (!identity) { console.log({ level: "debug", message: "failed to resolve identity", actor: actor, }); return null; }
const did = identity.did; const pdsEndpoint = identity.pds.replace(/\/$/, ""); // Remove trailing slash
if (!pdsEndpoint) { console.log({ level: "debug", message: "no PDS endpoint found", actor: actor, did: did, }); return null; }
const profileUrl = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=sh.tangled.actor.profile&rkey=self`;
// Fetch the Tangled profile record from PDS const profileResponse = await fetch(profileUrl);
if (!profileResponse.ok) { console.log({ level: "debug", message: "no Tangled profile found on PDS", actor: actor, status: profileResponse.status, }); return null; }
const profileData = await profileResponse.json(); const avatarBlob = profileData?.value?.avatar;
if (!avatarBlob) { console.log({ level: "debug", message: "Tangled profile has no avatar", actor: actor, }); return null; }
// Extract CID from blob reference object // The ref might be an object with $link property or a string let avatarCID; if (typeof avatarBlob.ref === "string") { avatarCID = avatarBlob.ref; } else if (avatarBlob.ref?.$link) { avatarCID = avatarBlob.ref.$link; } else if (typeof avatarBlob === "string") { avatarCID = avatarBlob; }
if (!avatarCID || typeof avatarCID !== "string") { console.log({ level: "warn", message: "could not extract valid CID from avatar blob", actor: actor, avatarBlob: avatarBlob, avatarBlobRef: avatarBlob.ref, }); return null; }
// Construct blob URL (pdsEndpoint already has trailing slash removed) const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${avatarCID}`;
return blobUrl; } catch (e) { console.log({ level: "warn", message: "error fetching Tangled avatar from PDS", actor: actor, error: e.message, }); return null; } };
const url = new URL(request.url); const { pathname, searchParams } = url;
if (!pathname || pathname === "/") { return new Response( `This is Tangled's avatar service. It fetches your pretty avatar from your PDS, Bluesky, or generates a placeholder.You can't use this directly unfortunately since all requests are signed and may only originate from the appview.`, ); }
const size = searchParams.get("size"); const resizeToTiny = size === "tiny"; const format = searchParams.get("format") || "webp"; const validFormats = ["webp", "jpeg", "png"]; const outputFormat = validFormats.includes(format) ? format : "webp"; const contentTypes = { webp: "image/webp", jpeg: "image/jpeg", png: "image/png", };
const cache = caches.default; let cacheKey = request.url; let response = await cache.match(cacheKey); if (response) return response;
const pathParts = pathname.slice(1).split("/"); if (pathParts.length < 2) { return new Response("Bad URL", { status: 400 }); }
const [signatureHex, actor] = pathParts; const actorBytes = new TextEncoder().encode(actor);
const key = await crypto.subtle.importKey( "raw", new TextEncoder().encode(env.AVATAR_SHARED_SECRET), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"], );
const computedSigBuffer = await crypto.subtle.sign("HMAC", key, actorBytes); const computedSig = Array.from(new Uint8Array(computedSigBuffer)) .map((b) => b.toString(16).padStart(2, "0")) .join("");
console.log({ level: "debug", message: "avatar request for: " + actor, computedSignature: computedSig, providedSignature: signatureHex, });
const sigBytes = Uint8Array.from( signatureHex.match(/.{2}/g).map((b) => parseInt(b, 16)), ); const valid = await crypto.subtle.verify("HMAC", key, sigBytes, actorBytes);
if (!valid) { return new Response("Invalid signature", { status: 403 }); }
try { let avatarUrl = null;
// Try to get Tangled avatar from user's PDS first avatarUrl = await getTangledAvatarFromPDS(actor);
// If no Tangled avatar, fall back to Bluesky if (!avatarUrl) { console.log({ level: "debug", message: "no Tangled avatar, falling back to Bluesky", actor: actor, });
const profileResponse = await fetch( `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`, );
if (profileResponse.ok) { const profile = await profileResponse.json(); avatarUrl = profile.avatar; } }
if (!avatarUrl) { // Generate a random color based on the actor string console.log({ level: "debug", message: "no avatar found, generating placeholder", actor: actor, });
const bgColor = stringToColor(actor); const size = resizeToTiny ? 32 : 128; const svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg"><rect width="${size}" height="${size}" fill="${bgColor}"/></svg>`; const svgData = new TextEncoder().encode(svg);
response = new Response(svgData, { headers: { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=43200", }, }); await cache.put(cacheKey, response.clone()); return response; }
// Fetch and optionally resize the avatar let avatarResponse; const cfOptions = outputFormat !== "webp" || resizeToTiny ? { cf: { image: { format: outputFormat, ...(resizeToTiny ? { width: 32, height: 32, fit: "cover" } : {}), }, }, }: {};
avatarResponse = await fetch(avatarUrl, cfOptions);
if (!avatarResponse.ok) { return new Response(`failed to fetch avatar for ${actor}.`, { status: avatarResponse.status, }); }
const avatarData = await avatarResponse.arrayBuffer();
response = new Response(avatarData, { headers: { "Content-Type": contentTypes[outputFormat], "Cache-Control": "public, max-age=43200", }, });
await cache.put(cacheKey, response.clone()); return response; } catch (error) { return new Response(`error fetching avatar: ${error.message}`, { status: 500, }); } },};