forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}