From 36c197af4278f66a3c6153e5b76247b0a5dae323 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 6 Oct 2024 15:53:33 -0300 Subject: [PATCH] sdk: FetchSpecificEvent() --- sdk/helpers.go | 18 +++-- sdk/replaceable_loader.go | 8 +- sdk/search.go | 4 +- sdk/specific_event.go | 159 ++++++++++++++++++++++++++++++++++++++ sdk/system.go | 76 +++++++++++------- 5 files changed, 226 insertions(+), 39 deletions(-) create mode 100644 sdk/specific_event.go diff --git a/sdk/helpers.go b/sdk/helpers.go index e669a7e..dbb36dd 100644 --- a/sdk/helpers.go +++ b/sdk/helpers.go @@ -1,12 +1,18 @@ package sdk -import "time" +import ( + "slices" + "time" +) -var serial = 0 - -func pickNext(list []string) string { - serial++ - return list[serial%len(list)] +func appendUnique[I comparable](arr []I, item ...I) []I { + for _, item := range item { + if slices.Contains(arr, item) { + return arr + } + arr = append(arr, item) + } + return arr } func doThisNotMoreThanOnceAnHour(key string) (doItNow bool) { diff --git a/sdk/replaceable_loader.go b/sdk/replaceable_loader.go index 183d3ec..334dacc 100644 --- a/sdk/replaceable_loader.go +++ b/sdk/replaceable_loader.go @@ -136,13 +136,13 @@ func (sys *System) determineRelaysToQuery(ctx context.Context, pubkey string, ki var next string switch kind { case 0: - next = pickNext(sys.MetadataRelays) + next = sys.MetadataRelays.Next() case 3: - next = pickNext(sys.FollowListRelays) + next = sys.FollowListRelays.Next() case 10002: - next = pickNext(sys.RelayListRelays) + next = sys.RelayListRelays.Next() default: - next = pickNext(sys.FallbackRelays) + next = sys.FallbackRelays.Next() } if !slices.Contains(relays, next) { diff --git a/sdk/search.go b/sdk/search.go index 300bea8..48550ca 100644 --- a/sdk/search.go +++ b/sdk/search.go @@ -8,9 +8,9 @@ import ( func (sys *System) SearchUsers(ctx context.Context, query string) []ProfileMetadata { limit := 10 - profiles := make([]ProfileMetadata, 0, limit*len(sys.UserSearchRelays)) + profiles := make([]ProfileMetadata, 0, limit*len(sys.UserSearchRelays.URLs)) - for ie := range sys.Pool.SubManyEose(ctx, sys.UserSearchRelays, nostr.Filters{ + for ie := range sys.Pool.SubManyEose(ctx, sys.UserSearchRelays.URLs, nostr.Filters{ { Search: query, Limit: limit, diff --git a/sdk/specific_event.go b/sdk/specific_event.go new file mode 100644 index 0000000..c080ac7 --- /dev/null +++ b/sdk/specific_event.go @@ -0,0 +1,159 @@ +package sdk + +import ( + "context" + "fmt" + "slices" + "sync" + "time" + + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" +) + +// FetchSpecificEvent tries to get a specific event from a NIP-19 code using whatever means necessary. +func (sys *System) FetchSpecificEvent( + ctx context.Context, + code string, + withRelays bool, +) (event *nostr.Event, successRelays []string, err error) { + // this is for deciding what relays will go on nevent and nprofile later + priorityRelays := make([]string, 0, 8) + + prefix, data, err := nip19.Decode(code) + if err != nil { + return nil, nil, fmt.Errorf("failed to decode %w", err) + } + + author := "" + + var filter nostr.Filter + relays := make([]string, 0, 10) + fallback := make([]string, 0, 10) + successRelays = make([]string, 0, 10) + + switch v := data.(type) { + case nostr.EventPointer: + author = v.Author + filter.IDs = []string{v.ID} + relays = append(relays, v.Relays...) + relays = appendUnique(relays, sys.FallbackRelays.Next()) + fallback = append(fallback, sys.JustIDRelays.URLs...) + fallback = appendUnique(fallback, sys.FallbackRelays.Next()) + for _, r := range v.Relays { + priorityRelays = append(priorityRelays, r) + } + case nostr.EntityPointer: + author = v.PublicKey + filter.Authors = []string{v.PublicKey} + filter.Tags = nostr.TagMap{"d": []string{v.Identifier}} + filter.Kinds = []int{v.Kind} + relays = append(relays, v.Relays...) + relays = appendUnique(relays, sys.FallbackRelays.Next()) + fallback = append(fallback, sys.FallbackRelays.Next(), sys.FallbackRelays.Next()) + case string: + if prefix == "note" { + filter.IDs = []string{v} + relays = append(relays, sys.JustIDRelays.Next(), sys.JustIDRelays.Next()) + fallback = appendUnique(fallback, + sys.FallbackRelays.Next(), sys.JustIDRelays.Next(), sys.FallbackRelays.Next()) + } + } + + // try to fetch in our internal eventstore first + if res, _ := sys.StoreRelay.QuerySync(ctx, filter); len(res) != 0 { + evt := res[0] + return evt, nil, nil + } + + if author != "" { + // fetch relays for author + authorRelays := sys.FetchOutboxRelays(ctx, author, 3) + relays = appendUnique(relays, authorRelays...) + priorityRelays = appendUnique(priorityRelays, authorRelays...) + } + + var result *nostr.Event + + for _, attempt := range []struct { + label string + relays []string + nonUnique bool + }{ + { + label: "fetch-" + prefix, + relays: relays, + // set this to true if the caller wants relays, so we won't return immediately + // but will instead wait a little while to see if more relays respond + nonUnique: withRelays, + }, + { + label: "fetchf-" + prefix, + relays: fallback, + nonUnique: false, + }, + } { + // actually fetch the event here + countdown := 6.0 + subManyCtx := ctx + + if attempt.nonUnique { + // keep track of where we have actually found the event so we can show that + var cancel context.CancelFunc + subManyCtx, cancel = context.WithTimeout(ctx, time.Second*6) + defer cancel() + + go func() { + for { + time.Sleep(100 * time.Millisecond) + if countdown <= 0 { + cancel() + break + } + countdown -= 0.1 + } + }() + } + + fetchProfileOnce := sync.Once{} + + for ie := range sys.Pool.SubManyEoseNonUnique( + subManyCtx, + attempt.relays, + nostr.Filters{filter}, + nostr.WithLabel(attempt.label), + ) { + fetchProfileOnce.Do(func() { + go sys.FetchProfileMetadata(ctx, ie.PubKey) + }) + + successRelays = append(successRelays, ie.Relay.URL) + if result == nil || ie.CreatedAt > result.CreatedAt { + result = ie.Event + } + countdown = min(countdown-0.5, 1) + } + } + + if result == nil { + return nil, nil, fmt.Errorf("couldn't find this %s", prefix) + } + + // save stuff in cache and in internal store + sys.StoreRelay.Publish(ctx, *result) + + // put priority relays first so they get used in nevent and nprofile + slices.SortFunc(successRelays, func(a, b string) int { + vpa := slices.Contains(priorityRelays, a) + vpb := slices.Contains(priorityRelays, b) + if vpa == vpb { + return 1 + } + if vpa && !vpb { + return 1 + } + return -1 + }) + + return result, successRelays, nil +} diff --git a/sdk/system.go b/sdk/system.go index 3ab7fdf..dd0fac0 100644 --- a/sdk/system.go +++ b/sdk/system.go @@ -19,12 +19,13 @@ type System struct { MetadataCache cache.Cache32[ProfileMetadata] Hints hints.HintsDB Pool *nostr.SimplePool - RelayListRelays []string - FollowListRelays []string - MetadataRelays []string - FallbackRelays []string - UserSearchRelays []string - NoteSearchRelays []string + RelayListRelays *RelayStream + FollowListRelays *RelayStream + MetadataRelays *RelayStream + FallbackRelays *RelayStream + JustIDRelays *RelayStream + UserSearchRelays *RelayStream + NoteSearchRelays *RelayStream Store eventstore.Store StoreRelay nostr.RelayStore @@ -35,35 +36,50 @@ type System struct { type SystemModifier func(sys *System) +type RelayStream struct { + URLs []string + serial int +} + +func NewRelayStream(urls ...string) *RelayStream { + return &RelayStream{URLs: urls, serial: -1} +} + +func (rs *RelayStream) Next() string { + rs.serial++ + return rs.URLs[rs.serial%len(rs.URLs)] +} + func NewSystem(mods ...SystemModifier) *System { sys := &System{ RelayListCache: cache_memory.New32[RelayList](1000), FollowListCache: cache_memory.New32[FollowList](1000), MetadataCache: cache_memory.New32[ProfileMetadata](1000), - RelayListRelays: []string{"wss://purplepag.es", "wss://user.kindpag.es", "wss://relay.nos.social"}, - FollowListRelays: []string{"wss://purplepag.es", "wss://user.kindpag.es", "wss://relay.nos.social"}, - MetadataRelays: []string{"wss://purplepag.es", "wss://user.kindpag.es", "wss://relay.nos.social"}, - FallbackRelays: []string{ - "wss://relay.primal.net", + RelayListRelays: NewRelayStream("wss://purplepag.es", "wss://user.kindpag.es", "wss://relay.nos.social"), + FollowListRelays: NewRelayStream("wss://purplepag.es", "wss://user.kindpag.es", "wss://relay.nos.social"), + MetadataRelays: NewRelayStream("wss://purplepag.es", "wss://user.kindpag.es", "wss://relay.nos.social"), + FallbackRelays: NewRelayStream( "wss://relay.damus.io", - "wss://nostr.wine", "wss://nostr.mom", - "wss://offchain.pub", "wss://nos.lol", "wss://mostr.pub", "wss://relay.nostr.band", - "wss://nostr21.com", - }, - UserSearchRelays: []string{ + ), + JustIDRelays: NewRelayStream( + "wss://cache2.primal.net/v1", + "wss://relay.noswhere.com", + "wss://relay.nostr.band", + ), + UserSearchRelays: NewRelayStream( + "wss://search.nos.today", "wss://nostr.wine", "wss://relay.nostr.band", - "wss://relay.noswhere.com", - }, - NoteSearchRelays: []string{ + ), + NoteSearchRelays: NewRelayStream( "wss://nostr.wine", "wss://relay.nostr.band", - "wss://relay.noswhere.com", - }, + "wss://search.nos.today", + ), Hints: memory_hints.NewHintDB(), outboxShortTermCache: cache_memory.New32[[]string](1000), @@ -99,37 +115,43 @@ func WithHintsDB(hdb hints.HintsDB) SystemModifier { func WithRelayListRelays(list []string) SystemModifier { return func(sys *System) { - sys.RelayListRelays = list + sys.RelayListRelays.URLs = list } } func WithMetadataRelays(list []string) SystemModifier { return func(sys *System) { - sys.MetadataRelays = list + sys.MetadataRelays.URLs = list } } func WithFollowListRelays(list []string) SystemModifier { return func(sys *System) { - sys.FollowListRelays = list + sys.FollowListRelays.URLs = list } } func WithFallbackRelays(list []string) SystemModifier { return func(sys *System) { - sys.FallbackRelays = list + sys.FallbackRelays.URLs = list + } +} + +func WithJustIDRelays(list []string) SystemModifier { + return func(sys *System) { + sys.JustIDRelays.URLs = list } } func WithUserSearchRelays(list []string) SystemModifier { return func(sys *System) { - sys.UserSearchRelays = list + sys.UserSearchRelays.URLs = list } } func WithNoteSearchRelays(list []string) SystemModifier { return func(sys *System) { - sys.NoteSearchRelays = list + sys.NoteSearchRelays.URLs = list } }