atmo.rsvp
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix profile bug, fix og images

Florian 700ad357 2893666e

+26 -57
+1 -2
src/app.d.ts
··· 21 21 CLIENT_ASSERTION_KEY: string; 22 22 COOKIE_SECRET: string; 23 23 OAUTH_PUBLIC_URL: string; 24 - PROFILE_CACHE?: KVNamespace; 25 - DB: D1Database; 24 + DB: D1Database; 26 25 CRON_SECRET: string; 27 26 }; 28 27 }
+4 -34
src/lib/atproto/server/profile.ts
··· 1 1 import type { Did } from '@atcute/lexicons'; 2 2 import { getProfileFromContrail, getProfileBlobUrl, getServerClient } from '$lib/contrail'; 3 3 4 - const PROFILE_CACHE_TTL = 60 * 60; // 1 hour 5 - 6 - /** 7 - * Loads a user's profile, with optional KV caching. 8 - * Falls back to a fresh fetch if the cache KV doesn't exist or on cache miss. 9 - * Returns undefined if the profile can't be loaded. 10 - */ 11 - export async function loadProfile(did: Did, db: D1Database, profileCache?: KVNamespace) { 12 - // Try cache first 13 - if (profileCache) { 14 - try { 15 - const cached = await profileCache.get(did, 'json'); 16 - if (cached) return cached as Record<string, unknown>; 17 - } catch { 18 - // Cache read failed, continue to fresh fetch 19 - } 20 - } 21 - 22 - const profile = await fetchProfile(did, db); 23 - 24 - // Write to cache (fire-and-forget) 25 - if (profileCache && profile) { 26 - profileCache 27 - .put(did, JSON.stringify(profile), { expirationTtl: PROFILE_CACHE_TTL }) 28 - .catch(() => {}); 29 - } 30 - 31 - return profile; 32 - } 33 - 34 - async function fetchProfile(did: Did, db: D1Database) { 4 + export async function loadProfile(did: Did, db: D1Database) { 35 5 try { 36 6 const client = getServerClient(db); 37 7 const p = await getProfileFromContrail(client, did); 38 8 39 - if (!p || p.handle === 'handle.invalid') { 40 - return { did, handle: 'handle.invalid' }; 9 + if (!p) { 10 + return { did, handle: did }; 41 11 } 42 12 43 13 return { 44 14 did: p.did, 45 - handle: p.handle ?? 'handle.invalid', 15 + handle: p.handle && p.handle !== 'handle.invalid' ? p.handle : did, 46 16 displayName: p.record?.displayName, 47 17 avatar: p.record?.avatar ? getProfileBlobUrl(p.did, p.record.avatar) : undefined 48 18 };
+2 -5
src/routes/(app)/p/+page.server.ts
··· 7 7 redirect(303, '/login'); 8 8 } 9 9 10 - const profile = await loadProfile(locals.did, platform?.env?.PROFILE_CACHE); 11 - const actor = 12 - typeof profile?.handle === 'string' && profile.handle !== 'handle.invalid' 13 - ? profile.handle 14 - : locals.did; 10 + const profile = await loadProfile(locals.did, platform!.env.DB); 11 + const actor = profile?.handle ?? locals.did; 15 12 16 13 redirect(303, `/p/${actor}`); 17 14 };
+18 -11
src/routes/(app)/p/[actor]/e/[rkey]/og.png/+server.ts
··· 3 3 import { error } from '@sveltejs/kit'; 4 4 import EventOgImage from './EventOgImage.svelte'; 5 5 import { getActor } from '$lib/actor'; 6 - import { flattenEventRecord, getEventRecordFromContrail } from '$lib/contrail'; 6 + import { flattenEventRecord, getEventRecordFromContrail, getServerClient } from '$lib/contrail'; 7 + import { render } from 'svelte/server'; 7 8 8 9 function formatDate(dateStr: string): string { 9 10 const date = new Date(dateStr); ··· 13 14 return `${weekday}, ${month} ${day}`; 14 15 } 15 16 16 - export async function GET({ params, platform, request }) { 17 + export async function GET({ params, platform }) { 17 18 const { rkey } = params; 18 19 19 20 const did = await getActor(params.actor); ··· 25 26 let eventData; 26 27 27 28 try { 28 - const eventRecord = await getEventRecordFromContrail({ did, rkey }); 29 + const client = getServerClient(platform!.env.DB); 30 + const eventRecord = await getEventRecordFromContrail(client, { did, rkey }); 29 31 eventData = eventRecord ? flattenEventRecord(eventRecord) : null; 30 32 } catch (e) { 31 33 if (e && typeof e === 'object' && 'status' in e) throw e; ··· 47 49 thumbnailUrl = getCDNImageBlobUrl({ did, blob: media.content, format: 'png' }) ?? null; 48 50 } 49 51 } 52 + const { body } = render(EventOgImage, { 53 + props: { name: eventData.name, dateStr, thumbnailUrl, rkey } 54 + }); 55 + // Decode HTML entities that Svelte SSR escapes, since satori-html doesn't decode them 56 + const decoded = body 57 + .replace(/&amp;/g, '&') 58 + .replace(/&lt;/g, '<') 59 + .replace(/&gt;/g, '>') 60 + .replace(/&quot;/g, '"') 61 + .replace(/&#39;/g, "'"); 62 + 50 63 return new ImageResponse( 51 - EventOgImage, 52 - { width: 1200, height: 630, debug: false, format: 'png' }, 53 - { 54 - name: eventData.name, 55 - dateStr, 56 - thumbnailUrl, 57 - rkey 58 - } 64 + decoded, 65 + { width: 1200, height: 630, debug: false, format: 'png' } 59 66 ); 60 67 }
+1 -1
src/routes/+layout.server.ts
··· 6 6 return { did: null, profile: null }; 7 7 } 8 8 9 - const profile = await loadProfile(locals.did, platform!.env.DB, platform?.env?.PROFILE_CACHE); 9 + const profile = await loadProfile(locals.did, platform!.env.DB); 10 10 11 11 return { 12 12 did: locals.did,
-4
wrangler.jsonc
··· 33 33 { 34 34 "binding": "OAUTH_STATES", 35 35 "id": "46cfb5c0bb8c41378e757b46afd7dd35" 36 - }, 37 - { 38 - "binding": "PROFILE_CACHE", 39 - "id": "f0dd7b5baa3e498c9d762bce1599a280" 40 36 } 41 37 ] 42 38 }