···001export default {
2 async fetch(request, env) {
3 // Helper function to generate a color from a string
···14 return color;
15 };
1600000000000000000000000000000000000000000000000000000000000000000000000000000000000017 const url = new URL(request.url);
18 const { pathname, searchParams } = url;
1920 if (!pathname || pathname === "/") {
21- return new Response(`This is Tangled's avatar service. It fetches your pretty avatar from Bluesky and caches it on Cloudflare.
22-You can't use this directly unfortunately since all requests are signed and may only originate from the appview.`);
0023 }
2425 const size = searchParams.get("size");
···68 }
6970 try {
71- const profileResponse = await fetch(
72- `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`,
73- );
74- const profile = await profileResponse.json();
75- const avatar = profile.avatar;
007677- let avatarUrl = profile.avatar;
00000000000000007879 if (!avatarUrl) {
80 // Generate a random color based on the actor string
00000081 const bgColor = stringToColor(actor);
82 const size = resizeToTiny ? 32 : 128;
83 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>`;
···93 return response;
94 }
9596- // Resize if requested
97 let avatarResponse;
98 if (resizeToTiny) {
99 avatarResponse = await fetch(avatarUrl, {
···1+import { IdResolver } from "@atproto/identity";
2+3export default {
4 async fetch(request, env) {
5 // Helper function to generate a color from a string
···16 return color;
17 };
1819+ // Helper function to fetch Tangled profile from PDS
20+ const getTangledAvatarFromPDS = async (actor, resolver) => {
21+ try {
22+ // Resolve the identity
23+ const identity = await resolver.resolve(actor);
24+ if (!identity) {
25+ console.log({
26+ level: "debug",
27+ message: "failed to resolve identity",
28+ actor: actor,
29+ });
30+ return null;
31+ }
32+33+ const did = identity.did;
34+ const pdsEndpoint = identity.pdsUrl;
35+36+ if (!pdsEndpoint) {
37+ console.log({
38+ level: "debug",
39+ message: "no PDS endpoint found",
40+ actor: actor,
41+ did: did,
42+ });
43+ return null;
44+ }
45+46+ console.log({
47+ level: "debug",
48+ message: "fetching Tangled profile from PDS",
49+ actor: actor,
50+ did: did,
51+ pdsEndpoint: pdsEndpoint,
52+ });
53+54+ // Fetch the Tangled profile record from PDS
55+ const profileResponse = await fetch(
56+ `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=org.tangled.actor.profile&rkey=self`,
57+ );
58+59+ if (!profileResponse.ok) {
60+ console.log({
61+ level: "debug",
62+ message: "no Tangled profile found on PDS",
63+ actor: actor,
64+ status: profileResponse.status,
65+ });
66+ return null;
67+ }
68+69+ const profileData = await profileResponse.json();
70+ const avatarCID = profileData?.value?.avatar;
71+72+ if (!avatarCID) {
73+ console.log({
74+ level: "debug",
75+ message: "Tangled profile has no avatar CID",
76+ actor: actor,
77+ });
78+ return null;
79+ }
80+81+ // Construct blob URL
82+ const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${avatarCID}`;
83+84+ console.log({
85+ level: "debug",
86+ message: "found Tangled avatar",
87+ actor: actor,
88+ avatarCID: avatarCID,
89+ });
90+91+ return blobUrl;
92+ } catch (e) {
93+ console.log({
94+ level: "warn",
95+ message: "error fetching Tangled avatar from PDS",
96+ actor: actor,
97+ error: e.message,
98+ });
99+ return null;
100+ }
101+ };
102+103 const url = new URL(request.url);
104 const { pathname, searchParams } = url;
105106 if (!pathname || pathname === "/") {
107+ return new Response(
108+ `This is Tangled's avatar service. It fetches your pretty avatar from your PDS, Bluesky, or generates a placeholder.
109+You can't use this directly unfortunately since all requests are signed and may only originate from the appview.`,
110+ );
111 }
112113 const size = searchParams.get("size");
···156 }
157158 try {
159+ let avatarUrl = null;
160+161+ // Create identity resolver
162+ const resolver = new IdResolver();
163+164+ // Try to get Tangled avatar from user's PDS first
165+ avatarUrl = await getTangledAvatarFromPDS(actor, resolver);
166167+ // If no Tangled avatar, fall back to Bluesky
168+ if (!avatarUrl) {
169+ console.log({
170+ level: "debug",
171+ message: "no Tangled avatar, falling back to Bluesky",
172+ actor: actor,
173+ });
174+175+ const profileResponse = await fetch(
176+ `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`,
177+ );
178+179+ if (profileResponse.ok) {
180+ const profile = await profileResponse.json();
181+ avatarUrl = profile.avatar;
182+ }
183+ }
184185 if (!avatarUrl) {
186 // Generate a random color based on the actor string
187+ console.log({
188+ level: "debug",
189+ message: "no avatar found, generating placeholder",
190+ actor: actor,
191+ });
192+193 const bgColor = stringToColor(actor);
194 const size = resizeToTiny ? 32 : 128;
195 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>`;
···205 return response;
206 }
207208+ // Fetch and optionally resize the avatar
209 let avatarResponse;
210 if (resizeToTiny) {
211 avatarResponse = await fetch(avatarUrl, {