package main import ( "context" "encoding/json" "fmt" "html" "html/template" "io" "net/http" "net/url" "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" sdk "github.com/nbd-wtf/nostr-sdk" ) 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 Style string const ( StyleTelegram Style = "telegram" StyleTwitter = "twitter" StyleIos = "ios" StyleAndroid = "android" StyleMattermost = "mattermost" StyleSlack = "slack" StyleDiscord = "discord" StyleWhatsapp = "whatsapp" StyleIframely = "iframely" StyleNormal = "normal" StyleUnknown = "unknown" ) func getPreviewStyle(r *http.Request) Style { if style := r.URL.Query().Get("style"); style != "" { // debug mode return Style(style) } ua := strings.ToLower(r.Header.Get("User-Agent")) accept := r.Header.Get("Accept") switch { case strings.Contains(ua, "telegrambot"): return StyleTelegram case strings.Contains(ua, "twitterbot"): return StyleTwitter case strings.Contains(ua, "iphone"), strings.Contains(ua, "ipad"), strings.Contains(ua, "ipod"): return StyleIos case strings.Contains(ua, "android"): return StyleAndroid case strings.Contains(ua, "mattermost"): return StyleMattermost case strings.Contains(ua, "slack"): return StyleSlack case strings.Contains(ua, "discord"): return StyleDiscord case strings.Contains(ua, "whatsapp"): return StyleWhatsapp case strings.Contains(ua, "iframely"): return StyleIframely case strings.Contains(accept, "text/html"): return StyleNormal default: return StyleUnknown } } 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 } func attachRelaysToEvent(eventId string, relays ...string) []string { key := "rls:" + eventId existingRelays := make([]string, 0, 10) if exists := cache.GetJSON(key, &existingRelays); exists { relays = unique(append(existingRelays, relays...)) } cache.SetJSONWithTTL(key, relays, time.Hour*24*7) return relays } func getRelaysForEvent(eventId string) []string { key := "rls:" + eventId relays := make([]string, 0, 10) cache.GetJSON(key, &relays) return relays } func scheduleEventExpiration(eventId string, ts time.Duration) { key := "ttl:" + eventId nextExpiration := time.Now().Add(ts).Unix() var currentExpiration int64 if exists := cache.GetJSON(key, ¤tExpiration); exists { if nextExpiration < currentExpiration { return } } cache.SetJSON(key, nextExpiration) } // 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 => `![](%s)` return fmt.Sprintf(imageReplacementTemplate, match) case videoExtensionMatcher.MatchString(match): // Match and replace video URLs with a custom replacement // Usually is html