From d68501adb9b1f815f14963391605b585ffb6c5a2 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:25:31 +0100 Subject: [PATCH] more event stuff --- src/lib/cache.ts | 2 + src/lib/events/fetch-attendees.ts | 74 ++++++- .../[[actor=actor]]/events/+page.server.ts | 19 +- .../events/[rkey]/+page.svelte | 125 ++++++----- .../events/[rkey]/EventAttendees.svelte | 2 +- .../events/[rkey]/EventRsvp.svelte | 12 ++ .../events/[rkey]/edit/+page.svelte | 16 +- .../events/api/refresh/+server.ts | 47 ++++ .../events/calendar/+server.ts | 2 +- .../events/rsvp-calendar/+server.ts | 135 ------------ .../[[actor=actor]]/rsvp/+layout.server.ts | 28 +++ .../[[actor=actor]]/rsvp/+layout.svelte | 9 + .../[[actor=actor]]/rsvp/+page.server.ts | 48 +++++ src/routes/[[actor=actor]]/rsvp/+page.svelte | 200 ++++++++++++++++++ .../rsvp/api/refresh/+server.ts | 37 ++++ .../[[actor=actor]]/rsvp/calendar/+server.ts | 106 ++++++++++ 16 files changed, 648 insertions(+), 214 deletions(-) create mode 100644 src/routes/[[actor=actor]]/events/api/refresh/+server.ts delete mode 100644 src/routes/[[actor=actor]]/events/rsvp-calendar/+server.ts create mode 100644 src/routes/[[actor=actor]]/rsvp/+layout.server.ts create mode 100644 src/routes/[[actor=actor]]/rsvp/+layout.svelte create mode 100644 src/routes/[[actor=actor]]/rsvp/+page.server.ts create mode 100644 src/routes/[[actor=actor]]/rsvp/+page.svelte create mode 100644 src/routes/[[actor=actor]]/rsvp/api/refresh/+server.ts create mode 100644 src/routes/[[actor=actor]]/rsvp/calendar/+server.ts diff --git a/src/lib/cache.ts b/src/lib/cache.ts index d0e0981..02b0f21 100644 --- a/src/lib/cache.ts +++ b/src/lib/cache.ts @@ -13,6 +13,8 @@ const NAMESPACE_TTL = { npmx: 60 * 60 * 12, // 12 hours profile: 60 * 60 * 24, // 24 hours ical: 60 * 60 * 2, // 2 hours + events: 60 * 60, // 1 hour + rsvps: 60 * 60, // 1 hour meta: 0 // no auto-expiry } as const; diff --git a/src/lib/events/fetch-attendees.ts b/src/lib/events/fetch-attendees.ts index 495ba3b..7be55de 100644 --- a/src/lib/events/fetch-attendees.ts +++ b/src/lib/events/fetch-attendees.ts @@ -1,9 +1,19 @@ -import { getBlentoOrBskyProfile, parseUri } from '$lib/atproto/methods'; +import { getBlentoOrBskyProfile, getRecord, listRecords, parseUri } from '$lib/atproto/methods'; import type { CacheService, CachedProfile } from '$lib/cache'; +import type { EventData } from '$lib/cards/social/EventCard'; import type { Did } from '@atcute/lexicons'; export type RsvpStatus = 'going' | 'interested'; +export interface ResolvedRsvp { + event: EventData; + rkey: string; + hostDid: string; + hostProfile: CachedProfile | null; + status: 'going' | 'interested'; + eventUri: string; +} + /** * Fetch raw RSVP data for an event from Microcosm Constellation backlinks. * Returns a map of DID -> status (going/interested). @@ -106,3 +116,65 @@ export function getProfileUrl(did: string, profile?: CachedProfile | null): stri const handle = profile?.handle; return handle ? `https://bsky.app/profile/${handle}` : `https://bsky.app/profile/${did}`; } + +interface RsvpRecord { + $type: string; + status: string; + subject: { uri: string; cid?: string }; + createdAt: string; +} + +/** + * Fetch a user's RSVPs (going/interested) and resolve each referenced event + host profile. + */ +export async function fetchUserRsvps( + did: string, + cache?: CacheService | null +): Promise { + const rsvpRecords = await listRecords({ + did: did as Did, + collection: 'community.lexicon.calendar.rsvp', + limit: 100 + }); + + const activeRsvps = rsvpRecords.filter((r) => { + const rsvp = r.value as unknown as RsvpRecord; + return rsvp.status?.endsWith('#going') || rsvp.status?.endsWith('#interested'); + }); + + const results = await Promise.all( + activeRsvps.map(async (r) => { + const rsvp = r.value as unknown as RsvpRecord; + const parsed = parseUri(rsvp.subject.uri); + if (!parsed?.rkey || !parsed?.repo) return null; + + try { + const [record, hostProfile] = await Promise.all([ + getRecord({ + did: parsed.repo as Did, + collection: 'community.lexicon.calendar.event', + rkey: parsed.rkey + }), + resolveProfile(parsed.repo, cache).catch(() => null) + ]); + + if (!record?.value) return null; + + return { + event: record.value as EventData, + rkey: parsed.rkey, + hostDid: parsed.repo, + hostProfile, + status: (rsvp.status?.endsWith('#going') ? 'going' : 'interested') as + | 'going' + | 'interested', + eventUri: rsvp.subject.uri + }; + } catch { + return null; + } + }) + ); + + return results.filter((r) => r !== null); +} diff --git a/src/routes/[[actor=actor]]/events/+page.server.ts b/src/routes/[[actor=actor]]/events/+page.server.ts index 976626e..9475139 100644 --- a/src/routes/[[actor=actor]]/events/+page.server.ts +++ b/src/routes/[[actor=actor]]/events/+page.server.ts @@ -15,6 +15,16 @@ export async function load({ params, platform, request }) { } try { + // Try cache first + if (cache) { + const cached = await cache.getJSON<{ + events: (EventData & { rkey: string })[]; + did: string; + hostProfile: CachedProfile | null; + }>('events', did); + if (cached) return cached; + } + const [records, hostProfile] = await Promise.all([ listRecords({ did: did as Did, @@ -42,11 +52,18 @@ export async function load({ params, platform, request }) { rkey: r.uri.split('/').pop() as string })); - return { + const result = { events, did, hostProfile: hostProfile ?? null }; + + // Cache the result + if (cache) { + await cache.putJSON('events', did, result).catch(() => {}); + } + + return result; } catch (e) { if (e && typeof e === 'object' && 'status' in e) throw e; throw error(404, 'Events not found'); diff --git a/src/routes/[[actor=actor]]/events/[rkey]/+page.svelte b/src/routes/[[actor=actor]]/events/[rkey]/+page.svelte index ddc9d0c..1d9ae3f 100644 --- a/src/routes/[[actor=actor]]/events/[rkey]/+page.svelte +++ b/src/routes/[[actor=actor]]/events/[rkey]/+page.svelte @@ -217,7 +217,7 @@
{#if !isBannerOnly} @@ -245,7 +245,7 @@ {/if} -
+

{eventData.name} @@ -337,76 +337,77 @@ > About

-

+

{@html descriptionHtml}

{/if}
- - - - {#if eventData.uris && eventData.uris.length > 0} - -
+ +
+ +

- Links + Hosted By

-
- {#each eventData.uris as link (link.name + link.uri)} - - + + + {hostProfile?.displayName || hostProfile?.handle || did} + + +
+ + {#if eventData.uris && eventData.uris.length > 0} + +
+

+ Links +

+
+ {#each eventData.uris as link (link.name + link.uri)} + - - - {link.name || link.uri.replace(/^https?:\/\//, '')} - - {/each} + + + + {link.name || link.uri.replace(/^https?:\/\//, '')} + + {/each} +
-
- {/if} + {/if} - -
+ -
- -
+
diff --git a/src/routes/[[actor=actor]]/events/[rkey]/EventAttendees.svelte b/src/routes/[[actor=actor]]/events/[rkey]/EventAttendees.svelte index bfebcfc..eff7d8b 100644 --- a/src/routes/[[actor=actor]]/events/[rkey]/EventAttendees.svelte +++ b/src/routes/[[actor=actor]]/events/[rkey]/EventAttendees.svelte @@ -166,7 +166,7 @@
{/if} - e.preventDefault()} class="p-0"> + e.preventDefault()} class="p-0">

{modalTitle} diff --git a/src/routes/[[actor=actor]]/events/[rkey]/EventRsvp.svelte b/src/routes/[[actor=actor]]/events/[rkey]/EventRsvp.svelte index 37be43a..06c19ee 100644 --- a/src/routes/[[actor=actor]]/events/[rkey]/EventRsvp.svelte +++ b/src/routes/[[actor=actor]]/events/[rkey]/EventRsvp.svelte @@ -94,6 +94,7 @@ rsvpStatus = status; rsvpRkey = key; onrsvp?.(status); + refreshRsvpCache(); } } catch (e) { console.error('Failed to submit RSVP:', e); @@ -113,12 +114,23 @@ rsvpStatus = null; rsvpRkey = null; oncancel?.(); + refreshRsvpCache(); } catch (e) { console.error('Failed to cancel RSVP:', e); } finally { rsvpSubmitting = false; } } + + function refreshRsvpCache() { + const handle = + user.profile?.handle && user.profile.handle !== 'handle.invalid' + ? user.profile.handle + : user.did; + if (handle) { + fetch(`/${handle}/rsvp/api/refresh`).catch(() => {}); + } + }

{}); goto(`/${handle}/events/${rkey}`); } else { error = `Failed to ${isNew ? 'create' : 'save'} event. Please try again.`; @@ -592,6 +593,7 @@ user.profile?.handle && user.profile.handle !== 'handle.invalid' ? user.profile.handle : user.did; + fetch(`/${handle}/events/api/refresh`).catch(() => {}); goto(`/${handle}/events`); } catch (e) { console.error('Failed to delete event:', e); @@ -1165,22 +1167,12 @@ > Cancel -
{:else} - + {/if}
{/if} diff --git a/src/routes/[[actor=actor]]/events/api/refresh/+server.ts b/src/routes/[[actor=actor]]/events/api/refresh/+server.ts new file mode 100644 index 0000000..d571444 --- /dev/null +++ b/src/routes/[[actor=actor]]/events/api/refresh/+server.ts @@ -0,0 +1,47 @@ +import { createCache } from '$lib/cache'; +import { error, json } from '@sveltejs/kit'; +import { getActor } from '$lib/actor'; +import { listRecords } from '$lib/atproto/methods.js'; +import type { EventData } from '$lib/cards/social/EventCard'; +import type { Did } from '@atcute/lexicons'; + +export async function GET({ params, platform, request }) { + const cache = createCache(platform); + if (!cache) return json('no cache'); + + const did = await getActor({ request, paramActor: params.actor, platform, blockBoth: false }); + + if (!did) { + throw error(404, 'Not found'); + } + + // Delete stale caches + await Promise.all([cache.delete('events', did), cache.delete('ical', `${did}:calendar`)]).catch( + () => {} + ); + + // Re-fetch and cache + const [records, hostProfile] = await Promise.all([ + listRecords({ + did: did as Did, + collection: 'community.lexicon.calendar.event', + limit: 100 + }), + cache.getProfile(did as Did).catch(() => null) + ]); + + const events = records.map((r) => ({ + ...(r.value as EventData), + rkey: r.uri.split('/').pop() as string + })); + + const result = { + events, + did, + hostProfile: hostProfile ?? null + }; + + await cache.putJSON('events', did, result).catch(() => {}); + + return json(result); +} diff --git a/src/routes/[[actor=actor]]/events/calendar/+server.ts b/src/routes/[[actor=actor]]/events/calendar/+server.ts index 0d59814..75d732a 100644 --- a/src/routes/[[actor=actor]]/events/calendar/+server.ts +++ b/src/routes/[[actor=actor]]/events/calendar/+server.ts @@ -4,7 +4,7 @@ import { getCDNImageBlobUrl, listRecords } from '$lib/atproto/methods.js'; import { createCache } from '$lib/cache'; import type { Did } from '@atcute/lexicons'; import { getActor } from '$lib/actor'; -import { generateICalFeed, type ICalEvent } from '$lib/ical'; +import { generateICalFeed, type ICalAttendee, type ICalEvent } from '$lib/ical'; import { fetchEventRsvps, getProfileUrl, resolveProfile } from '$lib/events/fetch-attendees'; export async function GET({ params, platform, request }) { diff --git a/src/routes/[[actor=actor]]/events/rsvp-calendar/+server.ts b/src/routes/[[actor=actor]]/events/rsvp-calendar/+server.ts deleted file mode 100644 index 000a85e..0000000 --- a/src/routes/[[actor=actor]]/events/rsvp-calendar/+server.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { error } from '@sveltejs/kit'; -import type { EventData } from '$lib/cards/social/EventCard'; -import { getCDNImageBlobUrl, getRecord, listRecords, parseUri } from '$lib/atproto/methods.js'; -import { createCache } from '$lib/cache'; -import type { Did } from '@atcute/lexicons'; -import { getActor } from '$lib/actor'; -import { generateICalFeed, type ICalAttendee, type ICalEvent } from '$lib/ical'; -import { fetchEventRsvps, getProfileUrl, resolveProfile } from '$lib/events/fetch-attendees'; - -interface RsvpRecord { - $type: string; - status: string; - subject: { uri: string; cid?: string }; - createdAt: string; -} - -export async function GET({ params, platform, request }) { - const cache = createCache(platform); - - const did = await getActor({ request, paramActor: params.actor, platform }); - - if (!did) { - throw error(404, 'Not found'); - } - - try { - // Check cache first - const cacheKey = `${did}:rsvp-calendar`; - if (cache) { - const cached = await cache.get('ical', cacheKey); - if (cached) { - return new Response(cached, { - headers: { - 'Content-Type': 'text/calendar; charset=utf-8', - 'Cache-Control': 'public, max-age=3600' - } - }); - } - } - - const [rsvpRecords, hostProfile] = await Promise.all([ - listRecords({ - did: did as Did, - collection: 'community.lexicon.calendar.rsvp', - limit: 100 - }), - resolveProfile(did, cache) - ]); - - // Filter to only going and interested RSVPs - const activeRsvps = rsvpRecords.filter((r) => { - const rsvp = r.value as unknown as RsvpRecord; - return rsvp.status?.endsWith('#going') || rsvp.status?.endsWith('#interested'); - }); - - // Fetch each referenced event in parallel - const eventResults = await Promise.all( - activeRsvps.map(async (r) => { - const rsvp = r.value as unknown as RsvpRecord; - const parsed = parseUri(rsvp.subject.uri); - if (!parsed?.rkey || !parsed?.repo) return null; - - try { - const [record, organizerProfile] = await Promise.all([ - getRecord({ - did: parsed.repo as Did, - collection: 'community.lexicon.calendar.event', - rkey: parsed.rkey - }), - resolveProfile(parsed.repo, cache).catch(() => null) - ]); - if (!record?.value) return null; - const eventData = record.value as EventData; - const actor = organizerProfile?.handle || parsed.repo; - const thumbnail = eventData.media?.find((m) => m.role === 'thumbnail'); - const imageUrl = thumbnail?.content - ? getCDNImageBlobUrl({ - did: parsed.repo, - blob: thumbnail.content, - type: 'jpeg' - }) - : undefined; - - // Fetch RSVPs and resolve handles - const rsvpMap = await fetchEventRsvps(rsvp.subject.uri).catch( - () => new Map() - ); - const attendees: ICalAttendee[] = []; - await Promise.all( - Array.from(rsvpMap.entries()).map(async ([attendeeDid, status]) => { - const profile = await resolveProfile(attendeeDid, cache).catch(() => null); - attendees.push({ - name: profile?.handle || attendeeDid, - status, - url: getProfileUrl(attendeeDid, profile) - }); - }) - ); - - return { - eventData, - uid: rsvp.subject.uri, - url: `https://blento.app/${actor}/events/${parsed.rkey}`, - organizer: actor, - imageUrl, - attendees - } satisfies ICalEvent; - } catch { - return null; - } - }) - ); - - const events: ICalEvent[] = eventResults.filter((e) => e !== null); - - const actor = hostProfile?.handle || did; - const calendarName = `${hostProfile?.displayName || actor}'s RSVP Events`; - const ical = generateICalFeed(events, calendarName); - - // Store in cache - if (cache) { - await cache.put('ical', cacheKey, ical).catch(() => {}); - } - - return new Response(ical, { - headers: { - 'Content-Type': 'text/calendar; charset=utf-8', - 'Cache-Control': 'public, max-age=3600' - } - }); - } catch (e) { - if (e && typeof e === 'object' && 'status' in e) throw e; - throw error(500, 'Failed to generate calendar'); - } -} diff --git a/src/routes/[[actor=actor]]/rsvp/+layout.server.ts b/src/routes/[[actor=actor]]/rsvp/+layout.server.ts new file mode 100644 index 0000000..5a88b2b --- /dev/null +++ b/src/routes/[[actor=actor]]/rsvp/+layout.server.ts @@ -0,0 +1,28 @@ +import { getRecord } from '$lib/atproto/methods.js'; +import type { Did } from '@atcute/lexicons'; +import { getActor } from '$lib/actor.js'; + +export async function load({ params, platform, request }) { + const did = await getActor({ request, paramActor: params.actor, platform }); + + if (!did) return { accentColor: undefined, baseColor: undefined }; + + try { + const publication = await getRecord({ + did: did as Did, + collection: 'site.standard.publication', + rkey: 'blento.self' + }); + + const preferences = publication?.value?.preferences as + | { accentColor?: string; baseColor?: string } + | undefined; + + return { + accentColor: preferences?.accentColor, + baseColor: preferences?.baseColor + }; + } catch { + return { accentColor: undefined, baseColor: undefined }; + } +} diff --git a/src/routes/[[actor=actor]]/rsvp/+layout.svelte b/src/routes/[[actor=actor]]/rsvp/+layout.svelte new file mode 100644 index 0000000..ea1c6fc --- /dev/null +++ b/src/routes/[[actor=actor]]/rsvp/+layout.svelte @@ -0,0 +1,9 @@ + + + + +{@render children()} diff --git a/src/routes/[[actor=actor]]/rsvp/+page.server.ts b/src/routes/[[actor=actor]]/rsvp/+page.server.ts new file mode 100644 index 0000000..9dc0160 --- /dev/null +++ b/src/routes/[[actor=actor]]/rsvp/+page.server.ts @@ -0,0 +1,48 @@ +import { error } from '@sveltejs/kit'; +import { createCache } from '$lib/cache'; +import type { CachedProfile } from '$lib/cache'; +import { getActor } from '$lib/actor.js'; +import { fetchUserRsvps, resolveProfile, type ResolvedRsvp } from '$lib/events/fetch-attendees'; + +export async function load({ params, platform, request }) { + const cache = createCache(platform); + + const did = await getActor({ request, paramActor: params.actor, platform }); + + if (!did) { + throw error(404, 'RSVPs not found'); + } + + try { + // Try cache first + if (cache) { + const cached = await cache.getJSON<{ + rsvps: ResolvedRsvp[]; + did: string; + userProfile: CachedProfile | null; + }>('rsvps', did); + if (cached) return cached; + } + + const [rsvps, userProfile] = await Promise.all([ + fetchUserRsvps(did, cache), + resolveProfile(did, cache) + ]); + + const result = { + rsvps, + did, + userProfile: userProfile ?? null + }; + + // Cache the result + if (cache) { + await cache.putJSON('rsvps', did, result).catch(() => {}); + } + + return result; + } catch (e) { + if (e && typeof e === 'object' && 'status' in e) throw e; + throw error(404, 'RSVPs not found'); + } +} diff --git a/src/routes/[[actor=actor]]/rsvp/+page.svelte b/src/routes/[[actor=actor]]/rsvp/+page.svelte new file mode 100644 index 0000000..85f352c --- /dev/null +++ b/src/routes/[[actor=actor]]/rsvp/+page.svelte @@ -0,0 +1,200 @@ + + + + {userName} - RSVPs + + + + + + + + +
+
+ +
+
+

+ {showPast ? 'Past' : 'Upcoming'} RSVPs +

+
+ + {userName} +
+
+ +
+ + +
+ + +
+ + {#if filteredRsvps.length === 0} +

+ No {showPast ? 'past' : 'upcoming'} RSVPs. +

+ {:else} +
+ {#each filteredRsvps as rsvp (rsvp.eventUri)} + {@const thumbnail = getThumbnail(rsvp.event, rsvp.hostDid)} + {@const hostHandle = rsvp.hostProfile?.handle || rsvp.hostDid} + + +
+ {#if thumbnail} + {thumbnail.alt} + {:else} +
+ +
+ {/if} +
+ + +
+

+ {rsvp.event.name} +

+ +

+ {formatDate(rsvp.event.startsAt)} · {formatTime(rsvp.event.startsAt)} +

+ +
+ {#if rsvp.event.mode} + {getModeLabel(rsvp.event.mode)} + {/if} + + {getStatusLabel(rsvp.status)} +
+
+
+ {/each} +
+ {/if} +
+
diff --git a/src/routes/[[actor=actor]]/rsvp/api/refresh/+server.ts b/src/routes/[[actor=actor]]/rsvp/api/refresh/+server.ts new file mode 100644 index 0000000..b0d32b9 --- /dev/null +++ b/src/routes/[[actor=actor]]/rsvp/api/refresh/+server.ts @@ -0,0 +1,37 @@ +import { createCache } from '$lib/cache'; +import { error, json } from '@sveltejs/kit'; +import { getActor } from '$lib/actor'; +import { fetchUserRsvps, resolveProfile } from '$lib/events/fetch-attendees'; + +export async function GET({ params, platform, request }) { + const cache = createCache(platform); + if (!cache) return json('no cache'); + + const did = await getActor({ request, paramActor: params.actor, platform, blockBoth: false }); + + if (!did) { + throw error(404, 'Not found'); + } + + // Delete stale caches + await Promise.all([ + cache.delete('rsvps', did), + cache.delete('ical', `${did}:rsvp-calendar`) + ]).catch(() => {}); + + // Re-fetch and cache + const [rsvps, userProfile] = await Promise.all([ + fetchUserRsvps(did, cache), + resolveProfile(did, cache) + ]); + + const result = { + rsvps, + did, + userProfile: userProfile ?? null + }; + + await cache.putJSON('rsvps', did, result).catch(() => {}); + + return json(result); +} diff --git a/src/routes/[[actor=actor]]/rsvp/calendar/+server.ts b/src/routes/[[actor=actor]]/rsvp/calendar/+server.ts new file mode 100644 index 0000000..c0f038e --- /dev/null +++ b/src/routes/[[actor=actor]]/rsvp/calendar/+server.ts @@ -0,0 +1,106 @@ +import { error } from '@sveltejs/kit'; +import { getCDNImageBlobUrl } from '$lib/atproto/methods.js'; +import { createCache } from '$lib/cache'; +import { getActor } from '$lib/actor'; +import { generateICalFeed, type ICalAttendee, type ICalEvent } from '$lib/ical'; +import { + fetchEventRsvps, + fetchUserRsvps, + getProfileUrl, + resolveProfile +} from '$lib/events/fetch-attendees'; + +export async function GET({ params, platform, request }) { + const cache = createCache(platform); + + const did = await getActor({ request, paramActor: params.actor, platform }); + + if (!did) { + throw error(404, 'Not found'); + } + + try { + // Check cache first + const cacheKey = `${did}:rsvp-calendar`; + if (cache) { + const cached = await cache.get('ical', cacheKey); + if (cached) { + return new Response(cached, { + headers: { + 'Content-Type': 'text/calendar; charset=utf-8', + 'Cache-Control': 'public, max-age=3600' + } + }); + } + } + + const [rsvps, userProfile] = await Promise.all([ + fetchUserRsvps(did, cache), + resolveProfile(did, cache) + ]); + + // Enrich each RSVP with attendees and image URLs for the iCal feed + const events: ICalEvent[] = ( + await Promise.all( + rsvps.map(async (rsvp) => { + try { + const actor = rsvp.hostProfile?.handle || rsvp.hostDid; + const thumbnail = rsvp.event.media?.find((m) => m.role === 'thumbnail'); + const imageUrl = thumbnail?.content + ? getCDNImageBlobUrl({ + did: rsvp.hostDid, + blob: thumbnail.content, + type: 'jpeg' + }) + : undefined; + + const rsvpMap = await fetchEventRsvps(rsvp.eventUri).catch( + () => new Map() + ); + const attendees: ICalAttendee[] = []; + await Promise.all( + Array.from(rsvpMap.entries()).map(async ([attendeeDid, status]) => { + const profile = await resolveProfile(attendeeDid, cache).catch(() => null); + attendees.push({ + name: profile?.handle || attendeeDid, + status, + url: getProfileUrl(attendeeDid, profile) + }); + }) + ); + + return { + eventData: rsvp.event, + uid: rsvp.eventUri, + url: `https://blento.app/${actor}/events/${rsvp.rkey}`, + organizer: actor, + imageUrl, + attendees + } satisfies ICalEvent; + } catch { + return null; + } + }) + ) + ).filter((e) => e !== null); + + const actor = userProfile?.handle || did; + const calendarName = `${userProfile?.displayName || actor}'s RSVP Events`; + const ical = generateICalFeed(events, calendarName); + + // Store in cache + if (cache) { + await cache.put('ical', cacheKey, ical).catch(() => {}); + } + + return new Response(ical, { + headers: { + 'Content-Type': 'text/calendar; charset=utf-8', + 'Cache-Control': 'public, max-age=3600' + } + }); + } catch (e) { + if (e && typeof e === 'object' && 'status' in e) throw e; + throw error(500, 'Failed to generate calendar'); + } +} -- 2.51.2