Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
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, (link) => trusted.has(link.did)),
57 (link) => link.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 let service = await resolvePdsServiceUrl(link.did)
82
83 const request = `${service}/xrpc/com.atproto.repo.getRecord?repo=${link.did}&collection=app.bsky.graph.verification&rkey=${link.rkey}`
84 const record = await verificationCache.getOrTryInsertWith(
85 request,
86 async () => {
87 const resp = await (await fetch(request)).json()
88 return resp.value
89 },
90 )
91 return {link, record}
92 },
93 (_, e) => {
94 console.error(e)
95 },
96 ),
97 // the explicit return type shouldn't be needed...
98 (d: {link: ConstellationLink; record: unknown}): d is LinkedRecord =>
99 bsky.validate<AppBskyGraphVerification.Record>(
100 d.record,
101 AppBskyGraphVerification.validateRecord,
102 ),
103 )
104
105 // Array.fromAsync will do this but not available everywhere yet
106 return asyncGenCollect(verificationRecords)
107 } catch (e) {
108 console.error(e)
109 return undefined
110 }
111}
112
113function createVerificationViews(
114 linkedRecords: LinkedRecord[],
115 profile: AnyProfileView,
116): VerificationView[] {
117 return linkedRecords.map(({link, record}) => ({
118 issuer: link.did,
119 isValid:
120 (profile.displayName ?? '') === record.displayName &&
121 profile.handle === record.handle,
122 createdAt: record.createdAt,
123 uri: asUri(link),
124 }))
125}
126
127function createVerificationState(
128 verifications: VerificationView[],
129 profile: AnyProfileView,
130 trusted: Set<string>,
131): VerificationState {
132 return {
133 verifications,
134 verifiedStatus:
135 verifications.length > 0
136 ? verifications.findIndex(v => v.isValid) !== -1
137 ? 'valid'
138 : 'invalid'
139 : 'none',
140 trustedVerifierStatus: trusted.has(profile.did) ? 'valid' : 'none',
141 }
142}
143
144export function useDeerVerificationState({
145 profile,
146 enabled,
147}: {
148 profile: AnyProfileView | undefined
149 enabled?: boolean
150}) {
151 const instance = useConstellationInstance()
152 const currentAccountProfile = useCurrentAccountProfile()
153 const trusted = useDeerVerificationTrusted(currentAccountProfile?.did)
154
155 const linkedRecords = useQuery<LinkedRecord[] | undefined>({
156 staleTime: STALE.HOURS.ONE,
157 queryKey: RQKEY(profile?.did || '', trusted),
158 async queryFn() {
159 if (!profile) return undefined
160
161 return await getDeerVerificationLinkedRecords(
162 instance,
163 profile.did,
164 trusted,
165 )
166 },
167 enabled: enabled && profile !== undefined,
168 })
169
170 if (linkedRecords.data === undefined || profile === undefined) return
171 const verifications = createVerificationViews(linkedRecords.data, profile)
172 const verificationState = createVerificationState(
173 verifications,
174 profile,
175 trusted,
176 )
177
178 return verificationState
179}
180
181export function useDeerVerificationProfileOverlay<V extends AnyProfileView>(
182 profile: V,
183): V {
184 const enabled = useDeerVerificationEnabled()
185 const verificationState = useDeerVerificationState({
186 profile,
187 enabled,
188 })
189
190 return enabled
191 ? {
192 ...profile,
193 verification: verificationState,
194 }
195 : profile
196}
197
198export function useMaybeDeerVerificationProfileOverlay<
199 V extends AnyProfileView,
200>(profile: V | undefined): V | undefined {
201 const enabled = useDeerVerificationEnabled()
202 const verificationState = useDeerVerificationState({
203 profile,
204 enabled,
205 })
206
207 if (!profile) return undefined
208
209 return enabled
210 ? {
211 ...profile,
212 verification: verificationState,
213 }
214 : profile
215}