Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at feat/custom-appview 215 lines 5.9 kB view raw
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}