Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at theme-changes 275 lines 6.5 kB view raw
1import {type ComAtprotoIdentityDefs, isDid} from '@atproto/api' 2import { 3 type IdentityInfo, 4 type IdentityResolver, 5} from '@atproto-labs/identity-resolver' 6 7import {getDidDocumentUrl} from '#/lib/atproto/did' 8import {DOH_ENDPOINT} from '#/lib/constants' 9import {readPlcDirectory} from '#/state/preferences/plc-directory' 10import {createPublicAgent} from './agent' 11 12type AtprotoDid = `did:plc:${string}` | `did:web:${string}` 13type DidDocument = { 14 id?: string 15 alsoKnownAs?: string[] 16 service?: Service[] 17} 18 19type Service = { 20 id?: string 21 type?: string 22 serviceEndpoint?: string 23} 24 25const HANDLE_INVALID = 'handle.invalid' 26 27function asNormalizedHandle(input: string) { 28 const handle = input.toLowerCase() 29 return /^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/.test( 30 handle, 31 ) 32 ? handle 33 : undefined 34} 35 36function extractNormalizedHandle(document: DidDocument) { 37 if (!Array.isArray(document.alsoKnownAs)) return 38 39 for (const value of document.alsoKnownAs) { 40 if (value.startsWith('at://')) { 41 return asNormalizedHandle(value.slice(5)) 42 } 43 } 44} 45 46function findService(doc: DidDocument, id: string, type?: string) { 47 if (!Array.isArray(doc?.service)) return 48 return doc.service.find( 49 service => 50 service?.serviceEndpoint && 51 service?.id === id && 52 (!type || service?.type === type), 53 ) 54} 55 56async function resolveHandleUsingAppView( 57 handle: string, 58 signal?: AbortSignal, 59): Promise<AtprotoDid> { 60 const agent = createPublicAgent() 61 62 try { 63 const res = await agent.resolveHandle({handle}, {signal}) 64 return res.data.did as AtprotoDid 65 } finally { 66 agent.dispose() 67 } 68} 69 70async function resolveHandleUsingDoh( 71 handle: string, 72 signal?: AbortSignal, 73): Promise<AtprotoDid | null> { 74 const url = new URL(DOH_ENDPOINT) 75 url.searchParams.set('type', 'TXT') 76 url.searchParams.set('name', `_atproto.${handle}`) 77 78 const response = await fetch(url, { 79 headers: { 80 accept: 'application/dns-json', 81 }, 82 redirect: 'follow', 83 signal, 84 }) 85 86 if (!response.ok) { 87 return null 88 } 89 90 const result = (await response.json()) as { 91 Answer?: Array<{type?: number; data?: string}> 92 } 93 const txtRecords = 94 result.Answer?.filter( 95 answer => answer.type === 16 && typeof answer.data === 'string', 96 ).map(answer => answer.data!.replace(/^"|"$/g, '').replace(/\\"/g, '"')) ?? 97 [] 98 99 let did: AtprotoDid | null = null 100 for (const record of txtRecords) { 101 if (!record.startsWith('did=')) continue 102 103 const nextDid = record.slice(4) 104 if (!isDid(nextDid)) { 105 return null 106 } 107 108 if (did && did !== nextDid) { 109 return null 110 } 111 112 did = nextDid as AtprotoDid 113 } 114 115 return did 116} 117 118async function resolveHandleUsingWellKnown( 119 handle: string, 120 signal?: AbortSignal, 121): Promise<AtprotoDid | null> { 122 try { 123 const response = await fetch(`https://${handle}/.well-known/atproto-did`, { 124 redirect: 'error', 125 signal, 126 }) 127 const text = await response.text() 128 const firstLine = text.split('\n')[0]?.trim() 129 return firstLine && isDid(firstLine) ? (firstLine as AtprotoDid) : null 130 } catch { 131 signal?.throwIfAborted() 132 return null 133 } 134} 135 136async function resolveHandleClientSide( 137 handle: string, 138 signal?: AbortSignal, 139): Promise<AtprotoDid | null> { 140 try { 141 const did = await resolveHandleUsingDoh(handle, signal) 142 if (did) return did 143 } catch { 144 signal?.throwIfAborted() 145 } 146 147 return resolveHandleUsingWellKnown(handle, signal) 148} 149 150async function resolveHandle( 151 handle: string, 152 signal?: AbortSignal, 153): Promise<AtprotoDid> { 154 try { 155 return await resolveHandleUsingAppView(handle, signal) 156 } catch (appViewError) { 157 const fallbackDid = await resolveHandleClientSide(handle, signal) 158 if (fallbackDid) { 159 return fallbackDid 160 } 161 162 throw appViewError 163 } 164} 165 166async function resolveDidDocument( 167 did: AtprotoDid, 168 signal?: AbortSignal, 169): Promise<DidDocument> { 170 const docUrl = getDidDocumentUrl(did, readPlcDirectory()) 171 if (!docUrl) { 172 throw new Error(`Unsupported DID method for ${did}`) 173 } 174 175 try { 176 const res = await fetch(docUrl, { 177 headers: { 178 accept: 'application/did+ld+json, application/json', 179 }, 180 signal, 181 }) 182 183 if (!res.ok) { 184 throw new Error(`Failed to resolve DID document for ${did}`) 185 } 186 187 return (await res.json()) as DidDocument 188 } catch (err) { 189 if (!did.startsWith('did:web:')) { 190 throw err 191 } 192 193 const agent = createPublicAgent() 194 try { 195 const res = await agent.com.atproto.identity.resolveDid({did}, {signal}) 196 return res.data.didDoc as DidDocument 197 } finally { 198 agent.dispose() 199 } 200 } 201} 202 203async function getValidatedHandleFromDidDocument( 204 did: AtprotoDid, 205 didDoc: DidDocument, 206 signal?: AbortSignal, 207) { 208 const handle = extractNormalizedHandle(didDoc) 209 if (!handle) return HANDLE_INVALID 210 211 try { 212 const resolvedDid = await resolveHandle(handle, signal) 213 return resolvedDid === did ? handle : HANDLE_INVALID 214 } catch { 215 return HANDLE_INVALID 216 } 217} 218 219export async function resolveIdentityUsingAppView( 220 identifier: string, 221 signal?: AbortSignal, 222): Promise<ComAtprotoIdentityDefs.IdentityInfo> { 223 if (isDid(identifier)) { 224 const did = identifier as AtprotoDid 225 const didDoc = await resolveDidDocument(did, signal) 226 const handle = await getValidatedHandleFromDidDocument(did, didDoc, signal) 227 228 return { 229 did, 230 didDoc, 231 handle, 232 } 233 } 234 235 const handle = asNormalizedHandle(identifier) 236 if (!handle) { 237 throw new Error(`Invalid handle "${identifier}" provided.`) 238 } 239 240 const did = await resolveHandle(handle, signal) 241 const didDoc = await resolveDidDocument(did, signal) 242 243 return { 244 did, 245 didDoc, 246 handle: extractNormalizedHandle(didDoc) ?? HANDLE_INVALID, 247 } 248} 249 250export function createIdentityResolver(): IdentityResolver { 251 return { 252 async resolve( 253 input: string, 254 options?: {signal?: AbortSignal}, 255 ): Promise<IdentityInfo> { 256 const identity = await resolveIdentityUsingAppView(input, options?.signal) 257 258 return { 259 did: identity.did as AtprotoDid, 260 didDoc: identity.didDoc as IdentityInfo['didDoc'], 261 handle: identity.handle, 262 } 263 }, 264 } 265} 266 267export function getPdsServiceUrlFromIdentityInfo( 268 identity: Pick<ComAtprotoIdentityDefs.IdentityInfo, 'didDoc'>, 269) { 270 return findService( 271 identity.didDoc as DidDocument, 272 '#atproto_pds', 273 'AtprotoPersonalDataServer', 274 )?.serviceEndpoint 275}