tinyrelayfile
tinyrelay/tinyclient/connections.go/

tinyclient/connections.go raw
1package tinyclient
2
3import (
4 "encoding/binary"
5 "encoding/hex"
6 "net/url"
7 "regexp"
8 "strings"
9)
10
11func identityNpub(identity string) string {
12 decoded, err := hex.DecodeString(identity)
13 if err != nil || len(decoded) != 32 {
14 return identity
15 }
16 return bech32("npub", decoded)
17}
18
19const bech32Charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
20
21// bech32 encodes data with the given human readable part.
22func bech32(hrp string, data []byte) string {
23 fiveBits := convertBits(data)
24 expanded := make([]byte, 0, len(hrp)*2+1)
25 for _, c := range hrp {
26 expanded = append(expanded, byte(c)>>5)
27 }
28 expanded = append(expanded, 0)
29 for _, c := range hrp {
30 expanded = append(expanded, byte(c)&31)
31 }
32 values := append(append(append([]byte{}, expanded...), fiveBits...), 0, 0, 0, 0, 0, 0)
33 polymod := bech32Polymod(values) ^ 1
34 checksum := make([]byte, 6)
35 for i := range checksum {
36 checksum[i] = byte(polymod >> uint(5*(5-i)) & 31)
37 }
38 var out strings.Builder
39 out.WriteString(hrp + "1")
40 for _, value := range append(fiveBits, checksum...) {
41 out.WriteByte(bech32Charset[value])
42 }
43 return out.String()
44}
45
46func convertBits(data []byte) []byte {
47 result := make([]byte, 0, 52)
48 accumulator, bits := 0, 0
49 for _, value := range data {
50 accumulator = (accumulator << 8) | int(value)
51 bits += 8
52 for bits >= 5 {
53 bits -= 5
54 result = append(result, byte(accumulator>>bits&31))
55 }
56 }
57 if bits > 0 {
58 result = append(result, byte(accumulator<<(5-bits)&31))
59 }
60 return result
61}
62
63func bech32Polymod(values []byte) uint64 {
64 generators := [...]uint64{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
65 checksum := uint64(1)
66 for _, value := range values {
67 top := checksum >> 25
68 checksum = (checksum&0x1ffffff)<<5 ^ uint64(value)
69 for i, generator := range generators {
70 if top>>uint(i)&1 != 0 {
71 checksum ^= generator
72 }
73 }
74 }
75 return checksum
76}
77
78func tlv(kind byte, value []byte) []byte {
79 return append([]byte{kind, byte(len(value))}, value...)
80}
81
82// nprofile encodes a NIP-19 profile pointer with one relay hint.
83func nprofile(pubkeyHex, relay string) string {
84 pubkey, err := hex.DecodeString(pubkeyHex)
85 if err != nil || len(pubkey) != 32 {
86 return ""
87 }
88 data := tlv(0, pubkey)
89 if relay != "" {
90 data = append(data, tlv(1, []byte(relay))...)
91 }
92 return bech32("nprofile", data)
93}
94
95// naddr encodes a NIP-19 address pointer for a replaceable event.
96func naddr(kind uint32, pubkeyHex, identifier, relay string) string {
97 pubkey, err := hex.DecodeString(pubkeyHex)
98 if err != nil || len(pubkey) != 32 {
99 return ""
100 }
101 data := tlv(0, []byte(identifier))
102 if relay != "" {
103 data = append(data, tlv(1, []byte(relay))...)
104 }
105 data = append(data, tlv(2, pubkey)...)
106 kindBytes := make([]byte, 4)
107 binary.BigEndian.PutUint32(kindBytes, kind)
108 data = append(data, tlv(3, kindBytes)...)
109 return bech32("naddr", data)
110}
111
112var placeholder = regexp.MustCompile(`\{([a-z]+):([a-z]+)(?:\|(enc))?\}`)
113
114// connectionValues resolves the placeholders connection templates use:
115// relay:url, relay:web, relay:host, relay:domain, relay:naddr, owner:npub,
116// owner:nprofile, user:npub and user:nprofile. The relay's group address is
117// its NIP-29 top-level group, published under the relay identity.
118func connectionValues(data PageData) map[string]string {
119 values := map[string]string{}
120 web := strings.TrimRight(data.URL, "/")
121 values["relay:web"] = web
122 values["relay:url"] = wsURL(web)
123 if parsed, err := url.Parse(web); err == nil {
124 values["relay:host"] = parsed.Host
125 values["relay:domain"] = parsed.Hostname()
126 }
127 if data.Identity != "" {
128 values["relay:naddr"] = naddr(39000, data.Identity, "_", values["relay:url"])
129 }
130 if data.Policy.Owner != "" {
131 values["owner:npub"] = identityNpub(data.Policy.Owner)
132 values["owner:nprofile"] = nprofile(data.Policy.Owner, values["relay:url"])
133 }
134 if data.Actor != "" {
135 values["user:npub"] = identityNpub(data.Actor)
136 values["user:nprofile"] = nprofile(data.Actor, values["relay:url"])
137 }
138 return values
139}
140
141// fillPlaceholders substitutes known values, leaving input placeholders for
142// the page script. It reports false when a needed value is missing, such as
143// a user value for a guest, so the link can be left out.
144func fillPlaceholders(text string, values map[string]string) (string, bool) {
145 complete := true
146 out := placeholder.ReplaceAllStringFunc(text, func(match string) string {
147 parts := placeholder.FindStringSubmatch(match)
148 if parts[1] == "input" {
149 return match
150 }
151 value, ok := values[parts[1]+":"+parts[2]]
152 if !ok || value == "" {
153 complete = false
154 return ""
155 }
156 if parts[3] == "enc" {
157 return url.QueryEscape(value)
158 }
159 return value
160 })
161 return out, complete
162}
163
164// expandConnections turns the daemon's connection rows into ready-to-render
165// maps with placeholders resolved. Links that need a signed-in user are
166// dropped for guests; the card keeps a note that signing in reveals them.
167func expandConnections(rows []any, data PageData) []any {
168 values := connectionValues(data)
169 out := make([]any, 0, len(rows))
170 for _, row := range rows {
171 item := valueMap(row)
172 card := map[string]any{"name": plainString(item["template"]), "title": plainString(item["title"]), "about": plainString(item["about"]), "app": plainString(item["app"]), "where": plainString(item["where"]), "visibility": plainString(item["visibility"]), "inputs": item["inputs"]}
173 if card["title"] == "" {
174 card["title"] = card["name"]
175 }
176 if qr, ok := fillPlaceholders(plainString(item["qr"]), values); ok && qr != "" {
177 card["qr"] = qr
178 }
179 links := []any{}
180 hidden := 0
181 for _, link := range browseRows(item["links"]) {
182 l := valueMap(link)
183 field := func(name string) string {
184 if value := plainString(l[name]); value != "" {
185 return value
186 }
187 return plainString(l[strings.ToUpper(name[:1])+name[1:]])
188 }
189 href, okHref := fillPlaceholders(field("href"), values)
190 copyText, okCopy := fillPlaceholders(field("copy"), values)
191 if !okHref || !okCopy {
192 hidden++
193 continue
194 }
195 links = append(links, map[string]any{"label": field("label"), "href": href, "copy": copyText})
196 }
197 card["links"], card["hidden"] = links, hidden
198 out = append(out, card)
199 }
200 return out
201}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs