Bluesky app fork with some witchin' additions 💫
witchsky.app
bluesky
fork
client
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}