| 1 | package tinyclient |
| 2 | |
| 3 | import ( |
| 4 | "encoding/hex" |
| 5 | "html" |
| 6 | "net/url" |
| 7 | "regexp" |
| 8 | "strings" |
| 9 | ) |
| 10 | |
| 11 | var socialNostrAnchor = regexp.MustCompile(`<a href="/open\?target=([^"&]+)">((?:nostr:|web\+nostr:)[^<]+)</a>` ) |
| 12 | |
| 13 | // socialNostrLinks gives NIP-27 references useful labels after autolinking. |
| 14 | // It only accepts the exact anchors emitted by autolinkOutsideTags. |
| 15 | func socialNostrLinks(rendered string) string { |
| 16 | return socialNostrAnchor.ReplaceAllStringFunc(rendered, func(anchor string) string { |
| 17 | match := socialNostrAnchor.FindStringSubmatch(anchor) |
| 18 | target, err := url.QueryUnescape(html.UnescapeString(match[1])) |
| 19 | if err != nil || !strings.EqualFold(target, html.UnescapeString(match[2])) { |
| 20 | return anchor |
| 21 | } |
| 22 | path, label, ok := socialNostrDestination(target) |
| 23 | if !ok { |
| 24 | return anchor |
| 25 | } |
| 26 | return `<a href="` + html.EscapeString(path) + `">` + label + `</a>` |
| 27 | }) |
| 28 | } |
| 29 | |
| 30 | func socialNostrDestination(target string) (string, string, bool) { |
| 31 | value := strings.Trim(strings.TrimSpace(target), "/" ) |
| 32 | for _, prefix := range []string{"web+nostr:" , "nostr:" } { |
| 33 | if strings.HasPrefix(strings.ToLower(value), prefix) { |
| 34 | value = value[len(prefix):] |
| 35 | break |
| 36 | } |
| 37 | } |
| 38 | hrp, data, ok := bech32Decode(strings.ToLower(value)) |
| 39 | if !ok { |
| 40 | return "" , "" , false |
| 41 | } |
| 42 | switch hrp { |
| 43 | case "npub" , "nprofile" : |
| 44 | fields := data |
| 45 | if hrp == "nprofile" { |
| 46 | fields = tlvFields(data)[0] |
| 47 | } |
| 48 | if len(fields) != 32 { |
| 49 | return "" , "" , false |
| 50 | } |
| 51 | pubkey := hex.EncodeToString(fields) |
| 52 | return "/social?author=" + pubkey, `<nostr-name pubkey="` + pubkey + `">` + shortID(pubkey) + `</nostr-name>` , true |
| 53 | case "note" , "nevent" : |
| 54 | id := data |
| 55 | if hrp == "nevent" { |
| 56 | id = tlvFields(data)[0] |
| 57 | } |
| 58 | if len(id) != 32 { |
| 59 | return "" , "" , false |
| 60 | } |
| 61 | return "/e/" + hex.EncodeToString(id), "Quoted post" , true |
| 62 | case "naddr" : |
| 63 | path, ok := nostrTargetPath(target) |
| 64 | label := "View post" |
| 65 | if strings.HasPrefix(path, "/a/30023:" ) { |
| 66 | label = "Article" |
| 67 | } |
| 68 | return path, label, ok |
| 69 | default: |
| 70 | return "" , "" , false |
| 71 | } |
| 72 | } |