@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
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { cache } from './cache.js';
|
|
2
|
+
|
|
3
|
+
interface MusicBrainzRelease {
|
|
4
|
+
id: string;
|
|
5
|
+
score: number;
|
|
6
|
+
title: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface MusicBrainzSearchResponse {
|
|
10
|
+
releases: MusicBrainzRelease[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface iTunesSearchResponse {
|
|
14
|
+
resultCount: number;
|
|
15
|
+
results: Array<{ artworkUrl100?: string }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function searchMusicBrainzRelease(
|
|
19
|
+
trackName: string,
|
|
20
|
+
artistName: string,
|
|
21
|
+
releaseName?: string
|
|
22
|
+
): Promise<string | null> {
|
|
23
|
+
const cacheKey = `mb:release:${trackName}:${artistName}:${releaseName || 'none'}`;
|
|
24
|
+
const cached = cache.get<string | null>(cacheKey);
|
|
25
|
+
if (cached !== null) return cached;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
if (releaseName) {
|
|
29
|
+
const result = await searchByReleaseName(releaseName, artistName);
|
|
30
|
+
if (result) { cache.set(cacheKey, result); return result; }
|
|
31
|
+
}
|
|
32
|
+
const result = await searchByTrackName(trackName, artistName);
|
|
33
|
+
if (result) { cache.set(cacheKey, result); return result; }
|
|
34
|
+
cache.set(cacheKey, null);
|
|
35
|
+
return null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function searchByReleaseName(releaseName: string, artistName: string): Promise<string | null> {
|
|
42
|
+
try {
|
|
43
|
+
const query = `release:"${releaseName}" AND artist:"${artistName}"`;
|
|
44
|
+
const url = `https://musicbrainz.org/ws/2/release/?query=${encodeURIComponent(query)}&fmt=json&limit=5`;
|
|
45
|
+
const response = await fetch(url, {
|
|
46
|
+
headers: { 'User-Agent': 'ewancroft.uk/1.0.0 (https://ewancroft.uk)', Accept: 'application/json' }
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) return null;
|
|
49
|
+
const data: MusicBrainzSearchResponse = await response.json();
|
|
50
|
+
if (!data.releases?.length) return null;
|
|
51
|
+
const best = data.releases[0];
|
|
52
|
+
if (best.score < 80) return null;
|
|
53
|
+
return best.id;
|
|
54
|
+
} catch { return null; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function searchByTrackName(trackName: string, artistName: string): Promise<string | null> {
|
|
58
|
+
try {
|
|
59
|
+
const query = `recording:"${trackName}" AND artist:"${artistName}"`;
|
|
60
|
+
const url = `https://musicbrainz.org/ws/2/release/?query=${encodeURIComponent(query)}&fmt=json&limit=5`;
|
|
61
|
+
const response = await fetch(url, {
|
|
62
|
+
headers: { 'User-Agent': 'ewancroft.uk/1.0.0 (https://ewancroft.uk)', Accept: 'application/json' }
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) return null;
|
|
65
|
+
const data: MusicBrainzSearchResponse = await response.json();
|
|
66
|
+
if (!data.releases?.length) return null;
|
|
67
|
+
const best = data.releases[0];
|
|
68
|
+
if (best.score < 75) return null;
|
|
69
|
+
return best.id;
|
|
70
|
+
} catch { return null; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildCoverArtUrl(releaseMbId: string, size: 250 | 500 | 1200 = 500): string {
|
|
74
|
+
return `https://coverartarchive.org/release/${releaseMbId}/front-${size}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function searchiTunesArtwork(
|
|
78
|
+
trackName: string,
|
|
79
|
+
artistName: string,
|
|
80
|
+
releaseName?: string
|
|
81
|
+
): Promise<string | null> {
|
|
82
|
+
const cacheKey = `itunes:artwork:${trackName}:${artistName}:${releaseName || 'none'}`;
|
|
83
|
+
const cached = cache.get<string | null>(cacheKey);
|
|
84
|
+
if (cached !== null) return cached;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const searchTerm = releaseName ? `${releaseName} ${artistName}` : `${trackName} ${artistName}`;
|
|
88
|
+
const url = `https://itunes.apple.com/search?term=${encodeURIComponent(searchTerm)}&entity=album&limit=5`;
|
|
89
|
+
const response = await fetch(url);
|
|
90
|
+
if (!response.ok) { cache.set(cacheKey, null); return null; }
|
|
91
|
+
const data: iTunesSearchResponse = await response.json();
|
|
92
|
+
if (!data.results?.length) { cache.set(cacheKey, null); return null; }
|
|
93
|
+
let artworkUrl = data.results[0].artworkUrl100;
|
|
94
|
+
if (artworkUrl) {
|
|
95
|
+
artworkUrl = artworkUrl.replace('100x100', '600x600');
|
|
96
|
+
cache.set(cacheKey, artworkUrl);
|
|
97
|
+
return artworkUrl;
|
|
98
|
+
}
|
|
99
|
+
cache.set(cacheKey, null);
|
|
100
|
+
return null;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function searchDeezerArtwork(
|
|
107
|
+
trackName: string,
|
|
108
|
+
artistName: string,
|
|
109
|
+
releaseName?: string
|
|
110
|
+
): Promise<string | null> {
|
|
111
|
+
// Deezer has CORS restrictions in browsers — skip silently
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function searchLastFmArtwork(
|
|
116
|
+
trackName: string,
|
|
117
|
+
artistName: string,
|
|
118
|
+
releaseName?: string
|
|
119
|
+
): Promise<string | null> {
|
|
120
|
+
if (!releaseName) return null;
|
|
121
|
+
|
|
122
|
+
const cacheKey = `lastfm:artwork:${trackName}:${artistName}:${releaseName}`;
|
|
123
|
+
const cached = cache.get<string | null>(cacheKey);
|
|
124
|
+
if (cached !== null) return cached;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const url = `https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=8de8b91ab0c3f8d08a35c33bf0e0e803&artist=${encodeURIComponent(artistName)}&album=${encodeURIComponent(releaseName)}&format=json`;
|
|
128
|
+
const response = await fetch(url);
|
|
129
|
+
if (!response.ok) { cache.set(cacheKey, null); return null; }
|
|
130
|
+
const data: any = await response.json();
|
|
131
|
+
if (!data.album?.image) { cache.set(cacheKey, null); return null; }
|
|
132
|
+
const images = data.album.image;
|
|
133
|
+
const largeImage =
|
|
134
|
+
images.find((img: any) => img.size === 'extralarge') ||
|
|
135
|
+
images.find((img: any) => img.size === 'large') ||
|
|
136
|
+
images.find((img: any) => img.size === 'medium');
|
|
137
|
+
if (largeImage?.['#text']) {
|
|
138
|
+
cache.set(cacheKey, largeImage['#text']);
|
|
139
|
+
return largeImage['#text'];
|
|
140
|
+
}
|
|
141
|
+
cache.set(cacheKey, null);
|
|
142
|
+
return null;
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Cascading artwork search: Cover Art Archive → MusicBrainz+CAA → iTunes → Last.fm
|
|
150
|
+
*/
|
|
151
|
+
export async function findArtwork(
|
|
152
|
+
trackName: string,
|
|
153
|
+
artistName: string,
|
|
154
|
+
releaseName?: string,
|
|
155
|
+
releaseMbId?: string,
|
|
156
|
+
fetchFn?: typeof fetch
|
|
157
|
+
): Promise<string | null> {
|
|
158
|
+
const _fetch = fetchFn || globalThis.fetch;
|
|
159
|
+
|
|
160
|
+
// 1. Try Cover Art Archive with known MBID
|
|
161
|
+
if (releaseMbId) {
|
|
162
|
+
const caaUrl = buildCoverArtUrl(releaseMbId, 500);
|
|
163
|
+
try {
|
|
164
|
+
const res = await _fetch(caaUrl, { method: 'HEAD' });
|
|
165
|
+
if (res.ok) return caaUrl;
|
|
166
|
+
} catch { /* continue */ }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 2. Search MusicBrainz for MBID, then try CAA
|
|
170
|
+
const mbId = await searchMusicBrainzRelease(trackName, artistName, releaseName);
|
|
171
|
+
if (mbId) {
|
|
172
|
+
const caaUrl = buildCoverArtUrl(mbId, 500);
|
|
173
|
+
try {
|
|
174
|
+
const res = await _fetch(caaUrl, { method: 'HEAD' });
|
|
175
|
+
if (res.ok) return caaUrl;
|
|
176
|
+
} catch { /* continue */ }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 3. Try iTunes
|
|
180
|
+
const iTunesUrl = await searchiTunesArtwork(trackName, artistName, releaseName);
|
|
181
|
+
if (iTunesUrl) return iTunesUrl;
|
|
182
|
+
|
|
183
|
+
// 4. Try Last.fm
|
|
184
|
+
const lastFmUrl = await searchLastFmArtwork(trackName, artistName, releaseName);
|
|
185
|
+
if (lastFmUrl) return lastFmUrl;
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { withFallback } from '../agents.js';
|
|
2
|
+
|
|
3
|
+
export interface FetchRecordsConfig {
|
|
4
|
+
repo: string;
|
|
5
|
+
collection: string;
|
|
6
|
+
limit?: number;
|
|
7
|
+
fetchFn?: typeof fetch;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AtProtoRecord<T = any> {
|
|
11
|
+
uri: string;
|
|
12
|
+
value: T;
|
|
13
|
+
cid?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function fetchAllRecords<T = any>(
|
|
17
|
+
config: FetchRecordsConfig
|
|
18
|
+
): Promise<AtProtoRecord<T>[]> {
|
|
19
|
+
const { repo, collection, limit = 100, fetchFn } = config;
|
|
20
|
+
const allRecords: AtProtoRecord<T>[] = [];
|
|
21
|
+
let cursor: string | undefined;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
do {
|
|
25
|
+
const records = await withFallback(
|
|
26
|
+
repo,
|
|
27
|
+
async (agent) => {
|
|
28
|
+
const response = await agent.com.atproto.repo.listRecords({
|
|
29
|
+
repo,
|
|
30
|
+
collection,
|
|
31
|
+
limit,
|
|
32
|
+
cursor
|
|
33
|
+
});
|
|
34
|
+
cursor = response.data.cursor;
|
|
35
|
+
return response.data.records;
|
|
36
|
+
},
|
|
37
|
+
true,
|
|
38
|
+
fetchFn
|
|
39
|
+
);
|
|
40
|
+
allRecords.push(...(records as AtProtoRecord<T>[]));
|
|
41
|
+
} while (cursor);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn(`Failed to fetch records from ${collection}:`, error);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return allRecords;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function fetchAllUserRecords<T = any>(
|
|
51
|
+
did: string,
|
|
52
|
+
collection: string,
|
|
53
|
+
fetchFn?: typeof fetch,
|
|
54
|
+
limit?: number
|
|
55
|
+
): Promise<AtProtoRecord<T>[]> {
|
|
56
|
+
return fetchAllRecords<T>({ repo: did, collection, limit, fetchFn });
|
|
57
|
+
}
|
package/src/posts.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { cache } from './cache.js';
|
|
2
|
+
import { withFallback } from './agents.js';
|
|
3
|
+
import type { BlueskyPost, PostAuthor, ExternalLink } from './types.js';
|
|
4
|
+
|
|
5
|
+
export async function fetchLatestBlueskyPost(
|
|
6
|
+
did: string,
|
|
7
|
+
fetchFn?: typeof fetch
|
|
8
|
+
): Promise<BlueskyPost | null> {
|
|
9
|
+
const cacheKey = `blueskypost:latest:${did}`;
|
|
10
|
+
const cached = cache.get<BlueskyPost>(cacheKey);
|
|
11
|
+
if (cached) return cached;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const feedResponse = await withFallback(
|
|
15
|
+
did,
|
|
16
|
+
async (agent) => agent.getAuthorFeed({ actor: did, limit: 5 }),
|
|
17
|
+
false,
|
|
18
|
+
fetchFn
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const feed = feedResponse.data.feed;
|
|
22
|
+
if (!feed.length) return null;
|
|
23
|
+
|
|
24
|
+
const latestFeedItem = feed[0];
|
|
25
|
+
const latestPostData = latestFeedItem.post;
|
|
26
|
+
|
|
27
|
+
const isRepost = latestFeedItem.reason?.$type === 'app.bsky.feed.defs#reasonRepost';
|
|
28
|
+
let repostAuthor: PostAuthor | undefined;
|
|
29
|
+
let repostCreatedAt: string | undefined;
|
|
30
|
+
|
|
31
|
+
if (isRepost && latestFeedItem.reason) {
|
|
32
|
+
const reason = latestFeedItem.reason as any;
|
|
33
|
+
repostAuthor = {
|
|
34
|
+
did: reason.by.did,
|
|
35
|
+
handle: reason.by.handle,
|
|
36
|
+
displayName: reason.by.displayName,
|
|
37
|
+
avatar: reason.by.avatar
|
|
38
|
+
};
|
|
39
|
+
repostCreatedAt = reason.indexedAt;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const post = await fetchPostFromUri(did, latestPostData.uri, 0, fetchFn);
|
|
43
|
+
if (!post) return null;
|
|
44
|
+
|
|
45
|
+
if (isRepost) {
|
|
46
|
+
post.isRepost = true;
|
|
47
|
+
post.repostAuthor = repostAuthor;
|
|
48
|
+
post.repostCreatedAt = repostCreatedAt;
|
|
49
|
+
post.originalPost = { ...post };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
cache.set(cacheKey, post);
|
|
53
|
+
return post;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function fetchPostFromUri(
|
|
60
|
+
did: string,
|
|
61
|
+
uri: string,
|
|
62
|
+
depth: number,
|
|
63
|
+
fetchFn?: typeof fetch
|
|
64
|
+
): Promise<BlueskyPost | null> {
|
|
65
|
+
if (depth >= 3) return null;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const threadResponse = await withFallback(
|
|
69
|
+
did,
|
|
70
|
+
async (agent) => agent.getPostThread({ uri, depth: 0 }),
|
|
71
|
+
false,
|
|
72
|
+
fetchFn
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (!threadResponse.data.thread || !('post' in threadResponse.data.thread)) return null;
|
|
76
|
+
|
|
77
|
+
const postData = threadResponse.data.thread.post;
|
|
78
|
+
const value = postData.record as any;
|
|
79
|
+
const embed = (postData as any).embed ?? null;
|
|
80
|
+
|
|
81
|
+
const author: PostAuthor = {
|
|
82
|
+
did: postData.author.did,
|
|
83
|
+
handle: postData.author.handle,
|
|
84
|
+
displayName: postData.author.displayName,
|
|
85
|
+
avatar: postData.author.avatar
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
let imageUrls: string[] | undefined;
|
|
89
|
+
let imageAlts: string[] | undefined;
|
|
90
|
+
let hasImages = false;
|
|
91
|
+
let hasVideo = false;
|
|
92
|
+
let videoUrl: string | undefined;
|
|
93
|
+
let videoThumbnail: string | undefined;
|
|
94
|
+
let quotedPost: BlueskyPost | undefined;
|
|
95
|
+
let quotedPostUri: string | undefined;
|
|
96
|
+
let externalLink: ExternalLink | undefined;
|
|
97
|
+
|
|
98
|
+
const facets = value.facets;
|
|
99
|
+
|
|
100
|
+
if (embed?.$type === 'app.bsky.embed.images#view' && Array.isArray(embed.images)) {
|
|
101
|
+
hasImages = true;
|
|
102
|
+
imageUrls = [];
|
|
103
|
+
imageAlts = [];
|
|
104
|
+
for (const img of embed.images) {
|
|
105
|
+
const imageUrl = img.fullsize || img.thumb;
|
|
106
|
+
if (imageUrl) { imageUrls.push(imageUrl); imageAlts.push(img.alt || ''); }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (embed?.$type === 'app.bsky.embed.video#view') {
|
|
111
|
+
if (embed.playlist) { hasVideo = true; videoUrl = embed.playlist; }
|
|
112
|
+
if (embed.thumbnail) videoThumbnail = embed.thumbnail;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (embed?.$type === 'app.bsky.embed.external#view' && embed.external) {
|
|
116
|
+
externalLink = {
|
|
117
|
+
uri: embed.external.uri,
|
|
118
|
+
title: embed.external.title,
|
|
119
|
+
description: embed.external.description,
|
|
120
|
+
thumb: embed.external.thumb
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (embed?.$type === 'app.bsky.embed.recordWithMedia#view') {
|
|
125
|
+
const media = embed.media;
|
|
126
|
+
if (media?.$type === 'app.bsky.embed.images#view' && Array.isArray(media.images)) {
|
|
127
|
+
hasImages = true;
|
|
128
|
+
imageUrls = [];
|
|
129
|
+
imageAlts = [];
|
|
130
|
+
for (const img of media.images) {
|
|
131
|
+
const imageUrl = img.fullsize || img.thumb;
|
|
132
|
+
if (imageUrl) { imageUrls.push(imageUrl); imageAlts.push(img.alt || ''); }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (media?.$type === 'app.bsky.embed.video#view') {
|
|
136
|
+
if (media.playlist) { hasVideo = true; videoUrl = media.playlist; }
|
|
137
|
+
if (media.thumbnail) videoThumbnail = media.thumbnail;
|
|
138
|
+
}
|
|
139
|
+
if (media?.$type === 'app.bsky.embed.external#view' && media.external) {
|
|
140
|
+
externalLink = {
|
|
141
|
+
uri: media.external.uri,
|
|
142
|
+
title: media.external.title,
|
|
143
|
+
description: media.external.description,
|
|
144
|
+
thumb: media.external.thumb
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const quotedRecord = embed.record?.record || embed.record;
|
|
148
|
+
if (quotedRecord?.uri) {
|
|
149
|
+
quotedPostUri = quotedRecord.uri as string;
|
|
150
|
+
quotedPost = (await fetchPostFromUri(did, quotedPostUri, depth + 1, fetchFn)) ?? undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (embed?.$type === 'app.bsky.embed.record#view') {
|
|
155
|
+
const quotedRecord = embed.record?.record || embed.record;
|
|
156
|
+
if (quotedRecord?.uri) {
|
|
157
|
+
quotedPostUri = quotedRecord.uri as string;
|
|
158
|
+
quotedPost = (await fetchPostFromUri(did, quotedPostUri, depth + 1, fetchFn)) ?? undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let replyParent: BlueskyPost | undefined;
|
|
163
|
+
let replyRoot: BlueskyPost | undefined;
|
|
164
|
+
if (value.reply) {
|
|
165
|
+
if (value.reply.parent?.uri) {
|
|
166
|
+
replyParent = (await fetchPostFromUri(did, value.reply.parent.uri as string, depth + 1, fetchFn)) ?? undefined;
|
|
167
|
+
}
|
|
168
|
+
if (value.reply.root?.uri && value.reply.root.uri !== value.reply.parent?.uri) {
|
|
169
|
+
replyRoot = (await fetchPostFromUri(did, value.reply.root.uri as string, depth + 1, fetchFn)) ?? undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
text: value.text,
|
|
175
|
+
createdAt: value.createdAt,
|
|
176
|
+
uri: postData.uri,
|
|
177
|
+
author,
|
|
178
|
+
likeCount: postData.likeCount || 0,
|
|
179
|
+
repostCount: postData.repostCount || 0,
|
|
180
|
+
replyCount: postData.replyCount,
|
|
181
|
+
hasImages,
|
|
182
|
+
imageUrls,
|
|
183
|
+
imageAlts,
|
|
184
|
+
hasVideo,
|
|
185
|
+
videoUrl,
|
|
186
|
+
videoThumbnail,
|
|
187
|
+
quotedPostUri,
|
|
188
|
+
quotedPost,
|
|
189
|
+
facets,
|
|
190
|
+
externalLink,
|
|
191
|
+
replyParent,
|
|
192
|
+
replyRoot
|
|
193
|
+
};
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for AT Protocol services
|
|
3
|
+
* Identical to src/lib/services/atproto/types.ts — no SvelteKit dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ProfileData {
|
|
7
|
+
did: string;
|
|
8
|
+
handle: string;
|
|
9
|
+
displayName?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
avatar?: string;
|
|
12
|
+
banner?: string;
|
|
13
|
+
followersCount?: number;
|
|
14
|
+
followsCount?: number;
|
|
15
|
+
postsCount?: number;
|
|
16
|
+
pronouns?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StatusData {
|
|
20
|
+
text: string;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Technology {
|
|
25
|
+
name: string;
|
|
26
|
+
url?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface License {
|
|
31
|
+
name: string;
|
|
32
|
+
url?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BasedOnItem {
|
|
36
|
+
section?: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
url?: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
type?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RelatedService {
|
|
44
|
+
section?: string;
|
|
45
|
+
name?: string;
|
|
46
|
+
url?: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
relationship?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface Repository {
|
|
52
|
+
platform?: string;
|
|
53
|
+
url: string;
|
|
54
|
+
type?: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Credit {
|
|
59
|
+
section?: string;
|
|
60
|
+
name?: string;
|
|
61
|
+
type: string;
|
|
62
|
+
url?: string;
|
|
63
|
+
author?: string;
|
|
64
|
+
license?: License;
|
|
65
|
+
description?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SectionLicense {
|
|
69
|
+
section?: string;
|
|
70
|
+
name?: string;
|
|
71
|
+
url?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SiteInfoData {
|
|
75
|
+
technologyStack?: Technology[];
|
|
76
|
+
privacyStatement?: string;
|
|
77
|
+
openSourceInfo?: {
|
|
78
|
+
description?: string;
|
|
79
|
+
license?: License;
|
|
80
|
+
basedOn?: BasedOnItem[];
|
|
81
|
+
relatedServices?: RelatedService[];
|
|
82
|
+
repositories?: Repository[];
|
|
83
|
+
};
|
|
84
|
+
credits?: Credit[];
|
|
85
|
+
additionalInfo?: {
|
|
86
|
+
websiteBirthYear?: number;
|
|
87
|
+
purpose?: string;
|
|
88
|
+
sectionLicense?: SectionLicense[];
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface LinkCard {
|
|
93
|
+
url: string;
|
|
94
|
+
text: string;
|
|
95
|
+
emoji: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface LinkData {
|
|
99
|
+
cards: LinkCard[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface BlogPost {
|
|
103
|
+
title: string;
|
|
104
|
+
url: string;
|
|
105
|
+
createdAt: string;
|
|
106
|
+
platform: 'standard.site';
|
|
107
|
+
description?: string;
|
|
108
|
+
rkey: string;
|
|
109
|
+
publicationName?: string;
|
|
110
|
+
publicationRkey?: string;
|
|
111
|
+
tags?: string[];
|
|
112
|
+
coverImage?: string;
|
|
113
|
+
textContent?: string;
|
|
114
|
+
updatedAt?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface BlogPostsData {
|
|
118
|
+
posts: BlogPost[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface Facet {
|
|
122
|
+
index: { byteStart: number; byteEnd: number };
|
|
123
|
+
features: Array<{ $type: string; uri?: string; did?: string; tag?: string }>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ExternalLink {
|
|
127
|
+
uri: string;
|
|
128
|
+
title: string;
|
|
129
|
+
description?: string;
|
|
130
|
+
thumb?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface PostAuthor {
|
|
134
|
+
did: string;
|
|
135
|
+
handle: string;
|
|
136
|
+
displayName?: string;
|
|
137
|
+
avatar?: string;
|
|
138
|
+
pronouns?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface BlueskyPost {
|
|
142
|
+
text: string;
|
|
143
|
+
createdAt: string;
|
|
144
|
+
uri: string;
|
|
145
|
+
author: PostAuthor;
|
|
146
|
+
likeCount?: number;
|
|
147
|
+
repostCount?: number;
|
|
148
|
+
replyCount?: number;
|
|
149
|
+
hasImages: boolean;
|
|
150
|
+
imageUrls?: string[];
|
|
151
|
+
imageAlts?: string[];
|
|
152
|
+
hasVideo?: boolean;
|
|
153
|
+
videoUrl?: string;
|
|
154
|
+
videoThumbnail?: string;
|
|
155
|
+
quotedPostUri?: string;
|
|
156
|
+
quotedPost?: BlueskyPost;
|
|
157
|
+
facets?: Facet[];
|
|
158
|
+
externalLink?: ExternalLink;
|
|
159
|
+
replyParent?: BlueskyPost;
|
|
160
|
+
replyRoot?: BlueskyPost;
|
|
161
|
+
isRepost?: boolean;
|
|
162
|
+
repostAuthor?: PostAuthor;
|
|
163
|
+
repostCreatedAt?: string;
|
|
164
|
+
originalPost?: BlueskyPost;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface ResolvedIdentity {
|
|
168
|
+
did: string;
|
|
169
|
+
pds: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface CacheEntry<T> {
|
|
173
|
+
data: T;
|
|
174
|
+
timestamp: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface MusicArtist {
|
|
178
|
+
artistName: string;
|
|
179
|
+
artistMbId?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface MusicStatusData {
|
|
183
|
+
trackName: string;
|
|
184
|
+
artists: MusicArtist[];
|
|
185
|
+
releaseName?: string;
|
|
186
|
+
playedTime: string;
|
|
187
|
+
originUrl?: string;
|
|
188
|
+
recordingMbId?: string;
|
|
189
|
+
releaseMbId?: string;
|
|
190
|
+
isrc?: string;
|
|
191
|
+
duration?: number;
|
|
192
|
+
musicServiceBaseDomain?: string;
|
|
193
|
+
submissionClientAgent?: string;
|
|
194
|
+
$type: 'fm.teal.alpha.actor.status' | 'fm.teal.alpha.feed.play';
|
|
195
|
+
expiry?: string;
|
|
196
|
+
artwork?: { ref?: { $link: string }; mimeType?: string; size?: number };
|
|
197
|
+
artworkUrl?: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface KibunStatusData {
|
|
201
|
+
text: string;
|
|
202
|
+
emoji: string;
|
|
203
|
+
createdAt: string;
|
|
204
|
+
$type: 'social.kibun.status';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface TangledRepo {
|
|
208
|
+
uri: string;
|
|
209
|
+
name: string;
|
|
210
|
+
description?: string;
|
|
211
|
+
knot: string;
|
|
212
|
+
createdAt: string;
|
|
213
|
+
labels?: string[];
|
|
214
|
+
source?: string;
|
|
215
|
+
spindle?: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface TangledReposData {
|
|
219
|
+
repos: TangledRepo[];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface StandardSiteThemeColor {
|
|
223
|
+
r: number;
|
|
224
|
+
g: number;
|
|
225
|
+
b: number;
|
|
226
|
+
a?: number;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface StandardSiteBasicTheme {
|
|
230
|
+
background: StandardSiteThemeColor;
|
|
231
|
+
foreground: StandardSiteThemeColor;
|
|
232
|
+
accent: StandardSiteThemeColor;
|
|
233
|
+
accentForeground: StandardSiteThemeColor;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface StandardSitePublication {
|
|
237
|
+
name: string;
|
|
238
|
+
rkey: string;
|
|
239
|
+
uri: string;
|
|
240
|
+
url: string;
|
|
241
|
+
description?: string;
|
|
242
|
+
icon?: string;
|
|
243
|
+
basicTheme?: StandardSiteBasicTheme;
|
|
244
|
+
preferences?: { showInDiscover?: boolean };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface StandardSitePublicationsData {
|
|
248
|
+
publications: StandardSitePublication[];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface StandardSiteDocument {
|
|
252
|
+
title: string;
|
|
253
|
+
rkey: string;
|
|
254
|
+
uri: string;
|
|
255
|
+
url: string;
|
|
256
|
+
site: string;
|
|
257
|
+
path?: string;
|
|
258
|
+
description?: string;
|
|
259
|
+
coverImage?: string;
|
|
260
|
+
content?: any;
|
|
261
|
+
textContent?: string;
|
|
262
|
+
bskyPostRef?: { uri: string; cid: string };
|
|
263
|
+
tags?: string[];
|
|
264
|
+
publishedAt: string;
|
|
265
|
+
updatedAt?: string;
|
|
266
|
+
publicationName?: string;
|
|
267
|
+
publicationRkey?: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface StandardSiteDocumentsData {
|
|
271
|
+
documents: StandardSiteDocument[];
|
|
272
|
+
}
|