your personal website on atproto - mirror blento.app
at fix-500-on-first-login 180 lines 5.1 kB view raw
1import { getBlentoOrBskyProfile, getRecord, listRecords, parseUri } from '$lib/atproto/methods'; 2import type { CacheService, CachedProfile } from '$lib/cache'; 3import type { EventData } from '$lib/cards/social/EventCard'; 4import type { Did } from '@atcute/lexicons'; 5 6export type RsvpStatus = 'going' | 'interested'; 7 8export interface ResolvedRsvp { 9 event: EventData; 10 rkey: string; 11 hostDid: string; 12 hostProfile: CachedProfile | null; 13 status: 'going' | 'interested'; 14 eventUri: string; 15} 16 17/** 18 * Fetch raw RSVP data for an event from Microcosm Constellation backlinks. 19 * Returns a map of DID -> status (going/interested). 20 */ 21export async function fetchEventRsvps(eventUri: string): Promise<Map<string, RsvpStatus>> { 22 const allRecords: Record<string, unknown> = {}; 23 let cursor: string | undefined; 24 25 do { 26 const params: Record<string, unknown> = { 27 subject: eventUri, 28 source: 'community.lexicon.calendar.rsvp:subject.uri' 29 }; 30 if (cursor) params.cursor = cursor; 31 32 const res = await fetch( 33 'https://slingshot.microcosm.blue/xrpc/com.bad-example.proxy.hydrateQueryResponse', 34 { 35 method: 'POST', 36 headers: { 'Content-Type': 'application/json' }, 37 body: JSON.stringify({ 38 atproto_proxy: 'did:web:constellation.microcosm.blue#constellation', 39 hydration_sources: [{ path: 'records[]', shape: 'at-uri-parts' }], 40 params, 41 xrpc: 'blue.microcosm.links.getBacklinks' 42 }) 43 } 44 ); 45 46 if (!res.ok) break; 47 48 const data = await res.json(); 49 for (const [key, value] of Object.entries(data.records ?? {})) { 50 allRecords[key] = value; 51 } 52 cursor = data.output?.cursor || undefined; 53 } while (cursor); 54 55 const rsvpMap = new Map<string, RsvpStatus>(); 56 57 for (const [uri, raw] of Object.entries(allRecords)) { 58 const record = raw as { value?: { status?: string } }; 59 const parts = parseUri(uri); 60 const repo = parts?.repo; 61 if (!repo) continue; 62 63 const status = record.value?.status || ''; 64 if (status.includes('#going')) { 65 rsvpMap.set(repo, 'going'); 66 } else if (status.includes('#interested')) { 67 rsvpMap.set(repo, 'interested'); 68 } 69 } 70 71 return rsvpMap; 72} 73 74/** 75 * Resolve a DID to a profile using cache or getBlentoOrBskyProfile as fallback. 76 */ 77export async function resolveProfile( 78 did: string, 79 cache?: CacheService | null 80): Promise<CachedProfile | null> { 81 if (cache) { 82 const profile = await cache.getProfile(did as Did).catch(() => null); 83 if (profile) return profile; 84 } 85 const p = await getBlentoOrBskyProfile({ did: did as Did }).catch(() => null); 86 if (!p) return null; 87 return { 88 did: p.did as string, 89 handle: p.handle as string, 90 displayName: p.displayName as string | undefined, 91 avatar: p.avatar as string | undefined, 92 hasBlento: p.hasBlento, 93 url: p.url 94 }; 95} 96 97/** 98 * Resolve a DID to a handle using cache or getBlentoOrBskyProfile as fallback. 99 */ 100export async function resolveHandleForDid( 101 did: string, 102 cache?: CacheService | null 103): Promise<string | null> { 104 const profile = await resolveProfile(did, cache); 105 return profile?.handle && profile.handle !== 'handle.invalid' ? profile.handle : null; 106} 107 108/** 109 * Get a profile URL for a user. Uses their Blento URL if they have one, 110 * otherwise falls back to their Bluesky profile. 111 */ 112export function getProfileUrl(did: string, profile?: CachedProfile | null): string { 113 if (profile?.hasBlento) { 114 return profile.url || `https://blento.app/${profile.handle || did}`; 115 } 116 const handle = profile?.handle; 117 return handle ? `https://bsky.app/profile/${handle}` : `https://bsky.app/profile/${did}`; 118} 119 120interface RsvpRecord { 121 $type: string; 122 status: string; 123 subject: { uri: string; cid?: string }; 124 createdAt: string; 125} 126 127/** 128 * Fetch a user's RSVPs (going/interested) and resolve each referenced event + host profile. 129 */ 130export async function fetchUserRsvps( 131 did: string, 132 cache?: CacheService | null 133): Promise<ResolvedRsvp[]> { 134 const rsvpRecords = await listRecords({ 135 did: did as Did, 136 collection: 'community.lexicon.calendar.rsvp', 137 limit: 100 138 }); 139 140 const activeRsvps = rsvpRecords.filter((r) => { 141 const rsvp = r.value as unknown as RsvpRecord; 142 return rsvp.status?.endsWith('#going') || rsvp.status?.endsWith('#interested'); 143 }); 144 145 const results = await Promise.all( 146 activeRsvps.map(async (r) => { 147 const rsvp = r.value as unknown as RsvpRecord; 148 const parsed = parseUri(rsvp.subject.uri); 149 if (!parsed?.rkey || !parsed?.repo) return null; 150 151 try { 152 const [record, hostProfile] = await Promise.all([ 153 getRecord({ 154 did: parsed.repo as Did, 155 collection: 'community.lexicon.calendar.event', 156 rkey: parsed.rkey 157 }), 158 resolveProfile(parsed.repo, cache).catch(() => null) 159 ]); 160 161 if (!record?.value) return null; 162 163 return { 164 event: record.value as EventData, 165 rkey: parsed.rkey, 166 hostDid: parsed.repo, 167 hostProfile, 168 status: (rsvp.status?.endsWith('#going') ? 'going' : 'interested') as 169 | 'going' 170 | 'interested', 171 eventUri: rsvp.subject.uri 172 }; 173 } catch { 174 return null; 175 } 176 }) 177 ); 178 179 return results.filter((r) => r !== null); 180}