your personal website on atproto - mirror
blento.app
1import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto';
2import { CardDefinitionsByType } from '$lib/cards';
3import type { CacheService } from '$lib/cache';
4import { createEmptyCard } from '$lib/helper';
5import type { Item, WebsiteData } from '$lib/types';
6import { error } from '@sveltejs/kit';
7import type { ActorIdentifier, Did } from '@atcute/lexicons';
8
9import { isDid, isHandle } from '@atcute/lexicons/syntax';
10import { fixAllCollisions, compactItems } from '$lib/layout';
11
12const CURRENT_CACHE_VERSION = 1;
13
14export async function getCache(identifier: ActorIdentifier, page: string, cache?: CacheService) {
15 try {
16 const cachedResult = await cache?.getBlento(identifier);
17
18 if (!cachedResult) return;
19 const result = JSON.parse(cachedResult);
20
21 if (!result.version || result.version !== CURRENT_CACHE_VERSION) {
22 console.log('skipping cache because of version mismatch');
23 return;
24 }
25
26 result.page = 'blento.' + page;
27
28 result.publication = (result.publications as Awaited<ReturnType<typeof listRecords>>).find(
29 (v) => parseUri(v.uri)?.rkey === result.page
30 )?.value;
31 result.publication ??= {
32 name: result.profile?.displayName || result.profile?.handle,
33 description: result.profile?.description
34 };
35
36 delete result['publications'];
37
38 return checkData(result);
39 } catch (error) {
40 console.log('getting cached result failed', error);
41 }
42}
43
44export async function loadData(
45 handle: ActorIdentifier,
46 cache: CacheService | undefined,
47 forceUpdate: boolean = false,
48 page: string = 'self',
49 env?: Record<string, string | undefined>
50): Promise<WebsiteData> {
51 if (!handle) throw error(404);
52 if (handle === 'favicon.ico') throw error(404);
53
54 if (!forceUpdate) {
55 const cachedResult = await getCache(handle, page, cache);
56
57 if (cachedResult) return cachedResult;
58 }
59
60 let did: Did | undefined = undefined;
61 if (isHandle(handle)) {
62 did = await resolveHandle({ handle });
63 } else if (isDid(handle)) {
64 did = handle;
65 } else {
66 throw error(404);
67 }
68
69 const [cards, mainPublication, pages, profile] = await Promise.all([
70 listRecords({ did, collection: 'app.blento.card', limit: 0 }).catch((e) => {
71 console.error('error getting records for collection app.blento.card', e);
72 return [] as Awaited<ReturnType<typeof listRecords>>;
73 }),
74 getRecord({
75 did,
76 collection: 'site.standard.publication',
77 rkey: 'blento.self'
78 }).catch(() => {
79 console.error('error getting record for collection site.standard.publication');
80 return undefined;
81 }),
82 listRecords({ did, collection: 'app.blento.page' }).catch(() => {
83 console.error('error getting records for collection app.blento.page');
84 return [] as Awaited<ReturnType<typeof listRecords>>;
85 }),
86 getDetailedProfile({ did })
87 ]);
88
89 const additionalData = await loadAdditionalData(
90 cards.map((v) => ({ ...v.value })) as Item[],
91 { did, handle, cache },
92 env
93 );
94
95 const result = {
96 page: 'blento.' + page,
97 handle,
98 did,
99 cards: (cards.map((v) => {
100 return { ...v.value };
101 }) ?? []) as Item[],
102 publications: [mainPublication, ...pages].filter((v) => v),
103 additionalData,
104 profile,
105 updatedAt: Date.now(),
106 version: CURRENT_CACHE_VERSION
107 };
108
109 // Only cache results that have cards to avoid caching PDS errors
110 if (result.cards.length > 0) {
111 const stringifiedResult = JSON.stringify(result);
112 await cache?.putBlento(did, handle as string, stringifiedResult);
113 }
114
115 const parsedResult = structuredClone(result) as any;
116
117 parsedResult.publication = (
118 parsedResult.publications as Awaited<ReturnType<typeof listRecords>>
119 ).find((v) => parseUri(v.uri)?.rkey === parsedResult.page)?.value;
120 parsedResult.publication ??= {
121 name: profile?.displayName || profile?.handle,
122 description: profile?.description
123 };
124
125 delete parsedResult['publications'];
126
127 return checkData(parsedResult);
128}
129
130export async function loadCardData(
131 handle: ActorIdentifier,
132 rkey: string,
133 cache: CacheService | undefined,
134 env?: Record<string, string | undefined>
135): Promise<WebsiteData> {
136 if (!handle) throw error(404);
137 if (handle === 'favicon.ico') throw error(404);
138
139 let did: Did | undefined = undefined;
140 if (isHandle(handle)) {
141 did = await resolveHandle({ handle });
142 } else if (isDid(handle)) {
143 did = handle;
144 } else {
145 throw error(404);
146 }
147
148 const [cardRecord, profile] = await Promise.all([
149 getRecord({
150 did,
151 collection: 'app.blento.card',
152 rkey
153 }).catch(() => undefined),
154 getDetailedProfile({ did })
155 ]);
156
157 if (!cardRecord?.value) {
158 throw error(404, 'Card not found');
159 }
160
161 const card = migrateCard(structuredClone(cardRecord.value) as Item);
162 const page = card.page ?? 'blento.self';
163
164 const publication = await getRecord({
165 did,
166 collection: page === 'blento.self' ? 'site.standard.publication' : 'app.blento.page',
167 rkey: page
168 }).catch(() => undefined);
169
170 const cards = [card];
171 const resolvedHandle = profile?.handle || (isHandle(handle) ? handle : did);
172
173 const additionalData = await loadAdditionalData(
174 cards,
175 { did, handle: resolvedHandle, cache },
176 env
177 );
178
179 const result = {
180 page,
181 handle: resolvedHandle,
182 did,
183 cards,
184 publication:
185 publication?.value ??
186 ({
187 name: profile?.displayName || profile?.handle,
188 description: profile?.description
189 } as WebsiteData['publication']),
190 additionalData,
191 profile,
192 updatedAt: Date.now(),
193 version: CURRENT_CACHE_VERSION
194 };
195
196 return result;
197}
198
199export async function loadCardTypeData(
200 handle: ActorIdentifier,
201 type: string,
202 cardData: Record<string, unknown>,
203 cache: CacheService | undefined,
204 env?: Record<string, string | undefined>
205): Promise<WebsiteData> {
206 if (!handle) throw error(404);
207 if (handle === 'favicon.ico') throw error(404);
208
209 const cardDef = CardDefinitionsByType[type];
210 if (!cardDef) {
211 throw error(404, 'Card type not found');
212 }
213
214 let did: Did | undefined = undefined;
215 if (isHandle(handle)) {
216 did = await resolveHandle({ handle });
217 } else if (isDid(handle)) {
218 did = handle;
219 } else {
220 throw error(404);
221 }
222
223 const [publication, profile] = await Promise.all([
224 getRecord({
225 did,
226 collection: 'site.standard.publication',
227 rkey: 'blento.self'
228 }).catch(() => undefined),
229 getDetailedProfile({ did })
230 ]);
231
232 const card = createEmptyCard('blento.self');
233 card.cardType = type;
234
235 cardDef.createNew?.(card);
236 card.cardData = {
237 ...card.cardData,
238 ...cardData
239 };
240
241 const cards = [card];
242 const resolvedHandle = profile?.handle || (isHandle(handle) ? handle : did);
243
244 const additionalData = await loadAdditionalData(
245 cards,
246 { did, handle: resolvedHandle, cache },
247 env
248 );
249
250 const result = {
251 page: 'blento.self',
252 handle: resolvedHandle,
253 did,
254 cards,
255 publication:
256 publication?.value ??
257 ({
258 name: profile?.displayName || profile?.handle,
259 description: profile?.description
260 } as WebsiteData['publication']),
261 additionalData,
262 profile,
263 updatedAt: Date.now(),
264 version: CURRENT_CACHE_VERSION
265 };
266
267 return checkData(result);
268}
269
270function migrateCard(card: Item): Item {
271 if (!card.version) {
272 card.x *= 2;
273 card.y *= 2;
274 card.h *= 2;
275 card.w *= 2;
276 card.mobileX *= 2;
277 card.mobileY *= 2;
278 card.mobileH *= 2;
279 card.mobileW *= 2;
280 card.version = 1;
281 }
282
283 if (!card.version || card.version < 2) {
284 card.page = 'blento.self';
285 card.version = 2;
286 }
287
288 const cardDef = CardDefinitionsByType[card.cardType];
289 cardDef?.migrate?.(card);
290
291 return card;
292}
293
294async function loadAdditionalData(
295 cards: Item[],
296 { did, handle, cache }: { did: Did; handle: string; cache?: CacheService },
297 env?: Record<string, string | undefined>
298) {
299 const cardTypes = new Set(cards.map((v) => v.cardType ?? '') as string[]);
300 const cardTypesArray = Array.from(cardTypes);
301 const additionDataPromises: Record<string, Promise<unknown>> = {};
302
303 for (const cardType of cardTypesArray) {
304 const cardDef = CardDefinitionsByType[cardType];
305 const items = cards.filter((v) => cardType === v.cardType);
306
307 try {
308 if (cardDef?.loadDataServer) {
309 additionDataPromises[cardType] = cardDef.loadDataServer(items, {
310 did,
311 handle,
312 cache,
313 env
314 });
315 } else if (cardDef?.loadData) {
316 additionDataPromises[cardType] = cardDef.loadData(items, { did, handle, cache });
317 }
318 } catch {
319 console.error('error getting additional data for', cardType);
320 }
321 }
322
323 await Promise.all(Object.values(additionDataPromises));
324
325 const additionalData: Record<string, unknown> = {};
326 for (const [key, value] of Object.entries(additionDataPromises)) {
327 try {
328 additionalData[key] = await value;
329 } catch (error) {
330 console.log('error loading', key, error);
331 }
332 }
333
334 return additionalData;
335}
336
337function checkData(data: WebsiteData): WebsiteData {
338 data = migrateData(data);
339
340 const cards = data.cards.filter((v) => v.page === data.page);
341
342 if (cards.length > 0) {
343 fixAllCollisions(cards, false);
344 fixAllCollisions(cards, true);
345
346 compactItems(cards, false);
347 compactItems(cards, true);
348 }
349
350 data.cards = cards;
351
352 return data;
353}
354
355function migrateData(data: WebsiteData): WebsiteData {
356 for (const card of data.cards) {
357 migrateCard(card);
358 }
359 return data;
360}