Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
119
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 125 lines 3.1 kB view raw
1import {type BskyAgent, type Facet, type RichText} from '@atproto/api' 2import {tokenize} from '@easrng/tr58' 3 4const TAG_CHARS = ['#', '#', '$'] 5 6export interface FacetRun { 7 text: string 8 features: Facet['features'] 9} 10 11export function detectFacetRunsWithoutResolution(text: string): FacetRun[] { 12 const facetRuns: FacetRun[] = [] 13 14 const tokens = tokenize(text, { 15 nonStandard: {domainHandle: true, tags: TAG_CHARS}, 16 }) 17 18 for (const token of tokens) { 19 if (token.type === 'URL') { 20 const val = token.value 21 22 // Handle mentions (@handle.com) 23 if (/^[@@]/.test(token.value)) { 24 facetRuns.push({ 25 text: val, 26 features: [ 27 { 28 $type: 'app.bsky.richtext.facet#mention', 29 did: val.slice(1) as any, 30 }, 31 ], 32 }) 33 } 34 // Handle tags (#tag or $cashtag) 35 else if (TAG_CHARS.some(char => val.startsWith(char))) { 36 const normalized = (val[0] === '$' ? val : val.slice(1)).normalize( 37 'NFKC', 38 ) 39 facetRuns.push({ 40 text: val, 41 features: /^\$?\d/.test(normalized) 42 ? [] 43 : [ 44 { 45 $type: 'app.bsky.richtext.facet#tag', 46 tag: normalized, 47 }, 48 ], 49 }) 50 } else { 51 let uri = val 52 if (!/^[a-z][a-z0-9+.-]*:\/\//.test(uri)) { 53 uri = `https://${uri}` 54 } 55 const NON_EMAIL = /^[^@@]+?([?/#:]|$)/ 56 facetRuns.push({ 57 text: val, 58 // don't link email addresses 59 features: NON_EMAIL.test(token.value) 60 ? [ 61 { 62 $type: 'app.bsky.richtext.facet#link', 63 uri: uri, 64 }, 65 ] 66 : [], 67 }) 68 } 69 } else { 70 facetRuns.push({ 71 text: token.value, 72 features: [], 73 }) 74 } 75 } 76 return facetRuns 77} 78 79export function detectFacetsWithoutResolution(rt: RichText) { 80 const facets: Facet[] = [] 81 82 let currentByteOffset = 0 83 84 for (const run of detectFacetRunsWithoutResolution(rt.text)) { 85 const runBytes = new TextEncoder().encode(run.text) 86 const start = currentByteOffset 87 const end = start + runBytes.byteLength 88 89 if (run.features.length) { 90 facets.push({ 91 index: {byteStart: start, byteEnd: end}, 92 features: run.features, 93 }) 94 } 95 96 currentByteOffset = end 97 } 98 99 rt.facets = facets 100 return rt 101} 102 103export async function detectFacets(agent: BskyAgent, rt: RichText) { 104 detectFacetsWithoutResolution(rt) 105 if (rt.facets) { 106 for (const facet of rt.facets) { 107 for (const feature of facet.features) { 108 if ( 109 feature.$type === 'app.bsky.richtext.facet#mention' && 110 'did' in feature && 111 !feature.did.startsWith('did:') 112 ) { 113 try { 114 const res = await agent.resolveHandle({handle: feature.did}) 115 feature.did = res.data.did 116 } catch (e) { 117 facet.features = facet.features.filter(f => f !== feature) 118 } 119 } 120 } 121 } 122 } 123 124 return rt 125}