forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {AppBskyGraphVerification, AtUri} from '@atproto/api'
2import {
3 type VerificationState,
4 type VerificationView,
5} from '@atproto/api/dist/client/types/app/bsky/actor/defs'
6import {useQuery} from '@tanstack/react-query'
7
8import {STALE} from '#/state/queries'
9import * as bsky from '#/types/bsky'
10import {type AnyProfileView} from '#/types/bsky/profile'
11import {useConstellationInstance} from '../preferences/constellation-instance'
12import {
13 useDeerVerificationEnabled,
14 useDeerVerificationTrusted,
15} from '../preferences/deer-verification'
16import {
17 asUri,
18 asyncGenCollect,
19 asyncGenDedupe,
20 asyncGenFilter,
21 asyncGenTryMap,
22 type ConstellationLink,
23 constellationLinks,
24} from './constellation'
25import {LRU} from './direct-fetch-record'
26import {resolvePdsServiceUrl} from './resolve-identity'
27import {useCurrentAccountProfile} from './useCurrentAccountProfile'
28
29const RQKEY_ROOT = 'deer-verification'
30export const RQKEY = (did: string, trusted: Set<string>) => [
31 RQKEY_ROOT,
32 did,
33 Array.from(trusted).sort(),
34]
35
36type LinkedRecord = {
37 link: ConstellationLink
38 record: AppBskyGraphVerification.Record
39}
40
41const verificationCache = new LRU<string, any>()
42
43export function getTrustedConstellationVerifications(
44 instance: string,
45 did: string,
46 trusted: Set<string>,
47) {
48 const urip = new AtUri(did)
49 const verificationLinks = constellationLinks(instance, {
50 target: urip.host,
51 collection: 'app.bsky.graph.verification',
52 path: '.subject',
53 from_dids: Array.from(trusted),
54 })
55 return asyncGenDedupe(
56 asyncGenFilter(verificationLinks, ({did}) => trusted.has(did)),
57 ({did}) => did,
58 )
59}
60
61async function getDeerVerificationLinkedRecords(
62 instance: string,
63 did: string,
64 trusted: Set<string>,
65): Promise<LinkedRecord[] | undefined> {
66 try {
67 const trustedVerificationLinks = getTrustedConstellationVerifications(
68 instance,
69 did,
70 trusted,
71 )
72
73 const verificationRecords = asyncGenFilter(
74 asyncGenTryMap<ConstellationLink, {link: ConstellationLink; record: any}>(
75 trustedVerificationLinks,
76 // using try map lets us:
77 // - cache the service url and verificatin record in independent lrus
78 // - clear the promise from the lru on failure
79 // - skip links that cause errors
80 async link => {
81 const {did, rkey} = link
82
83 let service = await resolvePdsServiceUrl(did)
84
85 const request = `${service}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=app.bsky.graph.verification&rkey=${rkey}`
86 const record = await verificationCache.getOrTryInsertWith(
87 request,
88 async () => {
89 const resp = await (await fetch(request)).json()
90 return resp.value
91 },
92 )
93 return {link, record}
94 },
95 (_, e) => {
96 console.error(e)
97 },
98 ),
99 // the explicit return type shouldn't be needed...
100 (d: {link: ConstellationLink; record: unknown}): d is LinkedRecord =>
101 bsky.validate<AppBskyGraphVerification.Record>(
102 d.record,
103 AppBskyGraphVerification.validateRecord,
104 ),
105 )
106
107 // Array.fromAsync will do this but not available everywhere yet
108 return asyncGenCollect(verificationRecords)
109 } catch (e) {
110 console.error(e)
111 return undefined
112 }
113}
114
115function createVerificationViews(
116 linkedRecords: LinkedRecord[],
117 profile: AnyProfileView,
118): VerificationView[] {
119 return linkedRecords.map(({link, record}) => ({
120 issuer: link.did,
121 isValid:
122 (profile.displayName ?? '') === record.displayName &&
123 profile.handle === record.handle,
124 createdAt: record.createdAt,
125 uri: asUri(link),
126 }))
127}
128
129function createVerificationState(
130 verifications: VerificationView[],
131 profile: AnyProfileView,
132 trusted: Set<string>,
133): VerificationState {
134 return {
135 verifications,
136 verifiedStatus:
137 verifications.length > 0
138 ? verifications.findIndex(v => v.isValid) !== -1
139 ? 'valid'
140 : 'invalid'
141 : 'none',
142 trustedVerifierStatus: trusted.has(profile.did) ? 'valid' : 'none',
143 }
144}
145
146export function useDeerVerificationState({
147 profile,
148 enabled,
149}: {
150 profile: AnyProfileView | undefined
151 enabled?: boolean
152}) {
153 const instance = useConstellationInstance()
154 const currentAccountProfile = useCurrentAccountProfile()
155 const trusted = useDeerVerificationTrusted(currentAccountProfile?.did)
156
157 const linkedRecords = useQuery<LinkedRecord[] | undefined>({
158 staleTime: STALE.HOURS.ONE,
159 queryKey: RQKEY(profile?.did || '', trusted),
160 async queryFn() {
161 if (!profile) return undefined
162
163 return await getDeerVerificationLinkedRecords(
164 instance,
165 profile.did,
166 trusted,
167 )
168 },
169 enabled: enabled && profile !== undefined,
170 })
171
172 if (linkedRecords.data === undefined || profile === undefined) return
173 const verifications = createVerificationViews(linkedRecords.data, profile)
174 const verificationState = createVerificationState(
175 verifications,
176 profile,
177 trusted,
178 )
179
180 return verificationState
181}
182
183export function useDeerVerificationProfileOverlay<V extends AnyProfileView>(
184 profile: V,
185): V {
186 const enabled = useDeerVerificationEnabled()
187 const verificationState = useDeerVerificationState({
188 profile,
189 enabled,
190 })
191
192 return enabled
193 ? {
194 ...profile,
195 verification: verificationState,
196 }
197 : profile
198}
199
200export function useMaybeDeerVerificationProfileOverlay<
201 V extends AnyProfileView,
202>(profile: V | undefined): V | undefined {
203 const enabled = useDeerVerificationEnabled()
204 const verificationState = useDeerVerificationState({
205 profile,
206 enabled,
207 })
208
209 if (!profile) return undefined
210
211 return enabled
212 ? {
213 ...profile,
214 verification: verificationState,
215 }
216 : profile
217}