From 4f3141f66a1c8a559025f796944bfceaace18020 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 2 Oct 2023 15:17:39 -0300 Subject: [PATCH] fetch seenOn relays for individual events. --- data.go | 6 ++-- go.mod | 4 +-- go.sum | 5 ++-- nostr.go | 80 +++++++++++++++++++++++++++++++++++++++++-------- render.go | 16 ++++++---- render_image.go | 4 +-- utils.go | 6 ++-- 7 files changed, 92 insertions(+), 29 deletions(-) diff --git a/data.go b/data.go index d02dfdc..a93ec72 100644 --- a/data.go +++ b/data.go @@ -25,6 +25,7 @@ type Event struct { type Data struct { typ string event *nostr.Event + relays []string npub string npubShort string nevent string @@ -47,7 +48,7 @@ type Data struct { 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, err := getEvent(ctx, code) + event, relays, err := getEvent(ctx, code) if err != nil { log.Warn().Err(err).Str("code", code).Msg("failed to fetch event for code") return nil, err @@ -143,7 +144,7 @@ func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, e if event.Kind != 0 { ctx, cancel := context.WithTimeout(ctx, time.Second*3) - author, _ = getEvent(ctx, npub) + author, _, _ = getEvent(ctx, npub) cancel() } @@ -191,6 +192,7 @@ func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, e return &Data{ typ: typ, event: event, + relays: relays, npub: npub, npubShort: npubShort, nevent: nevent, diff --git a/go.mod b/go.mod index 95705f5..0af9404 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module git.fiatjaf.com/njump -go 1.20 +go 1.21 require ( github.com/apatters/go-wordwrap v1.0.0 @@ -9,7 +9,7 @@ require ( github.com/kelseyhightower/envconfig v1.4.0 github.com/lukevers/freetype-go v0.0.0-20150513150840-77e276735410 github.com/microcosm-cc/bluemonday v1.0.24 - github.com/nbd-wtf/go-nostr v0.23.1 + github.com/nbd-wtf/go-nostr v0.24.1 github.com/pelletier/go-toml v1.9.5 github.com/rs/cors v1.10.0 github.com/rs/zerolog v1.29.1 diff --git a/go.sum b/go.sum index f56d5db..865b74f 100644 --- a/go.sum +++ b/go.sum @@ -84,6 +84,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= @@ -113,8 +114,8 @@ github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/nbd-wtf/go-nostr v0.23.1 h1:O2zHqPfGosbqBSGzwzL7S7zzJt57rY9HIKCMWIr2Lps= -github.com/nbd-wtf/go-nostr v0.23.1/go.mod h1:eE8Qf8QszZbCd9arBQyotXqATNUElWsTEEx+LLORhyQ= +github.com/nbd-wtf/go-nostr v0.24.1 h1:VqWDceiYTKZaOrizgwix/l/MXmDqqXHLqZd6rhiCxbo= +github.com/nbd-wtf/go-nostr v0.24.1/go.mod h1:eE8Qf8QszZbCd9arBQyotXqATNUElWsTEEx+LLORhyQ= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/nostr.go b/nostr.go index 35b4a1a..5350eca 100644 --- a/nostr.go +++ b/nostr.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "math/rand" + "strings" "time" "github.com/nbd-wtf/go-nostr" @@ -49,6 +50,11 @@ var ( } ) +type CachedEvent struct { + Event *nostr.Event `json:"e"` + Relays []string `json:"r"` +} + func getRelay() string { if serial == 0 { serial = rand.Intn(len(everything)) @@ -57,18 +63,26 @@ func getRelay() string { return everything[serial] } -func getEvent(ctx context.Context, code string) (*nostr.Event, error) { +func getEvent(ctx context.Context, code string) (*nostr.Event, []string, error) { if b, ok := cache.Get(code); ok { - v := &nostr.Event{} - err := json.Unmarshal(b, v) - return v, err + // at this point `b` may be a StoredEvent json or an old naked Event json + // TODO: after a week we can assume everything will be StoredEvent, so we can simplify this + v := CachedEvent{} + err := json.Unmarshal(b, &v) + if v.Event == nil { + v.Event = &nostr.Event{} + err = json.Unmarshal(b, v.Event) + } + return v.Event, v.Relays, err } + withRelays := true + prefix, data, err := nip19.Decode(code) if err != nil { pp, _ := nip05.QueryIdentifier(ctx, code) if pp == nil { - return nil, fmt.Errorf("failed to decode %w", err) + return nil, nil, fmt.Errorf("failed to decode %w", err) } data = *pp } @@ -86,6 +100,7 @@ func getEvent(ctx context.Context, code string) (*nostr.Event, error) { filter.Kinds = []int{0} relays = append(relays, profiles...) relays = append(relays, v.Relays...) + withRelays = false case nostr.EventPointer: author = v.Author filter.IDs = []string{v.ID} @@ -115,12 +130,16 @@ func getEvent(ctx context.Context, code string) (*nostr.Event, error) { filter.Authors = []string{v} filter.Kinds = []int{0} relays = append(relays, profiles...) + withRelays = false } } if author != "" { // fetch relays for author authorRelays := relaysForPubkey(ctx, author, relays...) + if len(authorRelays) > 5 { + authorRelays = authorRelays[:5] + } relays = append(relays, authorRelays...) } @@ -131,17 +150,52 @@ func getEvent(ctx context.Context, code string) (*nostr.Event, error) { relays = unique(relays) ctx, cancel := context.WithTimeout(ctx, time.Second*8) defer cancel() - if ie := pool.QuerySingle(ctx, relays, filter); ie.Event != nil { - b, err := json.Marshal(ie.Event) - if err != nil { - log.Error().Err(err).Stringer("event", ie.Event).Msg("error marshaling nson") - return ie.Event, nil + + // actually fetch the event here + var result *nostr.Event + var successRelays []string = nil + if withRelays { + successRelays = make([]string, 0, len(relays)) + countdown := 7.5 + go func() { + for { + time.Sleep(500 * time.Millisecond) + if countdown <= 0 { + cancel() + break + } + countdown -= 0.5 + } + }() + + for ie := range pool.SubManyEoseNonUnique(ctx, relays, nostr.Filters{filter}) { + s := strings.TrimSuffix( + strings.TrimPrefix( + strings.TrimPrefix( + nostr.NormalizeURL(ie.Relay.URL), + "wss://", + ), + "ws://", + ), + "/", + ) + successRelays = append(successRelays, s) + result = ie.Event + countdown = min(countdown, 1) + } + } else { + ie := pool.QuerySingle(ctx, relays, filter) + if ie != nil { + result = ie.Event } - cache.SetWithTTL(code, []byte(b), time.Hour*24*7) - return ie.Event, nil } - return nil, fmt.Errorf("couldn't find this %s", prefix) + if result == nil { + return nil, nil, fmt.Errorf("couldn't find this %s", prefix) + } + + cache.SetJSONWithTTL(code, CachedEvent{Event: result, Relays: successRelays}, time.Hour*24*7) + return result, successRelays, nil } func getLastNotes(ctx context.Context, code string, limit int) []*nostr.Event { diff --git a/render.go b/render.go index adbcef2..ae11276 100644 --- a/render.go +++ b/render.go @@ -129,16 +129,20 @@ func render(w http.ResponseWriter, r *http.Request) { } seenOnRelays := "" - // event.seenOn && event.seenOn.length > 0 - // ? `seen on [ ${event.seenOn.join(' ')} ]` - // : '' + if len(data.relays) > 0 { + seenOnRelays = fmt.Sprintf("seen on %s", strings.Join(data.relays, ", ")) + } textImageURL := "" description := "" if useTextImage { textImageURL = fmt.Sprintf("https://%s/njump/image/%s", host, code) if subject != "" { - description = fmt.Sprintf("%s -- %s", subject, seenOnRelays) + if seenOnRelays != "" { + description = fmt.Sprintf("%s -- %s", subject, seenOnRelays) + } else { + description = subject + } } else { description = seenOnRelays } @@ -224,8 +228,9 @@ func render(w http.ResponseWriter, r *http.Request) { w.Header().Add("Link", "<"+oembed+"&format=xml>; rel=\"alternate\"; type=\"text/xml+oembed\"") } - // template + // template stuff params := map[string]any{ + "style": style, "createdAt": data.createdAt, "modifiedAt": data.modifiedAt, "clients": generateClientList(code, data.event), @@ -253,6 +258,7 @@ func render(w http.ResponseWriter, r *http.Request) { "kindDescription": data.kindDescription, "kindNIP": data.kindNIP, "lastNotes": data.renderableLastNotes, + "seenOn": data.relays, "parentNevent": data.parentNevent, "authorRelays": data.authorRelays, "oembed": oembed, diff --git a/render_image.go b/render_image.go index 0c202ef..0ca7adc 100644 --- a/render_image.go +++ b/render_image.go @@ -33,7 +33,7 @@ func renderImage(w http.ResponseWriter, r *http.Request) { return } - event, err := getEvent(r.Context(), code) + event, _, err := getEvent(r.Context(), code) if err != nil { http.Error(w, "error fetching event: "+err.Error(), 404) return @@ -180,7 +180,7 @@ func renderQuotesAsBlockPrefixedText(ctx context.Context, input string) []string submatch := nostrNoteNeventMatcher.FindStringSubmatch(matchText) nip19 := submatch[0][6:] - event, err := getEvent(ctx, nip19) + event, _, err := getEvent(ctx, nip19) if err != nil { // error case concat this to previous block blocks[b] += matchText diff --git a/utils.go b/utils.go index b989ff0..85d58a0 100644 --- a/utils.go +++ b/utils.go @@ -179,7 +179,7 @@ func getPreviewStyle(r *http.Request) string { case strings.Contains(ua, "iframely"): return "iframely" case strings.Contains(accept, "text/html"): - return "" + return "normal" default: return "unknown" } @@ -264,7 +264,7 @@ func shortenNostrURLs(input string) string { } func getNameFromNip19(ctx context.Context, nip19 string) string { - author, err := getEvent(ctx, nip19) + author, _, err := getEvent(ctx, nip19) if err != nil { return nip19 } @@ -303,7 +303,7 @@ func renderQuotesAsHTML(ctx context.Context, input string, usingTelegramInstantV submatch := nostrNoteNeventMatcher.FindStringSubmatch(match) nip19 := submatch[1] - event, err := getEvent(ctx, nip19) + event, _, err := getEvent(ctx, nip19) if err != nil { log.Warn().Str("nip19", nip19).Msg("failed to get nip19") return nip19