appview-less bluesky client
1import { type ActorIdentifier, type Did, type RecordKey } from '@atcute/lexicons/syntax';
2import { type AtpClient } from './client.svelte';
3import { AppBskyFeedGenerator } from '@atcute/bluesky';
4import { img } from '$lib/cdn';
5import type { Blob as AtprotoBlob } from '@atcute/lexicons';
6
7export type FeedGenerator = {
8 uri: string;
9 displayName: string;
10 description?: string;
11 avatar?: string;
12 did: string;
13};
14
15export function parseFeedUri(uri: string): { repo: ActorIdentifier; rkey: RecordKey } | null {
16 if (uri.startsWith('at://')) {
17 const match = uri.match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.generator\/([^/]+)$/);
18 if (!match) return null;
19 return { repo: match[1] as ActorIdentifier, rkey: match[2] as RecordKey };
20 } else if (uri.startsWith('https://') || uri.startsWith('http://')) {
21 const match = uri.match(/^https?:\/\/(?:[^/]+)\/profile\/([^/]+)\/feed\/([^/]+)$/);
22 if (!match) return null;
23 return { repo: match[1] as ActorIdentifier, rkey: match[2] as RecordKey };
24 }
25 return null;
26}
27
28export async function fetchFeedGenerator(client: AtpClient, uri: string): Promise<FeedGenerator | null> {
29 const parsed = parseFeedUri(uri);
30 if (!parsed) return null;
31
32 try {
33 const response = await client.getRecord(AppBskyFeedGenerator.mainSchema, parsed.repo, parsed.rkey);
34 if (!response.ok) return null;
35
36 const value = response.value.record;
37 const did = response.value.uri.split('/')[2] as Did;
38 const avatar = value.avatar ? img('avatar_thumbnail', did, (value.avatar as AtprotoBlob<string>).ref.$link, 'webp') : undefined;
39
40 return {
41 uri: response.value.uri,
42 displayName: value.displayName,
43 description: value.description,
44 did: value.did,
45 avatar,
46 };
47 } catch {
48 return null;
49 }
50}
51
52export type FeedSkeletonItem = {
53 post: string;
54 reason?: { $type: string; repost?: string };
55};
56
57export type FeedSkeleton = {
58 feed: FeedSkeletonItem[];
59 cursor?: string;
60};
61
62export async function fetchFeedSkeleton(
63 client: AtpClient,
64 feedUri: string,
65 feedServiceDid: string,
66 cursor?: string,
67 limit: number = 25
68): Promise<FeedSkeleton | null> {
69 const auth = client.user;
70 if (!auth) return null;
71
72 const cursorParam = cursor ? `&cursor=${encodeURIComponent(cursor)}` : '';
73 const url = `/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}&limit=${limit}${cursorParam}`;
74
75 try {
76 const response = await auth.atcute.handler(url, {
77 method: 'GET',
78 headers: {
79 'atproto-proxy': `${feedServiceDid}#bsky_fg`
80 }
81 });
82
83 if (!response.ok) return null;
84 return (await response.json()) as FeedSkeleton;
85 } catch {
86 return null;
87 }
88}
89