Bluesky app fork with some witchin' additions 馃挮
at feat/tealfm 200 lines 4.7 kB view raw
1export type ConstellationLink = { 2 did: `did:${string}:${string}` 3 collection: string 4 rkey: string 5} 6 7type Collection = 8 | 'app.bsky.actor.profile' 9 | 'app.bsky.feed.generator' 10 | 'app.bsky.feed.like' 11 | 'app.bsky.feed.post' 12 | 'app.bsky.feed.repost' 13 | 'app.bsky.feed.threadgate' 14 | 'app.bsky.graph.block' 15 | 'app.bsky.graph.follow' 16 | 'app.bsky.graph.list' 17 | 'app.bsky.graph.listblock' 18 | 'app.bsky.graph.listitem' 19 | 'app.bsky.graph.starterpack' 20 | 'app.bsky.graph.verification' 21 | 'chat.bsky.actor.declaration' 22 23const headers = new Headers({ 24 Accept: 'application/json', 25 'User-Agent': 'Witchsky (witchsky.app)', 26}) 27 28const makeReqUrl = ( 29 instance: string, 30 route: string, 31 params: Record<string, string | string[]>, 32) => { 33 const url = new URL(instance) 34 url.pathname = route 35 for (const [k, v] of Object.entries(params)) { 36 // NOTE: in the future this should probably be a repeated param... 37 if (Array.isArray(v)) { 38 url.searchParams.set(k, v.join(',')) 39 } else { 40 url.searchParams.set(k, v) 41 } 42 } 43 return url 44} 45 46// using an async generator lets us kick off dependent requests before finishing pagination 47// this doesn't solve the gross N+1 queries thing going on here to get records, but it should make it faster :3 48export async function* constellationLinks( 49 instance: string, 50 params: { 51 target: string 52 collection: Collection 53 path: string 54 from_dids?: string[] 55 }, 56) { 57 const url = makeReqUrl(instance, 'links', params) 58 59 const req = async () => 60 (await (await fetch(url, {method: 'GET', headers})).json()) as { 61 total: number 62 linking_records: ConstellationLink[] 63 cursor: string | null 64 } 65 66 let cursor: string | null = null 67 while (true) { 68 const resp = await req() 69 70 for (const link of resp.linking_records) { 71 yield link 72 } 73 74 cursor = resp.cursor 75 if (cursor === null) break 76 url.searchParams.set('cursor', cursor) 77 } 78} 79 80export async function constellationCounts( 81 instance: string, 82 params: {target: string}, 83) { 84 const url = makeReqUrl(instance, 'links/all', params) 85 const json = (await (await fetch(url, {method: 'GET', headers})).json()) as { 86 links: { 87 [P in Collection]?: { 88 [k: string]: {distinct_dids: number; records: number} | undefined 89 } 90 } 91 } 92 const links = json.links 93 return { 94 likeCount: 95 links?.['app.bsky.feed.like']?.['.subject.uri']?.distinct_dids ?? 0, 96 repostCount: 97 links?.['app.bsky.feed.repost']?.['.subject.uri']?.distinct_dids ?? 0, 98 replyCount: 99 links?.['app.bsky.feed.post']?.['.reply.parent.uri']?.records ?? 0, 100 } 101} 102 103export function asUri(link: ConstellationLink): string { 104 return `at://${link.did}/${link.collection}/${link.rkey}` 105} 106 107export async function* asyncGenMap<K, V>( 108 gen: AsyncGenerator<K, void, unknown>, 109 fn: (val: K) => V, 110) { 111 for await (const v of gen) { 112 yield fn(v) 113 } 114} 115 116export async function* asyncGenTryMap<K, V>( 117 gen: AsyncGenerator<K, void, unknown>, 118 fn: (val: K) => Promise<V>, 119 err: (val: K, e: unknown) => void, 120) { 121 for await (const v of gen) { 122 try { 123 // make sure we resolve inside the try catch 124 yield await fn(v) 125 } catch (e) { 126 err(v, e) 127 } 128 } 129} 130 131export function asyncGenFilter<K, V extends K>( 132 gen: AsyncGenerator<K, void, unknown>, 133 predicate: (item: K) => item is V, 134): AsyncGenerator<Awaited<V>, void, unknown> 135 136export function asyncGenFilter<K>( 137 gen: AsyncGenerator<K, void, unknown>, 138 predicate: (item: K) => boolean, 139): AsyncGenerator<Awaited<K>, void, unknown> 140 141export async function* asyncGenFilter<K>( 142 gen: AsyncGenerator<K, void, unknown>, 143 predicate: (item: K) => boolean, 144) { 145 for await (const v of gen) { 146 if (predicate(v)) yield v 147 } 148} 149 150export async function* asyncGenTake<V>( 151 gen: AsyncGenerator<V, void, unknown>, 152 n: number, 153) { 154 if (n <= 0) return 155 156 let taken = 0 157 for await (const v of gen) { 158 yield v 159 if (++taken >= n) break 160 } 161} 162 163export async function* asyncGenDedupe<V, K>( 164 gen: AsyncGenerator<V, void, unknown>, 165 keyFn: (_: V) => K, 166) { 167 const seen = new Set<K>() 168 for await (const v of gen) { 169 const key = keyFn(v) 170 if (!seen.has(key)) { 171 seen.add(key) 172 yield v 173 } 174 } 175} 176 177export async function asyncGenCollect<V>( 178 gen: AsyncGenerator<V, void, unknown>, 179) { 180 const out = [] 181 for await (const v of gen) { 182 out.push(v) 183 } 184 return out 185} 186 187export async function asyncGenFind<V>( 188 gen: AsyncGenerator<V, void, unknown>, 189 predicate: (item: V) => boolean, 190) { 191 for await (const v of gen) { 192 if (predicate(v)) return v 193 } 194 return undefined 195} 196 197export function dbg<V>(v: V): V { 198 console.log(v) 199 return v 200}