package main import ( "context" "fmt" "iter" "slices" "time" "github.com/fiatjaf/eventstore/badger" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/sdk" badger_kv "github.com/nbd-wtf/go-nostr/sdk/kvstore/badger" ) type RelayConfig struct { Everything []string `json:"everything"` Profiles []string `json:"profiles"` JustIds []string `json:"justIds"` } const DB_MAX_LIMIT = 500 var ( sys *sdk.System serial int relayConfig = RelayConfig{ Everything: nil, // use the defaults from nostr-sdk Profiles: nil, // use the defaults from nostr-sdk JustIds: []string{ "wss://cache2.primal.net/v1", "wss://relay.noswhere.com", "wss://relay.damus.io", }, } defaultTrustedPubKeys = []string{ "7bdef7be22dd8e59f4600e044aa53a1cf975a9dc7d27df5833bc77db784a5805", // dtonon "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", // fiatjaf "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", // hodlbod "ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49", // Michael Dilger "30c25d24b998c6b51253253fd66d7ceccc7e47ae3d8c540d2a914bec77e89b1d", // ---- } ) func initSystem() func() { db := &badger.BadgerBackend{ Path: s.EventStorePath, MaxLimit: DB_MAX_LIMIT, } if err := db.Init(); err != nil { panic(err) } kv, err := badger_kv.NewStore(s.KVStorePath) if err != nil { panic(err) } sys = sdk.NewSystem( sdk.WithStore(db), sdk.WithKVStore(kv), ) return db.Close } func getEvent(ctx context.Context, code string, withRelays bool) (*nostr.Event, []string, error) { evt, relays, err := sys.FetchSpecificEventFromInput(ctx, code, sdk.FetchSpecificEventParameters{ WithRelays: withRelays, }) if err != nil { return nil, nil, fmt.Errorf("couldn't find this event, did you include accurate relay or author hints in it?") } if !withRelays { return evt, nil, nil } if relays == nil { return evt, internal.getRelaysForEvent(evt.ID), nil } // save relays if we got them allRelays := internal.attachRelaysToEvent(evt.ID, relays...) return evt, allRelays, nil } func authorLastNotes(ctx context.Context, pubkey string) (lastNotes []EnhancedEvent, justFetched bool) { limit := 100 go sys.FetchProfileMetadata(ctx, pubkey) // fetch this before so the cache is filled for later filter := nostr.Filter{ Kinds: []int{nostr.KindTextNote}, Authors: []string{pubkey}, Limit: limit, } lastNotes = make([]EnhancedEvent, 0, filter.Limit) latestTimestamp := nostr.Timestamp(0) // fetch from local store if available ch, err := sys.Store.QueryEvents(ctx, filter) if err == nil { evt, has := <-ch if has { lastNotes = append(lastNotes, NewEnhancedEvent(ctx, evt)) latestTimestamp = evt.CreatedAt for evt = range ch { lastNotes = append(lastNotes, NewEnhancedEvent(ctx, evt)) } } } if (len(lastNotes) < limit/10) || (len(lastNotes) < limit/5 && latestTimestamp > nostr.Now()-60*60*24*2) || (len(lastNotes) < limit/2 && latestTimestamp < nostr.Now()-60*60*24*2) { // if we didn't get enough notes then try to fetch from external relays (but do not wait for it) justFetched = true go func() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer cancel() relays := sys.FetchOutboxRelays(ctx, pubkey, 3) for len(relays) < 3 { relays = appendUnique(relays, sys.FallbackRelays.Next()) } ch := sys.Pool.FetchMany(ctx, relays, filter, nostr.WithLabel("authorlast")) out: for { select { case ie, more := <-ch: if !more { break out } ee := NewEnhancedEvent(ctx, ie.Event) ee.relays = appendUnique([]string{ie.Relay.URL}, internal.getRelaysForEvent(ie.Event.ID)...) lastNotes = append(lastNotes, ee) sys.Store.SaveEvent(ctx, ie.Event) internal.attachRelaysToEvent(ie.Event.ID, ie.Relay.URL) case <-ctx.Done(): break out } } }() } return lastNotes, justFetched } func relayLastNotes(ctx context.Context, hostname string, limit int) iter.Seq[*nostr.Event] { ctx, cancel := context.WithTimeout(ctx, time.Second*4) return func(yield func(*nostr.Event) bool) { defer cancel() for id := range internal.getEventsInRelay(hostname) { res, _ := sys.StoreRelay.QuerySync(ctx, nostr.Filter{IDs: []string{id}}) if len(res) == 0 { internal.notCached(id) continue } limit-- if !yield(res[0]) { return } if limit == 0 { return } } if limit > 0 { limit = max(limit, 50) if relay, err := sys.Pool.EnsureRelay(hostname); err == nil { ch, err := relay.QueryEvents(ctx, nostr.Filter{ Kinds: []int{1}, Limit: limit, }) if err != nil { log.Error().Err(err).Stringer("relay", relay).Msg("failed to fetch relay notes") return } for evt := range ch { sys.StoreRelay.Publish(ctx, *evt) internal.attachRelaysToEvent(evt.ID, hostname) if !yield(evt) { return } } } } } } func relaysPretty(ctx context.Context, pubkey string) []string { s := make([]string, 0, 3) for _, url := range sys.FetchOutboxRelays(ctx, pubkey, 3) { trimmed := trimProtocolAndEndingSlash(url) if slices.Contains(s, trimmed) { continue } s = append(s, trimmed) } return s }