From a1e2a46b5b5699d5c4d7cc914071b99abf2490a0 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 25 Mar 2025 17:00:37 -0300 Subject: [PATCH] sdk/hints: add badger implementation. --- sdk/hints/badgerh/db.go | 168 ++++++++++++++++++++++++++++++++++ sdk/hints/badgerh/keys.go | 40 ++++++++ sdk/hints/test/badger_test.go | 21 +++++ 3 files changed, 229 insertions(+) create mode 100644 sdk/hints/badgerh/db.go create mode 100644 sdk/hints/badgerh/keys.go create mode 100644 sdk/hints/test/badger_test.go diff --git a/sdk/hints/badgerh/db.go b/sdk/hints/badgerh/db.go new file mode 100644 index 0000000..bedb429 --- /dev/null +++ b/sdk/hints/badgerh/db.go @@ -0,0 +1,168 @@ +package badgerh + +import ( + "encoding/hex" + "fmt" + "math" + "slices" + + "github.com/dgraph-io/badger/v4" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/sdk/hints" +) + +var _ hints.HintsDB = (*BadgerHints)(nil) + +type BadgerHints struct { + db *badger.DB +} + +func NewBadgerHints(path string) (*BadgerHints, error) { + opts := badger.DefaultOptions(path) + db, err := badger.Open(opts) + if err != nil { + return nil, err + } + return &BadgerHints{db: db}, nil +} + +func (bh *BadgerHints) Close() { + bh.db.Close() +} + +func (bh *BadgerHints) Save(pubkey string, relay string, hintkey hints.HintKey, ts nostr.Timestamp) { + if now := nostr.Now(); ts > now { + ts = now + } + + err := bh.db.Update(func(txn *badger.Txn) error { + k := encodeKey(pubkey, relay) + var tss timestamps + item, err := txn.Get(k) + if err == nil { + err = item.Value(func(val []byte) error { + // there is a value, so we may update it or not + tss = parseValue(val) + return nil + }) + if err != nil { + return err + } + } else if err != badger.ErrKeyNotFound { + return err + } + + if tss[hintkey] < ts { + tss[hintkey] = ts + return txn.Set(k, encodeValue(tss)) + } + + return nil + }) + if err != nil { + nostr.InfoLogger.Printf("[sdk/hints/badger] unexpected error on save: %s\n", err) + } +} + +func (bh *BadgerHints) TopN(pubkey string, n int) []string { + type relayScore struct { + relay string + score int64 + } + + scores := make([]relayScore, 0, n) + err := bh.db.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.Prefix, _ = hex.DecodeString(pubkey) + it := txn.NewIterator(opts) + defer it.Close() + + for it.Seek(opts.Prefix); it.Valid(); it.Next() { + item := it.Item() + k := item.Key() + relay := string(k[32:]) + + err := item.Value(func(val []byte) error { + tss := parseValue(val) + scores = append(scores, relayScore{relay, tss.sum()}) + return nil + }) + if err != nil { + continue + } + } + return nil + }) + if err != nil { + nostr.InfoLogger.Printf("[sdk/hints/badger] unexpected error on topn: %s\n", err) + return nil + } + + slices.SortFunc(scores, func(a, b relayScore) int { + return int(b.score - a.score) + }) + + result := make([]string, 0, n) + for i, rs := range scores { + if i >= n { + break + } + result = append(result, rs.relay) + } + return result +} + +func (bh *BadgerHints) PrintScores() { + fmt.Println("= print scores") + + err := bh.db.View(func(txn *badger.Txn) error { + it := txn.NewIterator(badger.DefaultIteratorOptions) + defer it.Close() + + var lastPubkey string + i := 0 + + for it.Seek(nil); it.Valid(); it.Next() { + item := it.Item() + k := item.Key() + pubkey, relay := parseKey(k) + + if pubkey != lastPubkey { + fmt.Println("== relay scores for", pubkey) + lastPubkey = pubkey + i = 0 + } else { + i++ + } + + err := item.Value(func(val []byte) error { + tss := parseValue(val) + fmt.Printf(" %3d :: %30s ::> %12d\n", i, relay, tss.sum()) + return nil + }) + if err != nil { + continue + } + } + return nil + }) + if err != nil { + nostr.InfoLogger.Printf("[sdk/hints/badger] unexpected error on print: %s\n", err) + } +} + +type timestamps [4]nostr.Timestamp + +func (tss timestamps) sum() int64 { + now := nostr.Now() + 24*60*60 + var sum int64 + for i, ts := range tss { + if ts == 0 { + continue + } + value := float64(hints.HintKey(i).BasePoints()) * 10000000000 / math.Pow(float64(max(now-ts, 1)), 1.3) + // fmt.Println(" ", i, "value:", value) + sum += int64(value) + } + return sum +} diff --git a/sdk/hints/badgerh/keys.go b/sdk/hints/badgerh/keys.go new file mode 100644 index 0000000..5c74f18 --- /dev/null +++ b/sdk/hints/badgerh/keys.go @@ -0,0 +1,40 @@ +package badgerh + +import ( + "encoding/binary" + "encoding/hex" + "unsafe" + + "github.com/nbd-wtf/go-nostr" +) + +func encodeKey(pubhintkey, relay string) []byte { + k := make([]byte, 32+len(relay)) + hex.Decode(k[0:32], []byte(pubhintkey)) + copy(k[32:], unsafe.Slice(unsafe.StringData(relay), len(relay))) + return k +} + +func parseKey(k []byte) (pubkey string, relay string) { + pubkey = hex.EncodeToString(k[0:32]) + relay = string(k[32:]) + return +} + +func encodeValue(tss timestamps) []byte { + v := make([]byte, 16) + binary.LittleEndian.PutUint32(v[0:], uint32(tss[0])) + binary.LittleEndian.PutUint32(v[4:], uint32(tss[1])) + binary.LittleEndian.PutUint32(v[8:], uint32(tss[2])) + binary.LittleEndian.PutUint32(v[12:], uint32(tss[3])) + return v +} + +func parseValue(v []byte) timestamps { + return timestamps{ + nostr.Timestamp(binary.LittleEndian.Uint32(v[0:])), + nostr.Timestamp(binary.LittleEndian.Uint32(v[4:])), + nostr.Timestamp(binary.LittleEndian.Uint32(v[8:])), + nostr.Timestamp(binary.LittleEndian.Uint32(v[12:])), + } +} diff --git a/sdk/hints/test/badger_test.go b/sdk/hints/test/badger_test.go new file mode 100644 index 0000000..15f0bca --- /dev/null +++ b/sdk/hints/test/badger_test.go @@ -0,0 +1,21 @@ +package test + +import ( + "os" + "testing" + + "github.com/nbd-wtf/go-nostr/sdk/hints/badgerh" +) + +func TestBadgerHints(t *testing.T) { + path := "/tmp/tmpsdkhintsbadger" + os.RemoveAll(path) + + hdb, err := badgerh.NewBadgerHints(path) + if err != nil { + t.Fatal(err) + } + defer hdb.Close() + + runTestWith(t, hdb) +}