appview-less bluesky client
24
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 0eaf2dff795c9df008f40a99ca4e2714395bd79d 148 lines 4.7 kB view raw
1import { 2 parseCanonicalResourceUri, 3 type CanonicalResourceUri, 4 type Cid, 5 type ResourceUri 6} from '@atcute/lexicons'; 7import { type AtpClient } from './client.svelte'; 8import { err, expect, ok, type Ok, type Result } from '$lib/result'; 9import type { Backlinks } from './constellation'; 10import { AppBskyFeedPost } from '@atcute/bluesky'; 11import type { Did, RecordKey } from '@atcute/lexicons/syntax'; 12import { replySource, toCanonicalUri } from '$lib'; 13 14export type PostWithUri = { uri: ResourceUri; cid: Cid | undefined; record: AppBskyFeedPost.Main }; 15export type PostWithBacklinks = PostWithUri & { 16 replies?: Backlinks; 17}; 18 19export const fetchPosts = async ( 20 subject: Did, 21 client: AtpClient, 22 cursor?: string, 23 limit?: number, 24 withBacklinks: boolean = true 25): Promise<Result<{ posts: PostWithBacklinks[]; cursor?: string }, string>> => { 26 const recordsList = await client.listRecords(subject, 'app.bsky.feed.post', cursor, limit); 27 if (!recordsList.ok) return err(`can't retrieve posts: ${recordsList.error}`); 28 cursor = recordsList.value.cursor; 29 const records = recordsList.value.records; 30 31 if (!withBacklinks) { 32 return ok({ 33 posts: records.map((r) => ({ 34 uri: r.uri, 35 cid: r.cid, 36 record: r.value as AppBskyFeedPost.Main 37 })), 38 cursor 39 }); 40 } 41 42 try { 43 const allBacklinks = await Promise.all( 44 records.map(async (r): Promise<PostWithBacklinks> => { 45 const result = await client.getBacklinks(r.uri, replySource); 46 if (!result.ok) throw `cant fetch replies: ${result.error}`; 47 const replies = result.value; 48 return { 49 uri: r.uri, 50 cid: r.cid, 51 record: r.value as AppBskyFeedPost.Main, 52 replies 53 }; 54 }) 55 ); 56 return ok({ posts: allBacklinks, cursor }); 57 } catch (error) { 58 return err(`cant fetch posts backlinks: ${error}`); 59 } 60}; 61 62export const hydratePosts = async ( 63 client: AtpClient, 64 repo: Did, 65 data: PostWithBacklinks[], 66 cacheFn: (did: Did, rkey: RecordKey) => Ok<PostWithUri> | undefined 67): Promise<Result<Map<ResourceUri, PostWithUri>, string>> => { 68 let posts: Map<ResourceUri, PostWithUri> = new Map(); 69 try { 70 const allPosts = await Promise.all( 71 data.map(async (post) => { 72 const result: PostWithUri[] = [post]; 73 if (post.replies) { 74 const replies = await Promise.all( 75 post.replies.records.map(async (r) => { 76 const reply = 77 cacheFn(r.did, r.rkey) ?? 78 (await client.getRecord(AppBskyFeedPost.mainSchema, r.did, r.rkey)); 79 if (!reply.ok) throw `cant fetch reply: ${reply.error}`; 80 return reply.value; 81 }) 82 ); 83 result.push(...replies); 84 } 85 return result; 86 }) 87 ); 88 posts = new Map(allPosts.flat().map((post) => [post.uri, post])); 89 } catch (error) { 90 return err(`cant hydrate immediate replies: ${error}`); 91 } 92 93 const fetchUpwardsChain = async (post: PostWithUri) => { 94 let parent = post.record.reply?.parent; 95 while (parent) { 96 const parentUri = parent.uri as CanonicalResourceUri; 97 // if we already have this parent, then we already fetched this chain / are fetching it 98 if (posts.has(parentUri)) return; 99 const parsedParentUri = expect(parseCanonicalResourceUri(parentUri)); 100 const p = 101 cacheFn(parsedParentUri.repo, parsedParentUri.rkey) ?? 102 (await client.getRecord( 103 AppBskyFeedPost.mainSchema, 104 parsedParentUri.repo, 105 parsedParentUri.rkey 106 )); 107 if (p.ok) { 108 posts.set(p.value.uri, p.value); 109 parent = p.value.record.reply?.parent; 110 continue; 111 } 112 // TODO: handle deleted parent posts 113 parent = undefined; 114 } 115 }; 116 await Promise.all(posts.values().map(fetchUpwardsChain)); 117 118 try { 119 const fetchDownwardsChain = async (post: PostWithUri) => { 120 const { repo: postRepo } = expect(parseCanonicalResourceUri(post.uri)); 121 if (repo === postRepo) return; 122 123 // get chains that are the same author until we exhaust them 124 const backlinks = await client.getBacklinks(post.uri, replySource); 125 if (!backlinks.ok) return; 126 127 const promises = []; 128 for (const reply of backlinks.value.records) { 129 if (reply.did !== postRepo) continue; 130 // if we already have this reply, then we already fetched this chain / are fetching it 131 if (posts.has(toCanonicalUri(reply))) continue; 132 const record = 133 cacheFn(reply.did, reply.rkey) ?? 134 (await client.getRecord(AppBskyFeedPost.mainSchema, reply.did, reply.rkey)); 135 if (!record.ok) break; // TODO: this doesnt handle deleted posts in between 136 posts.set(record.value.uri, record.value); 137 promises.push(fetchDownwardsChain(record.value)); 138 } 139 140 await Promise.all(promises); 141 }; 142 await Promise.all(posts.values().map(fetchDownwardsChain)); 143 } catch (error) { 144 return err(`cant fetch post reply chain: ${error}`); 145 } 146 147 return ok(posts); 148};