···
2
-
async fetch(request, env) {
3
-
const url = new URL(request.url);
4
-
const { pathname } = url;
6
-
if (!pathname || pathname === '/') {
7
-
return new Response(`This is Tangled's avatar service. It fetches your pretty avatar from Bluesky and caches it on Cloudflare.
8
-
You can't use this directly unforunately since all requests are signed and may only originate from the appview.`);
11
-
const cache = caches.default;
13
-
let cacheKey = request.url;
14
-
let response = await cache.match(cacheKey);
19
-
const pathParts = pathname.slice(1).split('/');
20
-
if (pathParts.length < 2) {
21
-
return new Response('Bad URL', { status: 400 });
24
-
const [signatureHex, actor] = pathParts;
26
-
const actorBytes = new TextEncoder().encode(actor);
28
-
const key = await crypto.subtle.importKey(
30
-
new TextEncoder().encode(env.AVATAR_SHARED_SECRET),
31
-
{ name: 'HMAC', hash: 'SHA-256' },
36
-
const computedSigBuffer = await crypto.subtle.sign('HMAC', key, actorBytes);
37
-
const computedSig = Array.from(new Uint8Array(computedSigBuffer))
38
-
.map((b) => b.toString(16).padStart(2, '0'))
43
-
message: 'avatar request for: ' + actor,
44
-
computedSignature: computedSig,
45
-
providedSignature: signatureHex,
48
-
const sigBytes = Uint8Array.from(signatureHex.match(/.{2}/g).map((b) => parseInt(b, 16)));
49
-
const valid = await crypto.subtle.verify('HMAC', key, sigBytes, actorBytes);
52
-
return new Response('Invalid signature', { status: 403 });
56
-
const profileResponse = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`, { method: 'GET' });
57
-
const profile = await profileResponse.json();
58
-
const avatar = profile.avatar;
61
-
return new Response(`avatar not found for ${actor}.`, { status: 404 });
64
-
// fetch the actual avatar image
65
-
const avatarResponse = await fetch(avatar);
66
-
if (!avatarResponse.ok) {
67
-
return new Response(`failed to fetch avatar for ${actor}.`, { status: avatarResponse.status });
70
-
const avatarData = await avatarResponse.arrayBuffer();
71
-
const contentType = avatarResponse.headers.get('content-type') || 'image/jpeg';
73
-
response = new Response(avatarData, {
75
-
'Content-Type': contentType,
76
-
'Cache-Control': 'public, max-age=43200', // 12 h
80
-
// cache it in cf using request.url as the key
81
-
await cache.put(cacheKey, response.clone());
85
-
return new Response(`error fetching avatar: ${error.message}`, { status: 500 });
2
+
async fetch(request, env) {
3
+
const url = new URL(request.url);
4
+
const { pathname, searchParams } = url;
6
+
if (!pathname || pathname === "/") {
7
+
return new Response(`This is Tangled's avatar service. It fetches your pretty avatar from Bluesky and caches it on Cloudflare.
8
+
You can't use this directly unfortunately since all requests are signed and may only originate from the appview.`);
11
+
const size = searchParams.get("size");
12
+
const resizeToTiny = size === "tiny";
14
+
const cache = caches.default;
15
+
let cacheKey = request.url;
16
+
let response = await cache.match(cacheKey);
17
+
if (response) return response;
19
+
const pathParts = pathname.slice(1).split("/");
20
+
if (pathParts.length < 2) {
21
+
return new Response("Bad URL", { status: 400 });
24
+
const [signatureHex, actor] = pathParts;
25
+
const actorBytes = new TextEncoder().encode(actor);
27
+
const key = await crypto.subtle.importKey(
29
+
new TextEncoder().encode(env.AVATAR_SHARED_SECRET),
30
+
{ name: "HMAC", hash: "SHA-256" },
35
+
const computedSigBuffer = await crypto.subtle.sign("HMAC", key, actorBytes);
36
+
const computedSig = Array.from(new Uint8Array(computedSigBuffer))
37
+
.map((b) => b.toString(16).padStart(2, "0"))
42
+
message: "avatar request for: " + actor,
43
+
computedSignature: computedSig,
44
+
providedSignature: signatureHex,
47
+
const sigBytes = Uint8Array.from(
48
+
signatureHex.match(/.{2}/g).map((b) => parseInt(b, 16)),
50
+
const valid = await crypto.subtle.verify("HMAC", key, sigBytes, actorBytes);
53
+
return new Response("Invalid signature", { status: 403 });
57
+
const profileResponse = await fetch(
58
+
`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`,
60
+
const profile = await profileResponse.json();
61
+
const avatar = profile.avatar;
64
+
return new Response(`avatar not found for ${actor}.`, { status: 404 });
67
+
// Resize if requested
70
+
avatarResponse = await fetch(avatar, {
81
+
avatarResponse = await fetch(avatar);
84
+
if (!avatarResponse.ok) {
85
+
return new Response(`failed to fetch avatar for ${actor}.`, {
86
+
status: avatarResponse.status,
90
+
const avatarData = await avatarResponse.arrayBuffer();
92
+
avatarResponse.headers.get("content-type") || "image/jpeg";
94
+
response = new Response(avatarData, {
96
+
"Content-Type": contentType,
97
+
"Cache-Control": "public, max-age=43200",
101
+
await cache.put(cacheKey, response.clone());
104
+
return new Response(`error fetching avatar: ${error.message}`, {