import AnimationTitles from '../../src/data/animation-titles.json';
import ActorDisplayNames from '../../src/data/actor-display-names.json';
import { fromUrlSafeBase64 } from '../../src/core/base64.js';
interface Env {
ASSETS: R2Bucket;
API_URL: string;
}
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function formatDateTime(timestamp: number): string {
if (!timestamp) return '';
const d = new Date(timestamp * 1000);
const date = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
const time = d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
return `${date} \u00b7 ${time}`;
}
interface Participant {
n?: string;
displayName?: string;
c?: number;
charIndex?: number;
}
function buildDescription(participants: Participant[], timestamp?: number): string {
const parts: string[] = [];
for (const p of participants) {
const name = p.displayName || p.n || 'Unknown';
const charIndex = p.charIndex ?? p.c ?? 0;
const charName = (ActorDisplayNames as string[])[charIndex] || `#${charIndex}`;
parts.push(`${name} as ${charName}`);
}
if (timestamp) {
parts.push(formatDateTime(timestamp));
}
return parts.join(' \u00b7 ');
}
function injectOgTags(html: string, title: string, description: string, url: string): string {
const safeTitle = escapeHtml(title);
const safeDesc = escapeHtml(description);
const safeUrl = escapeHtml(url);
const fullTitle = `${safeTitle} — LEGO® Island`;
html = html.replace(/
[^<]*<\/title>/, `${fullTitle}`);
html = html.replace(
/( {
const key = `${getR2Prefix(request)}index.html`;
const obj = await env.ASSETS.get(key);
if (!obj) throw new Error(`${key} not found in R2`);
return obj.text();
}
async function handleMemory(eventId: string, request: Request, env: Env): Promise {
const html = await getIndexHtml(request, env);
try {
const apiRes = await fetch(`${env.API_URL}/api/memory/${encodeURIComponent(eventId)}`);
if (!apiRes.ok) {
return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8' } });
}
const data = await apiRes.json() as {
animIndex: number;
participants: Participant[];
completedAt?: number;
};
const titles = AnimationTitles as Record;
const title = titles[String(data.animIndex)] || `Animation #${data.animIndex}`;
const description = buildDescription(data.participants, data.completedAt);
const url = new URL(request.url);
return new Response(injectOgTags(html, title, description, url.href), {
headers: {
'content-type': 'text/html; charset=utf-8',
'cache-control': 'public, max-age=300',
},
});
} catch {
return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8' } });
}
}
async function handleScene(encoded: string, request: Request, env: Env): Promise {
const html = await getIndexHtml(request, env);
try {
const json = fromUrlSafeBase64(encoded);
const data = JSON.parse(json) as {
a: number;
p: Array<{ n: string; c: number }>;
t?: number;
};
const titles = AnimationTitles as Record;
const title = titles[String(data.a)] || `Animation #${data.a}`;
const participants = data.p.map(p => ({ n: p.n, c: p.c }));
const description = buildDescription(participants, data.t);
const url = new URL(request.url);
return new Response(injectOgTags(html, title, description, url.href), {
headers: {
'content-type': 'text/html; charset=utf-8',
'cache-control': 'public, max-age=3600',
},
});
} catch {
return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8' } });
}
}
export default {
async fetch(request: Request, env: Env): Promise {
const url = new URL(request.url);
const path = url.pathname;
const memoryMatch = path.match(/^\/memory\/([A-Za-z0-9_-]+)$/);
if (memoryMatch) {
return handleMemory(memoryMatch[1], request, env);
}
const sceneMatch = path.match(/^\/scene\/([A-Za-z0-9_-]+)$/);
if (sceneMatch) {
return handleScene(sceneMatch[1], request, env);
}
return new Response('Not found', { status: 404 });
},
};