your personal website on atproto - mirror
blento.app
1import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto';
2import { CardDefinitionsByType } from '$lib/cards';
3import type { Item, UserCache, WebsiteData } from '$lib/types';
4import { compactItems, fixAllCollisions } from '$lib/helper';
5import { error } from '@sveltejs/kit';
6import type { ActorIdentifier, Did } from '@atcute/lexicons';
7
8import { isDid, isHandle } from '@atcute/lexicons/syntax';
9
10const CURRENT_CACHE_VERSION = 1;
11
12export async function getCache(handle: string, page: string, cache?: UserCache) {
13 try {
14 const cachedResult = await cache?.get?.(handle);
15
16 if (!cachedResult) return;
17 const result = JSON.parse(cachedResult);
18 const update = result.updatedAt;
19 const timePassed = (Date.now() - update) / 1000;
20
21 const ONE_DAY = 60 * 60 * 24;
22
23 if (!result.version || result.version !== CURRENT_CACHE_VERSION) {
24 console.log('skipping cache because of version mismatch');
25 return;
26 }
27
28 if (timePassed > ONE_DAY) {
29 console.log('skipping cache because of age');
30 return;
31 }
32
33 result.page = 'blento.' + page;
34
35 result.publication = (result.publications as Awaited<ReturnType<typeof listRecords>>).find(
36 (v) => parseUri(v.uri)?.rkey === result.page
37 )?.value;
38 result.publication ??= {
39 name: result.profile?.displayName || result.profile?.handle,
40 description: result.profile?.description
41 };
42
43 delete result['publications'];
44
45 console.log('using cached result for handle', handle, 'last update', timePassed, 'seconds ago');
46 return checkData(result);
47 } catch (error) {
48 console.log('getting cached result failed', error);
49 }
50}
51
52export async function loadData(
53 handle: ActorIdentifier,
54 cache: UserCache | undefined,
55 forceUpdate: boolean = false,
56 page: string = 'self'
57): Promise<WebsiteData> {
58 if (!handle) throw error(404);
59 if (handle === 'favicon.ico') throw error(404);
60
61 if (!forceUpdate) {
62 const cachedResult = await getCache(handle, page, cache);
63
64 if (cachedResult) return cachedResult;
65 }
66
67 let did: Did | undefined = undefined;
68 if (isHandle(handle)) {
69 did = await resolveHandle({ handle });
70 } else if (isDid(handle)) {
71 did = handle;
72 } else {
73 throw error(404);
74 }
75
76 const cards = await listRecords({ did, collection: 'app.blento.card' }).catch(() => {
77 console.error('error getting records for collection app.blento.card');
78 return [] as Awaited<ReturnType<typeof listRecords>>;
79 });
80
81 const mainPublication = await getRecord({
82 did,
83 collection: 'site.standard.publication',
84 rkey: 'blento.self'
85 }).catch(() => {
86 console.error('error getting record for collection site.standard.publication');
87 return undefined;
88 });
89
90 const pages = await listRecords({ did, collection: 'app.blento.page' }).catch(() => {
91 console.error('error getting records for collection app.blento.page');
92 return [] as Awaited<ReturnType<typeof listRecords>>;
93 });
94
95 const profile = await getDetailedProfile({ did });
96
97 const cardTypes = new Set(cards.map((v) => v.value.cardType ?? '') as string[]);
98 const cardTypesArray = Array.from(cardTypes);
99
100 const additionDataPromises: Record<string, Promise<unknown>> = {};
101
102 const loadOptions = { did, handle, cache };
103
104 for (const cardType of cardTypesArray) {
105 const cardDef = CardDefinitionsByType[cardType];
106
107 if (!cardDef?.loadData) continue;
108
109 try {
110 additionDataPromises[cardType] = cardDef.loadData(
111 cards.filter((v) => cardType === v.value.cardType).map((v) => v.value) as Item[],
112 loadOptions
113 );
114 } catch {
115 console.error('error getting additional data for', cardType);
116 }
117 }
118
119 await Promise.all(Object.values(additionDataPromises));
120
121 const additionalData: Record<string, unknown> = {};
122 for (const [key, value] of Object.entries(additionDataPromises)) {
123 try {
124 additionalData[key] = await value;
125 } catch (error) {
126 console.log('error loading', key, error);
127 }
128 }
129
130 const result = {
131 page: 'blento.' + page,
132 handle,
133 did,
134 cards: (cards.map((v) => {
135 return { ...v.value };
136 }) ?? []) as Item[],
137 publications: [mainPublication, ...pages].filter((v) => v),
138 additionalData,
139 profile,
140 updatedAt: Date.now(),
141 version: CURRENT_CACHE_VERSION
142 };
143
144 const stringifiedResult = JSON.stringify(result);
145 await cache?.put?.(handle, stringifiedResult);
146
147 const parsedResult = JSON.parse(stringifiedResult);
148
149 parsedResult.publication = (
150 parsedResult.publications as Awaited<ReturnType<typeof listRecords>>
151 ).find((v) => parseUri(v.uri)?.rkey === parsedResult.page)?.value;
152 parsedResult.publication ??= {
153 name: profile?.displayName || profile?.handle,
154 description: profile?.description
155 };
156
157 delete parsedResult['publications'];
158
159 return checkData(parsedResult);
160}
161
162function migrateFromV0ToV1(data: WebsiteData): WebsiteData {
163 for (const card of data.cards) {
164 if (card.version) continue;
165 card.x *= 2;
166 card.y *= 2;
167 card.h *= 2;
168 card.w *= 2;
169 card.mobileX *= 2;
170 card.mobileY *= 2;
171 card.mobileH *= 2;
172 card.mobileW *= 2;
173 card.version = 1;
174 }
175
176 return data;
177}
178
179function migrateFromV1ToV2(data: WebsiteData): WebsiteData {
180 for (const card of data.cards) {
181 if (!card.version || card.version < 2) {
182 card.page = 'blento.self';
183 card.version = 2;
184 }
185 }
186 return data;
187}
188
189function migrateCards(data: WebsiteData): WebsiteData {
190 for (const card of data.cards) {
191 const cardDef = CardDefinitionsByType[card.cardType];
192
193 if (!cardDef?.migrate) continue;
194
195 cardDef.migrate(card);
196 }
197 return data;
198}
199
200function checkData(data: WebsiteData): WebsiteData {
201 data = migrateData(data);
202
203 const cards = data.cards.filter((v) => v.page === data.page);
204
205 if (cards.length > 0) {
206 fixAllCollisions(cards);
207 fixAllCollisions(cards, true);
208
209 compactItems(cards);
210 compactItems(cards, true);
211 }
212
213 data.cards = cards;
214
215 return data;
216}
217
218function migrateData(data: WebsiteData): WebsiteData {
219 return migrateCards(migrateFromV1ToV2(migrateFromV0ToV1(data)));
220}