package main import ( "context" "fmt" "html" "html/template" "regexp" "strconv" "strings" "time" "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" "github.com/texttheater/golang-levenshtein/levenshtein" ) type EnhancedEvent struct { event *nostr.Event relays []string } func (ee EnhancedEvent) IsReply() bool { return nip10.GetImmediateReply(ee.event.Tags) != nil } func (ee EnhancedEvent) Reply() *nostr.Tag { return nip10.GetImmediateReply(ee.event.Tags) } func (ee EnhancedEvent) Preview() template.HTML { lines := strings.Split(html.EscapeString(ee.event.Content), "\n") var processedLines []string for _, line := range lines { if strings.TrimSpace(line) == "" { continue } processedLine := shortenNostrURLs(line) processedLines = append(processedLines, processedLine) } return template.HTML(strings.Join(processedLines, "
")) } func (ee EnhancedEvent) RssTitle() string { regex := regexp.MustCompile(`(?i)`) replacedString := regex.ReplaceAllString(string(ee.Preview()), " ") words := strings.Fields(replacedString) title := "" for i, word := range words { if len(title)+len(word)+1 <= 65 { // +1 for space if title != "" { title += " " } title += word } else { if i > 1 { // the first word len is > 65 title = title + " ..." } else { title = "" } break } } content := ee.RssContent() distance := levenshtein.DistanceForStrings([]rune(title), []rune(content), levenshtein.DefaultOptions) similarityThreshold := 5 if distance <= similarityThreshold { return "" } else { return title } } func (ee EnhancedEvent) RssContent() string { content := ee.event.Content if ee.IsReply() { nevent, _ := nip19.EncodeEvent(ee.Reply().Value(), ee.relays, ee.event.PubKey) content = content + "\n\n____________________\nIn reply to nostr:" + nevent } content = basicFormatting(html.EscapeString(content), true, false) content = renderQuotesAsHTML(context.Background(), content, false) // content = linkQuotes(content) return content } func (ee EnhancedEvent) Thumb() string { imgRegex := regexp.MustCompile(`(https?://[^\s]+\.(?:png|jpe?g|gif|bmp|svg)(?:/[^\s]*)?)`) matches := imgRegex.FindAllStringSubmatch(ee.event.Content, -1) if len(matches) > 0 { // The first match group captures the image URL return matches[0][1] } return "" } func (ee EnhancedEvent) Npub() string { npub, _ := nip19.EncodePublicKey(ee.event.PubKey) return npub } func (ee EnhancedEvent) NpubShort() string { npub := ee.Npub() return npub[:8] + "…" + npub[len(npub)-4:] } func (ee EnhancedEvent) Nevent() string { nevent, _ := nip19.EncodeEvent(ee.event.ID, ee.relays, ee.event.PubKey) return nevent } func (ee EnhancedEvent) CreatedAtStr() string { return time.Unix(int64(ee.event.CreatedAt), 0).Format("2006-01-02 15:04:05") } func (ee EnhancedEvent) ModifiedAtStr() string { return time.Unix(int64(ee.event.CreatedAt), 0).Format("2006-01-02T15:04:05Z07:00") } type Data struct { templateId TemplateID event *nostr.Event relays []string npub string npubShort string nprofile string nevent string neventNaked string naddr string naddrNaked string createdAt string modifiedAt string parentLink template.HTML metadata *sdk.ProfileMetadata authorRelays []string authorLong string authorShort string renderableLastNotes []EnhancedEvent kindDescription string kindNIP string video string videoType string image string content string alt string kind1063Metadata *Kind1063Metadata kind30311Metadata *Kind30311Metadata kind1311Metadata *Kind1311Metadata } type Kind1063Metadata struct { Magnet string Dim string Size string Summary string Image string URL string AES256GCM string M string X string I string Blurhash string Thumb string } type Kind30311Metadata struct { Title string Summary string Image string Status string Host sdk.ProfileMetadata HostNpub string Tags []string } type Kind1311Metadata struct { // ... } func (fm Kind1063Metadata) IsVideo() bool { return strings.Split(fm.M, "/")[0] == "video" } func (fm Kind1063Metadata) IsImage() bool { return strings.Split(fm.M, "/")[0] == "image" } func (fm Kind1063Metadata) DisplayImage() string { if fm.Image != "" { return fm.Image } else if fm.IsImage() { return fm.URL } else { return "" } } func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, error) { // code can be a nevent, nprofile, npub or nip05 identifier, in which case we try to fetch the associated event event, relays, err := getEvent(ctx, code, nil) if err != nil { log.Warn().Err(err).Str("code", code).Msg("failed to fetch event for code") return nil, fmt.Errorf("error fetching event: %w", err) } relaysForNip19 := make([]string, 0, 3) c := 0 for _, relayUrl := range relays { if shouldUseRelayForNip19(relayUrl) { relaysForNip19 = append(relaysForNip19, relayUrl) if c == 2 { break } } } data := &Data{ event: event, relays: relays, } data.npub, _ = nip19.EncodePublicKey(event.PubKey) data.npubShort = data.npub[:8] + "…" + data.npub[len(data.npub)-4:] data.authorLong = data.npub // hopefully will be replaced later data.authorShort = data.npubShort // hopefully will be replaced later data.nevent, _ = nip19.EncodeEvent(event.ID, relaysForNip19, event.PubKey) data.neventNaked, _ = nip19.EncodeEvent(event.ID, nil, event.PubKey) data.naddr = "" data.naddrNaked = "" data.createdAt = time.Unix(int64(event.CreatedAt), 0).Format("2006-01-02 15:04:05") data.modifiedAt = time.Unix(int64(event.CreatedAt), 0).Format("2006-01-02T15:04:05Z07:00") data.authorRelays = []string{} if event.Kind >= 30000 && event.Kind < 40000 { if d := event.Tags.GetFirst([]string{"d", ""}); d != nil { data.naddr, _ = nip19.EncodeEntity(event.PubKey, event.Kind, d.Value(), relaysForNip19) data.naddrNaked, _ = nip19.EncodeEntity(event.PubKey, event.Kind, d.Value(), nil) } } if tag := event.Tags.GetFirst([]string{"alt", ""}); tag != nil { data.alt = (*tag)[1] } switch event.Kind { case 0: { rawAuthorRelays := []string{} ctx, cancel := context.WithTimeout(ctx, time.Second*4) rawAuthorRelays = relaysForPubkey(ctx, event.PubKey) cancel() for _, relay := range rawAuthorRelays { for _, excluded := range excludedRelays { if strings.Contains(relay, excluded) { continue } } if strings.Contains(relay, "/npub1") { continue // skip relays with personalyzed query like filter.nostr.wine } data.authorRelays = append(data.authorRelays, trimProtocol(relay)) } } lastNotes := authorLastNotes(ctx, event.PubKey, data.authorRelays, isProfileSitemap) data.renderableLastNotes = make([]EnhancedEvent, len(lastNotes)) for i, levt := range lastNotes { data.renderableLastNotes[i] = EnhancedEvent{levt, []string{}} } if err != nil { return nil, err } case 1, 7, 30023, 30024: data.templateId = Note data.content = event.Content if parentNevent := getParentNevent(event); parentNevent != "" { data.parentLink = template.HTML(replaceNostrURLsWithTags(nostrNoteNeventMatcher, "nostr:"+parentNevent)) } case 6: data.templateId = Note if reposted := event.Tags.GetFirst([]string{"e", ""}); reposted != nil { originalNevent, _ := nip19.EncodeEvent((*reposted)[1], []string{}, "") data.content = "Repost of nostr:" + originalNevent } case 1063: data.templateId = FileMetadata data.kind1063Metadata = &Kind1063Metadata{} if tag := event.Tags.GetFirst([]string{"url", ""}); tag != nil { data.kind1063Metadata.URL = (*tag)[1] } if tag := event.Tags.GetFirst([]string{"m", ""}); tag != nil { data.kind1063Metadata.M = (*tag)[1] } if tag := event.Tags.GetFirst([]string{"aes-256-gcm", ""}); tag != nil { data.kind1063Metadata.AES256GCM = (*tag)[1] } if tag := event.Tags.GetFirst([]string{"x", ""}); tag != nil { data.kind1063Metadata.X = (*tag)[1] } if tag := event.Tags.GetFirst([]string{"size", ""}); tag != nil { data.kind1063Metadata.Size = (*tag)[1] } if tag := event.Tags.GetFirst([]string{"dim", ""}); tag != nil { data.kind1063Metadata.Dim = (*tag)[1] } if tag := event.Tags.GetFirst([]string{"magnet", ""}); tag != nil { data.kind1063Metadata.Magnet = (*tag)[1] } if tag := event.Tags.GetFirst([]string{"i", ""}); tag != nil { data.kind1063Metadata.I = (*tag)[1] } if tag := event.Tags.GetFirst([]string{"blurhash", ""}); tag != nil { data.kind1063Metadata.Blurhash = (*tag)[1] } if tag := event.Tags.GetFirst([]string{"thumb", ""}); tag != nil { data.kind1063Metadata.Thumb = (*tag)[1] } if tag := event.Tags.GetFirst([]string{"image", ""}); tag != nil { data.kind1063Metadata.Image = (*tag)[1] data.image = (*tag)[1] } if tag := event.Tags.GetFirst([]string{"summary", ""}); tag != nil { data.kind1063Metadata.Summary = (*tag)[1] } case 30311: data.templateId = LiveEvent data.kind30311Metadata = &Kind30311Metadata{} if tag := event.Tags.GetFirst([]string{"title", ""}); tag != nil { data.kind30311Metadata.Title = (*tag)[1] } if tag := event.Tags.GetFirst([]string{"summary", ""}); tag != nil { data.kind30311Metadata.Summary = (*tag)[1] } if tag := event.Tags.GetFirst([]string{"image", ""}); tag != nil { data.kind30311Metadata.Image = (*tag)[1] data.image = (*tag)[1] } if tag := event.Tags.GetFirst([]string{"status", ""}); tag != nil { data.kind30311Metadata.Status = (*tag)[1] } pTags := event.Tags.GetAll([]string{"p", ""}) for _, p := range pTags { if p[3] == "host" { data.kind30311Metadata.Host = sdk.FetchProfileMetadata(ctx, pool, p[1], data.relays...) data.kind30311Metadata.HostNpub = data.kind30311Metadata.Host.Npub() } } tTags := event.Tags.GetAll([]string{"t", ""}) for _, t := range tTags { data.kind30311Metadata.Tags = append(data.kind30311Metadata.Tags, t[1]) } case 1311: data.templateId = LiveEventMessage data.kind1311Metadata = &Kind1311Metadata{} data.content = event.Content if atag := event.Tags.GetFirst([]string{"a", ""}); atag != nil { parts := strings.Split((*atag)[1], ":") kind, _ := strconv.Atoi(parts[0]) parentNevent, _ := nip19.EncodeEntity(parts[1], kind, parts[2], data.relays) data.parentLink = template.HTML(replaceNostrURLsWithTags(nostrEveryMatcher, "nostr:"+parentNevent)) } default: data.templateId = Other } if event.Kind == 0 { data.nprofile, _ = nip19.EncodeProfile(event.PubKey, limitAt(relays, 2)) data.metadata, _ = sdk.ParseMetadata(event) } else { ctx, cancel := context.WithTimeout(ctx, time.Second*3) defer cancel() author, relays, _ := getEvent(ctx, data.npub, relaysForNip19) if author != nil { data.metadata, _ = sdk.ParseMetadata(author) if data.metadata != nil { data.authorLong = fmt.Sprintf("%s (%s)", data.metadata.Name, data.npub) data.authorShort = fmt.Sprintf("%s (%s)", data.metadata.Name, data.npubShort) } } if len(relays) > 0 { data.nprofile, _ = nip19.EncodeProfile(event.PubKey, limitAt(relays, 2)) } } data.kindDescription = kindNames[event.Kind] if data.kindDescription == "" { data.kindDescription = fmt.Sprintf("Kind %d", event.Kind) } data.kindNIP = kindNIPs[event.Kind] if event.Kind == 1063 { if data.kind1063Metadata.IsImage() { data.image = data.kind1063Metadata.URL } else if data.kind1063Metadata.IsVideo() { data.video = data.kind1063Metadata.URL data.videoType = strings.Split(data.kind1063Metadata.M, "/")[1] } } else { urls := urlMatcher.FindAllString(event.Content, -1) for _, url := range urls { switch { case imageExtensionMatcher.MatchString(url): if data.image == "" { data.image = url } case videoExtensionMatcher.MatchString(url): if data.video == "" { data.video = url if strings.HasSuffix(data.video, "mp4") { data.videoType = "mp4" } else if strings.HasSuffix(data.video, "mov") { data.videoType = "mov" } else { data.videoType = "webm" } } } } } return data, nil }