@ewanc26/atproto 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/package.json +29 -0
- package/src/agents.ts +127 -0
- package/src/cache.ts +63 -0
- package/src/documents.ts +247 -0
- package/src/engagement.ts +56 -0
- package/src/fetch.ts +355 -0
- package/src/index.ts +74 -0
- package/src/media.ts +106 -0
- package/src/musicbrainz.ts +188 -0
- package/src/pagination/fetchAllRecords.ts +57 -0
- package/src/pagination/index.ts +2 -0
- package/src/posts.ts +197 -0
- package/src/types.ts +272 -0
package/src/fetch.ts
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { cache } from './cache.js';
|
|
2
|
+
import { withFallback, resolveIdentity } from './agents.js';
|
|
3
|
+
import { buildPdsBlobUrl } from './media.js';
|
|
4
|
+
import { findArtwork } from './musicbrainz.js';
|
|
5
|
+
import type {
|
|
6
|
+
ProfileData,
|
|
7
|
+
SiteInfoData,
|
|
8
|
+
LinkData,
|
|
9
|
+
MusicStatusData,
|
|
10
|
+
KibunStatusData,
|
|
11
|
+
TangledRepo,
|
|
12
|
+
TangledReposData
|
|
13
|
+
} from './types.js';
|
|
14
|
+
|
|
15
|
+
export async function fetchProfile(did: string, fetchFn?: typeof fetch): Promise<ProfileData> {
|
|
16
|
+
const cacheKey = `profile:${did}`;
|
|
17
|
+
const cached = cache.get<ProfileData>(cacheKey);
|
|
18
|
+
if (cached) return cached;
|
|
19
|
+
|
|
20
|
+
const profile = await withFallback(
|
|
21
|
+
did,
|
|
22
|
+
async (agent) => {
|
|
23
|
+
const response = await agent.getProfile({ actor: did });
|
|
24
|
+
return response.data;
|
|
25
|
+
},
|
|
26
|
+
false,
|
|
27
|
+
fetchFn
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
let pronouns: string | undefined;
|
|
31
|
+
try {
|
|
32
|
+
const recordResponse = await withFallback(
|
|
33
|
+
did,
|
|
34
|
+
async (agent) => {
|
|
35
|
+
const response = await agent.com.atproto.repo.getRecord({
|
|
36
|
+
repo: did,
|
|
37
|
+
collection: 'app.bsky.actor.profile',
|
|
38
|
+
rkey: 'self'
|
|
39
|
+
});
|
|
40
|
+
return response.data;
|
|
41
|
+
},
|
|
42
|
+
false,
|
|
43
|
+
fetchFn
|
|
44
|
+
);
|
|
45
|
+
pronouns = (recordResponse.value as any).pronouns;
|
|
46
|
+
} catch { /* pronouns optional */ }
|
|
47
|
+
|
|
48
|
+
const data: ProfileData = {
|
|
49
|
+
did: profile.did,
|
|
50
|
+
handle: profile.handle,
|
|
51
|
+
displayName: profile.displayName,
|
|
52
|
+
description: profile.description,
|
|
53
|
+
avatar: profile.avatar,
|
|
54
|
+
banner: profile.banner,
|
|
55
|
+
followersCount: profile.followersCount,
|
|
56
|
+
followsCount: profile.followsCount,
|
|
57
|
+
postsCount: profile.postsCount,
|
|
58
|
+
pronouns
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
cache.set(cacheKey, data);
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function fetchSiteInfo(
|
|
66
|
+
did: string,
|
|
67
|
+
fetchFn?: typeof fetch
|
|
68
|
+
): Promise<SiteInfoData | null> {
|
|
69
|
+
const cacheKey = `siteinfo:${did}`;
|
|
70
|
+
const cached = cache.get<SiteInfoData>(cacheKey);
|
|
71
|
+
if (cached) return cached;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const result = await withFallback(
|
|
75
|
+
did,
|
|
76
|
+
async (agent) => {
|
|
77
|
+
try {
|
|
78
|
+
const response = await agent.com.atproto.repo.getRecord({
|
|
79
|
+
repo: did,
|
|
80
|
+
collection: 'uk.ewancroft.site.info',
|
|
81
|
+
rkey: 'self'
|
|
82
|
+
});
|
|
83
|
+
return response.data;
|
|
84
|
+
} catch (err: any) {
|
|
85
|
+
if (err.error === 'RecordNotFound') return null;
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
true,
|
|
90
|
+
fetchFn
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (!result?.value) return null;
|
|
94
|
+
const data = result.value as SiteInfoData;
|
|
95
|
+
cache.set(cacheKey, data);
|
|
96
|
+
return data;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function fetchLinks(
|
|
103
|
+
did: string,
|
|
104
|
+
fetchFn?: typeof fetch
|
|
105
|
+
): Promise<LinkData | null> {
|
|
106
|
+
const cacheKey = `links:${did}`;
|
|
107
|
+
const cached = cache.get<LinkData>(cacheKey);
|
|
108
|
+
if (cached) return cached;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const value = await withFallback(
|
|
112
|
+
did,
|
|
113
|
+
async (agent) => {
|
|
114
|
+
const response = await agent.com.atproto.repo.getRecord({
|
|
115
|
+
repo: did,
|
|
116
|
+
collection: 'blue.linkat.board',
|
|
117
|
+
rkey: 'self'
|
|
118
|
+
});
|
|
119
|
+
return response.data.value;
|
|
120
|
+
},
|
|
121
|
+
true,
|
|
122
|
+
fetchFn
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (!value || !Array.isArray((value as any).cards)) return null;
|
|
126
|
+
const data: LinkData = { cards: (value as any).cards };
|
|
127
|
+
cache.set(cacheKey, data);
|
|
128
|
+
return data;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function fetchMusicStatus(
|
|
135
|
+
did: string,
|
|
136
|
+
fetchFn?: typeof fetch
|
|
137
|
+
): Promise<MusicStatusData | null> {
|
|
138
|
+
const cacheKey = `music-status:${did}`;
|
|
139
|
+
const cached = cache.get<MusicStatusData>(cacheKey);
|
|
140
|
+
if (cached) return cached;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Try actor status first
|
|
144
|
+
try {
|
|
145
|
+
const statusRecords = await withFallback(
|
|
146
|
+
did,
|
|
147
|
+
async (agent) => {
|
|
148
|
+
const response = await agent.com.atproto.repo.listRecords({
|
|
149
|
+
repo: did,
|
|
150
|
+
collection: 'fm.teal.alpha.actor.status',
|
|
151
|
+
limit: 1
|
|
152
|
+
});
|
|
153
|
+
return response.data.records;
|
|
154
|
+
},
|
|
155
|
+
true,
|
|
156
|
+
fetchFn
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (statusRecords?.length) {
|
|
160
|
+
const record = statusRecords[0];
|
|
161
|
+
const value = record.value as any;
|
|
162
|
+
if (value.expiry) {
|
|
163
|
+
const expiryTime = parseInt(value.expiry) * 1000;
|
|
164
|
+
if (Date.now() <= expiryTime) {
|
|
165
|
+
const trackName = value.item?.trackName || value.trackName;
|
|
166
|
+
const artists = value.item?.artists || value.artists || [];
|
|
167
|
+
const releaseName = value.item?.releaseName || value.releaseName;
|
|
168
|
+
const artistName = artists[0]?.artistName;
|
|
169
|
+
const releaseMbId = value.item?.releaseMbId || value.releaseMbId;
|
|
170
|
+
|
|
171
|
+
let artworkUrl: string | undefined;
|
|
172
|
+
if (releaseName && artistName) {
|
|
173
|
+
artworkUrl = (await findArtwork(releaseName, artistName, releaseName, releaseMbId, fetchFn)) || undefined;
|
|
174
|
+
}
|
|
175
|
+
if (!artworkUrl && trackName && artistName) {
|
|
176
|
+
artworkUrl = (await findArtwork(trackName, artistName, releaseName, releaseMbId, fetchFn)) || undefined;
|
|
177
|
+
}
|
|
178
|
+
if (!artworkUrl) {
|
|
179
|
+
const artwork = value.item?.artwork || value.artwork;
|
|
180
|
+
if (artwork?.ref?.$link) {
|
|
181
|
+
const identity = await resolveIdentity(did, fetchFn);
|
|
182
|
+
artworkUrl = buildPdsBlobUrl(identity.pds, did, artwork.ref.$link);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const data: MusicStatusData = {
|
|
187
|
+
trackName,
|
|
188
|
+
artists,
|
|
189
|
+
releaseName,
|
|
190
|
+
playedTime: value.item?.playedTime || value.playedTime,
|
|
191
|
+
originUrl: value.item?.originUrl || value.originUrl,
|
|
192
|
+
recordingMbId: value.item?.recordingMbId || value.recordingMbId,
|
|
193
|
+
releaseMbId,
|
|
194
|
+
isrc: value.isrc,
|
|
195
|
+
duration: value.duration,
|
|
196
|
+
musicServiceBaseDomain: value.item?.musicServiceBaseDomain || value.musicServiceBaseDomain,
|
|
197
|
+
submissionClientAgent: value.item?.submissionClientAgent || value.submissionClientAgent,
|
|
198
|
+
$type: 'fm.teal.alpha.actor.status',
|
|
199
|
+
expiry: value.expiry,
|
|
200
|
+
artwork: value.item?.artwork || value.artwork,
|
|
201
|
+
artworkUrl
|
|
202
|
+
};
|
|
203
|
+
cache.set(cacheKey, data);
|
|
204
|
+
return data;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch { /* fall through to feed play */ }
|
|
209
|
+
|
|
210
|
+
// Fall back to feed play
|
|
211
|
+
const playRecords = await withFallback(
|
|
212
|
+
did,
|
|
213
|
+
async (agent) => {
|
|
214
|
+
const response = await agent.com.atproto.repo.listRecords({
|
|
215
|
+
repo: did,
|
|
216
|
+
collection: 'fm.teal.alpha.feed.play',
|
|
217
|
+
limit: 1
|
|
218
|
+
});
|
|
219
|
+
return response.data.records;
|
|
220
|
+
},
|
|
221
|
+
true,
|
|
222
|
+
fetchFn
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
if (playRecords?.length) {
|
|
226
|
+
const record = playRecords[0];
|
|
227
|
+
const value = record.value as any;
|
|
228
|
+
const artists = value.artists || [];
|
|
229
|
+
const artistName = artists[0]?.artistName;
|
|
230
|
+
|
|
231
|
+
let artworkUrl: string | undefined;
|
|
232
|
+
if (value.releaseName && artistName) {
|
|
233
|
+
artworkUrl = (await findArtwork(value.releaseName, artistName, value.releaseName, value.releaseMbId, fetchFn)) || undefined;
|
|
234
|
+
}
|
|
235
|
+
if (!artworkUrl && value.trackName && artistName) {
|
|
236
|
+
artworkUrl = (await findArtwork(value.trackName, artistName, value.releaseName, value.releaseMbId, fetchFn)) || undefined;
|
|
237
|
+
}
|
|
238
|
+
if (!artworkUrl && value.artwork?.ref?.$link) {
|
|
239
|
+
const identity = await resolveIdentity(did, fetchFn);
|
|
240
|
+
artworkUrl = buildPdsBlobUrl(identity.pds, did, value.artwork.ref.$link);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const data: MusicStatusData = {
|
|
244
|
+
trackName: value.trackName,
|
|
245
|
+
artists,
|
|
246
|
+
releaseName: value.releaseName,
|
|
247
|
+
playedTime: value.playedTime,
|
|
248
|
+
originUrl: value.originUrl,
|
|
249
|
+
recordingMbId: value.recordingMbId,
|
|
250
|
+
releaseMbId: value.releaseMbId,
|
|
251
|
+
isrc: value.isrc,
|
|
252
|
+
duration: value.duration,
|
|
253
|
+
musicServiceBaseDomain: value.musicServiceBaseDomain,
|
|
254
|
+
submissionClientAgent: value.submissionClientAgent,
|
|
255
|
+
$type: 'fm.teal.alpha.feed.play',
|
|
256
|
+
artwork: value.artwork,
|
|
257
|
+
artworkUrl
|
|
258
|
+
};
|
|
259
|
+
cache.set(cacheKey, data);
|
|
260
|
+
return data;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return null;
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function fetchKibunStatus(
|
|
270
|
+
did: string,
|
|
271
|
+
fetchFn?: typeof fetch
|
|
272
|
+
): Promise<KibunStatusData | null> {
|
|
273
|
+
const cacheKey = `kibun-status:${did}`;
|
|
274
|
+
const cached = cache.get<KibunStatusData>(cacheKey);
|
|
275
|
+
if (cached) return cached;
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const statusRecords = await withFallback(
|
|
279
|
+
did,
|
|
280
|
+
async (agent) => {
|
|
281
|
+
const response = await agent.com.atproto.repo.listRecords({
|
|
282
|
+
repo: did,
|
|
283
|
+
collection: 'social.kibun.status',
|
|
284
|
+
limit: 1
|
|
285
|
+
});
|
|
286
|
+
return response.data.records;
|
|
287
|
+
},
|
|
288
|
+
true,
|
|
289
|
+
fetchFn
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
if (statusRecords?.length) {
|
|
293
|
+
const value = statusRecords[0].value as any;
|
|
294
|
+
const data: KibunStatusData = {
|
|
295
|
+
text: value.text,
|
|
296
|
+
emoji: value.emoji,
|
|
297
|
+
createdAt: value.createdAt,
|
|
298
|
+
$type: 'social.kibun.status'
|
|
299
|
+
};
|
|
300
|
+
cache.set(cacheKey, data);
|
|
301
|
+
return data;
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function fetchTangledRepos(
|
|
310
|
+
did: string,
|
|
311
|
+
fetchFn?: typeof fetch
|
|
312
|
+
): Promise<TangledReposData | null> {
|
|
313
|
+
const cacheKey = `tangled:${did}`;
|
|
314
|
+
const cached = cache.get<TangledReposData>(cacheKey);
|
|
315
|
+
if (cached) return cached;
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const records = await withFallback(
|
|
319
|
+
did,
|
|
320
|
+
async (agent) => {
|
|
321
|
+
const response = await agent.com.atproto.repo.listRecords({
|
|
322
|
+
repo: did,
|
|
323
|
+
collection: 'sh.tangled.repo',
|
|
324
|
+
limit: 100
|
|
325
|
+
});
|
|
326
|
+
return response.data.records;
|
|
327
|
+
},
|
|
328
|
+
true,
|
|
329
|
+
fetchFn
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
if (!records.length) return null;
|
|
333
|
+
|
|
334
|
+
const repos: TangledRepo[] = records.map((record) => {
|
|
335
|
+
const value = record.value as any;
|
|
336
|
+
return {
|
|
337
|
+
uri: record.uri,
|
|
338
|
+
name: value.name,
|
|
339
|
+
description: value.description,
|
|
340
|
+
knot: value.knot,
|
|
341
|
+
createdAt: value.createdAt,
|
|
342
|
+
labels: value.labels,
|
|
343
|
+
source: value.source,
|
|
344
|
+
spindle: value.spindle
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
repos.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
349
|
+
const data: TangledReposData = { repos };
|
|
350
|
+
cache.set(cacheKey, data);
|
|
351
|
+
return data;
|
|
352
|
+
} catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Re-export types and functions from the AT Protocol service layer.
|
|
2
|
+
// Key API difference from the app's src/lib/services/atproto:
|
|
3
|
+
// All functions that previously read PUBLIC_ATPROTO_DID from the environment
|
|
4
|
+
// now accept `did: string` as their first argument.
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
ProfileData,
|
|
8
|
+
SiteInfoData,
|
|
9
|
+
LinkData,
|
|
10
|
+
LinkCard,
|
|
11
|
+
BlueskyPost,
|
|
12
|
+
BlogPost,
|
|
13
|
+
PostAuthor,
|
|
14
|
+
ExternalLink,
|
|
15
|
+
Facet,
|
|
16
|
+
Technology,
|
|
17
|
+
License,
|
|
18
|
+
BasedOnItem,
|
|
19
|
+
RelatedService,
|
|
20
|
+
Repository,
|
|
21
|
+
Credit,
|
|
22
|
+
SectionLicense,
|
|
23
|
+
ResolvedIdentity,
|
|
24
|
+
CacheEntry,
|
|
25
|
+
MusicStatusData,
|
|
26
|
+
MusicArtist,
|
|
27
|
+
KibunStatusData,
|
|
28
|
+
TangledRepo,
|
|
29
|
+
TangledReposData,
|
|
30
|
+
StandardSitePublication,
|
|
31
|
+
StandardSitePublicationsData,
|
|
32
|
+
StandardSiteDocument,
|
|
33
|
+
StandardSiteDocumentsData,
|
|
34
|
+
StandardSiteBasicTheme,
|
|
35
|
+
StandardSiteThemeColor
|
|
36
|
+
} from './types';
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
fetchProfile,
|
|
40
|
+
fetchSiteInfo,
|
|
41
|
+
fetchLinks,
|
|
42
|
+
fetchMusicStatus,
|
|
43
|
+
fetchKibunStatus,
|
|
44
|
+
fetchTangledRepos
|
|
45
|
+
} from './fetch';
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
fetchPublications,
|
|
49
|
+
fetchDocuments,
|
|
50
|
+
fetchRecentDocuments,
|
|
51
|
+
fetchBlogPosts
|
|
52
|
+
} from './documents';
|
|
53
|
+
|
|
54
|
+
export { fetchLatestBlueskyPost, fetchPostFromUri } from './posts';
|
|
55
|
+
|
|
56
|
+
export { buildPdsBlobUrl, extractCidFromImageObject, extractImageUrlsFromValue } from './media';
|
|
57
|
+
|
|
58
|
+
export { createAgent, constellationAgent, defaultAgent, resolveIdentity, getPublicAgent, getPDSAgent, withFallback, resetAgents } from './agents';
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
searchMusicBrainzRelease,
|
|
62
|
+
buildCoverArtUrl,
|
|
63
|
+
searchiTunesArtwork,
|
|
64
|
+
searchDeezerArtwork,
|
|
65
|
+
searchLastFmArtwork,
|
|
66
|
+
findArtwork
|
|
67
|
+
} from './musicbrainz';
|
|
68
|
+
|
|
69
|
+
export { fetchEngagementFromConstellation, fetchAllEngagement } from './engagement';
|
|
70
|
+
|
|
71
|
+
export { cache, ATProtoCache, CACHE_TTL } from './cache';
|
|
72
|
+
|
|
73
|
+
export { fetchAllRecords, fetchAllUserRecords } from './pagination';
|
|
74
|
+
export type { FetchRecordsConfig, AtProtoRecord } from './pagination';
|
package/src/media.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export function buildPdsBlobUrl(pds: string, did: string, cid: string): string {
|
|
2
|
+
return `${pds.replace(/\/$/, '')}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function extractCidFromImageObject(img: any): string | null {
|
|
6
|
+
if (!img) return null;
|
|
7
|
+
if (img.image && img.image.ref && img.image.ref.$link) return img.image.ref.$link as string;
|
|
8
|
+
if (img.ref && img.ref.$link) return img.ref.$link as string;
|
|
9
|
+
if (img.cid) return img.cid as string;
|
|
10
|
+
if (typeof img === 'string') return img;
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function extractImageUrlsFromValue(value: any, did: string, limit = 4): string[] {
|
|
15
|
+
const urls: string[] = [];
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const embed = (value as any)?.embed ?? null;
|
|
19
|
+
|
|
20
|
+
if (embed) {
|
|
21
|
+
if (embed.$type === 'app.bsky.embed.images#view' && Array.isArray(embed.images)) {
|
|
22
|
+
for (const img of embed.images) {
|
|
23
|
+
const imageUrl = img.fullsize || img.thumb;
|
|
24
|
+
if (imageUrl) urls.push(imageUrl);
|
|
25
|
+
else {
|
|
26
|
+
const cid = extractCidFromImageObject(img);
|
|
27
|
+
if (cid) urls.push(`https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${cid}@jpeg`);
|
|
28
|
+
}
|
|
29
|
+
if (urls.length >= limit) return urls;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (embed.$type === 'app.bsky.embed.video#view' || embed.$type === 'app.bsky.embed.video') {
|
|
34
|
+
const videoCid =
|
|
35
|
+
(embed as any)?.jobStatus?.blob ??
|
|
36
|
+
(embed as any)?.video?.ref?.$link ??
|
|
37
|
+
(embed as any)?.video?.cid ??
|
|
38
|
+
null;
|
|
39
|
+
if (videoCid) {
|
|
40
|
+
urls.push(`https://video.bsky.app/watch/${did}/${videoCid}/playlist.m3u8`);
|
|
41
|
+
if (urls.length >= limit) return urls;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (embed.$type === 'app.bsky.embed.recordWithMedia#view') {
|
|
46
|
+
const media = embed.media;
|
|
47
|
+
if (
|
|
48
|
+
media &&
|
|
49
|
+
media.$type === 'app.bsky.embed.images#view' &&
|
|
50
|
+
Array.isArray(media.images)
|
|
51
|
+
) {
|
|
52
|
+
for (const img of media.images) {
|
|
53
|
+
const imageUrl = img.fullsize || img.thumb;
|
|
54
|
+
if (imageUrl) urls.push(imageUrl);
|
|
55
|
+
else {
|
|
56
|
+
const cid = extractCidFromImageObject(img);
|
|
57
|
+
if (cid)
|
|
58
|
+
urls.push(`https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${cid}@jpeg`);
|
|
59
|
+
}
|
|
60
|
+
if (urls.length >= limit) return urls;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const quotedRecord = embed.record;
|
|
64
|
+
if (quotedRecord) {
|
|
65
|
+
const quotedValue = quotedRecord.value ?? quotedRecord.record?.value ?? null;
|
|
66
|
+
if (quotedValue) {
|
|
67
|
+
const nested = extractImageUrlsFromValue(quotedValue, did, limit - urls.length);
|
|
68
|
+
urls.push(...nested);
|
|
69
|
+
if (urls.length >= limit) return urls;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (embed.$type === 'app.bsky.embed.record#view' && embed.record) {
|
|
75
|
+
const quotedValue =
|
|
76
|
+
embed.record.value ?? embed.record.record?.value ?? null;
|
|
77
|
+
if (quotedValue) {
|
|
78
|
+
const nested = extractImageUrlsFromValue(quotedValue, did, limit - urls.length);
|
|
79
|
+
urls.push(...nested);
|
|
80
|
+
if (urls.length >= limit) return urls;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (Array.isArray((value as any).embeds)) {
|
|
86
|
+
for (const e of (value as any).embeds) {
|
|
87
|
+
if (e.$type === 'app.bsky.embed.images#view' && Array.isArray(e.images)) {
|
|
88
|
+
for (const img of e.images) {
|
|
89
|
+
const imageUrl = img.fullsize || img.thumb;
|
|
90
|
+
if (imageUrl) urls.push(imageUrl);
|
|
91
|
+
else {
|
|
92
|
+
const cid = extractCidFromImageObject(img);
|
|
93
|
+
if (cid)
|
|
94
|
+
urls.push(`https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${cid}@jpeg`);
|
|
95
|
+
}
|
|
96
|
+
if (urls.length >= limit) return urls;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// conservative: return what we have
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return urls.slice(0, limit);
|
|
106
|
+
}
|