package main
import (
"context"
"encoding/json"
"fmt"
html "html"
"html/template"
"io"
"net/http"
"regexp"
"strings"
"time"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/ast"
mdhtml "github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/microcosm-cc/bluemonday"
"mvdan.cc/xurls/v2"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip10"
"github.com/nbd-wtf/go-nostr/nip19"
)
const XML_HEADER = "\n"
var (
urlSuffixMatcher = regexp.MustCompile(`[\w-_.]+\.[\w-_.]+(\/[\/\w]*)?$`)
nostrEveryMatcher = regexp.MustCompile(`nostr:((npub|note|nevent|nprofile|naddr)1[a-z0-9]+)\b`)
nostrNoteNeventMatcher = regexp.MustCompile(`nostr:((note|nevent)1[a-z0-9]+)\b`)
nostrNpubNprofileMatcher = regexp.MustCompile(`nostr:((npub|nprofile)1[a-z0-9]+)\b`)
urlMatcher = func() *regexp.Regexp {
// hack to only allow these schemes while still using this library
xurls.Schemes = []string{"https"}
xurls.SchemesNoAuthority = []string{"blob"}
xurls.SchemesUnofficial = []string{"http"}
return xurls.Strict()
}()
imageExtensionMatcher = regexp.MustCompile(`.*\.(png|jpg|jpeg|gif|webp)(\?.*)?$`)
videoExtensionMatcher = regexp.MustCompile(`.*\.(mp4|ogg|webm|mov)(\?.*)?$`)
)
var kindNames = map[int]string{
0: "Metadata",
1: "Short Text Note",
2: "Recommend Relay",
3: "Contacts",
4: "Encrypted Direct Messages",
5: "Event Deletion",
6: "Reposts",
7: "Reaction",
8: "Badge Award",
40: "Channel Creation",
41: "Channel Metadata",
42: "Channel Message",
43: "Channel Hide Message",
44: "Channel Mute User",
1063: "File Metadata",
1984: "Reporting",
9734: "Zap Request",
9735: "Zap",
10000: "Mute List",
10001: "Pin List",
10002: "Relay List Metadata",
13194: "Wallet Info",
22242: "Client Authentication",
23194: "Wallet Request",
23195: "Wallet Response",
24133: "Nostr Connect",
30000: "Categorized People List",
30001: "Categorized Bookmark List",
30008: "Profile Badges",
30009: "Badge Definition",
30017: "Create or update a stall",
30018: "Create or update a product",
30023: "Long-form Content",
30078: "Application-specific Data",
}
var kindNIPs = map[int]string{
0: "01",
1: "01",
2: "01",
3: "02",
4: "04",
5: "09",
6: "18",
7: "25",
8: "58",
40: "28",
41: "28",
42: "28",
43: "28",
44: "28",
1063: "94",
1984: "56",
9734: "57",
9735: "57",
10000: "51",
10001: "51",
10002: "65",
13194: "47",
22242: "42",
23194: "47",
23195: "47",
24133: "46",
30000: "51",
30001: "51",
30008: "58",
30009: "58",
30017: "15",
30018: "15",
30023: "23",
30078: "78",
}
type ClientReference struct {
ID string
Name string
URL string
}
func generateClientList(code string, event *nostr.Event) []ClientReference {
if event.Kind == 1 || event.Kind == 6 {
return []ClientReference{
{ID: "native", Name: "Your native client", URL: "nostr:" + code},
{ID: "snort", Name: "Snort", URL: "https://Snort.social/e/" + code},
{ID: "nostrudel", Name: "Nostrudel", URL: "https://nostrudel.ninja/#/n/" + code},
{ID: "satellite", Name: "Satellite", URL: "https://satellite.earth/thread/" + event.ID},
{ID: "coracle", Name: "Coracle", URL: "https://coracle.social/" + code},
{ID: "primal", Name: "Primal", URL: "https://primal.net/thread/" + event.ID},
{ID: "nostter", Name: "Nostter", URL: "https://nostter.vercel.app/" + code},
{ID: "highlighter", Name: "Highlighter", URL: "https://highlighter.com/a/" + code},
{ID: "iris", Name: "Iris", URL: "https://iris.to/" + code},
}
} else if event.Kind == 0 {
return []ClientReference{
{ID: "native", Name: "Your native client", URL: "nostr:" + code},
{ID: "nosta", Name: "Nosta", URL: "https://nosta.me/" + code},
{ID: "snort", Name: "Snort", URL: "https://snort.social/p/" + code},
{ID: "satellite", Name: "Satellite", URL: "https://satellite.earth/@" + code},
{ID: "coracle", Name: "Coracle", URL: "https://coracle.social/" + code},
{ID: "primal", Name: "Primal", URL: "https://primal.net/profile/" + event.PubKey},
{ID: "nostrudel", Name: "Nostrudel", URL: "https://nostrudel.ninja/#/u/" + code},
{ID: "nostter", Name: "Nostter", URL: "https://nostter.vercel.app/" + code},
{ID: "iris", Name: "Iris", URL: "https://iris.to/" + code},
}
} else if event.Kind == 30023 || event.Kind == 30024 {
return []ClientReference{
{ID: "native", Name: "Your native client", URL: "nostr:" + code},
{ID: "yakihonne", Name: "YakiHonne", URL: "https://yakihonne.com/article/" + code},
{ID: "habla", Name: "Habla", URL: "https://habla.news/a/" + code},
{ID: "highlighter", Name: "Highlighter", URL: "https://highlighter.com/a/" + code},
{ID: "blogstack", Name: "Blogstack", URL: "https://blogstack.io/" + code},
}
}
return nil
}
func generateRelayBrowserClientList(host string) []ClientReference {
return []ClientReference{
{ID: "coracle", Name: "Coracle", URL: "https://coracle.social/relays/" + host},
}
}
func getPreviewStyle(r *http.Request) string {
if style := r.URL.Query().Get("style"); style != "" {
// debug mode
return style
}
ua := strings.ToLower(r.Header.Get("User-Agent"))
accept := r.Header.Get("Accept")
switch {
case strings.Contains(ua, "telegrambot"):
return "telegram"
case strings.Contains(ua, "twitterbot"):
return "twitter"
case strings.Contains(ua, "mattermost"):
return "mattermost"
case strings.Contains(ua, "slack"):
return "slack"
case strings.Contains(ua, "discord"):
return "discord"
case strings.Contains(ua, "whatsapp"):
return "whatsapp"
case strings.Contains(ua, "iframely"):
return "iframely"
case strings.Contains(accept, "text/html"):
return "normal"
default:
return "unknown"
}
}
func getParentNevent(event *nostr.Event) string {
parentNevent := ""
replyTag := nip10.GetImmediateReply(event.Tags)
if replyTag != nil {
relay := ""
if len(*replyTag) > 2 {
relay = (*replyTag)[2]
} else {
relay = ""
}
parentNevent, _ = nip19.EncodeEvent((*replyTag)[1], []string{relay}, "")
}
return parentNevent
}
// Rendering functions
// ### ### ### ### ### ### ### ### ### ### ###
func replaceURLsWithTags(input string, imageReplacementTemplate, videoReplacementTemplate string) string {
return urlMatcher.ReplaceAllStringFunc(input, func(match string) string {
switch {
case imageExtensionMatcher.MatchString(match):
// Match and replace image URLs with a custom replacement
// Usually is html => `
`
// or markdown !()[...] tags for further processing => ``
return fmt.Sprintf(imageReplacementTemplate, match)
case videoExtensionMatcher.MatchString(match):
// Match and replace video URLs with a custom replacement
// Usually is html
`)) } return ast.GoToNext, true } return ast.GoToNext, false } } // create HTML renderer with extensions opts := mdhtml.RendererOptions{ Flags: mdhtml.CommonFlags | mdhtml.HrefTargetBlank, RenderNodeHook: customNodeHook, } renderer := mdhtml.NewRenderer(opts) output := string(markdown.Render(doc, renderer)) // sanitize content output = sanitizeXSS(output) return output } func unique(strSlice []string) []string { keys := make(map[string]bool) list := []string{} for _, entry := range strSlice { if _, ok := keys[entry]; !ok { keys[entry] = true list = append(list, entry) } } return list } func trimProtocol(relay string) string { relay = strings.TrimPrefix(relay, "wss://") relay = strings.TrimPrefix(relay, "ws://") relay = strings.TrimPrefix(relay, "wss:/") // Some browsers replace upfront '//' with '/' relay = strings.TrimPrefix(relay, "ws:/") // Some browsers replace upfront '//' with '/' return relay } func normalizeWebsiteURL(u string) string { if strings.HasPrefix(u, "http") { return u } return "https://" + u } func loadNpubsArchive(ctx context.Context) { log.Debug().Msg("refreshing the npubs archive") contactsArchive := make([]string, 0, 500) for _, pubkey := range trustedPubKeys { ctx, cancel := context.WithTimeout(ctx, time.Second*4) pubkeyContacts := contactsForPubkey(ctx, pubkey) contactsArchive = append(contactsArchive, pubkeyContacts...) cancel() } contactsArchive = unique(contactsArchive) for _, contact := range contactsArchive { log.Debug().Msgf("adding contact %s", contact) cache.SetWithTTL("pa:"+contact, nil, time.Hour*24*90) } } func loadRelaysArchive(ctx context.Context) { log.Debug().Msg("refreshing the relays archive") relaysArchive := make([]string, 0, 500) for _, pubkey := range trustedPubKeys { ctx, cancel := context.WithTimeout(ctx, time.Second*4) pubkeyContacts := relaysForPubkey(ctx, pubkey) relaysArchive = append(relaysArchive, pubkeyContacts...) cancel() } relaysArchive = unique(relaysArchive) for _, relay := range relaysArchive { for _, excluded := range excludedRelays { if strings.Contains(relay, excluded) { log.Debug().Msgf("skipping relay %s", relay) continue } } if strings.Contains(relay, "/npub1") { continue // skip relays with personalyzed query like filter.nostr.wine } log.Debug().Msgf("adding relay %s", relay) cache.SetWithTTL("ra:"+relay, nil, time.Hour*24*7) } } func eventToHTML(evt *nostr.Event) template.HTML { tagsHTML := "[" for t, tag := range evt.Tags { tagsHTML += "\n [" for i, item := range tag { cls := `"text-zinc-500 dark:text-zinc-50"` if i == 0 { cls = `"text-amber-500 dark:text-amber-200"` } itemJSON, _ := json.Marshal(item) tagsHTML += "\n " + html.EscapeString(string(itemJSON)) if i < len(tag)-1 { tagsHTML += "," } else { tagsHTML += "\n " } } tagsHTML += "]" if t < len(evt.Tags)-1 { tagsHTML += "," } else { tagsHTML += "\n " } } tagsHTML += "]" contentJSON, _ := json.Marshal(evt.Content) keyCls := "text-purple-700 dark:text-purple-300" return template.HTML(fmt.Sprintf( `{ "id": "%s", "pubkey": "%s", "created_at": %d, "kind": %d, "tags": %s, "content": %s, "sig": "%s" }`, evt.ID, evt.PubKey, evt.CreatedAt, evt.Kind, tagsHTML, html.EscapeString(string(contentJSON)), evt.Sig), ) } func limitAt[V any](list []V, n int) []V { if len(list) < n { return list } return list[0:n] }