Compare commits

...

10 Commits

32 changed files with 316 additions and 182 deletions

View File

@@ -56,6 +56,11 @@ func (b *BleveBackend) Init() error {
return nil return nil
} }
func (b *BleveBackend) CountEvents(nostr.Filter) (uint32, error) { func (b *BleveBackend) CountEvents(filter nostr.Filter) (uint32, error) {
if filter.String() == "{}" {
count, err := b.index.DocCount()
return uint32(count), err
}
return 0, errors.New("not supported") return 0, errors.New("not supported")
} }

View File

@@ -5,7 +5,6 @@ import (
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore" "fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/eventstore/internal"
) )
func (b *BleveBackend) ReplaceEvent(evt nostr.Event) error { func (b *BleveBackend) ReplaceEvent(evt nostr.Event) error {
@@ -19,7 +18,7 @@ func (b *BleveBackend) ReplaceEvent(evt nostr.Event) error {
shouldStore := true shouldStore := true
for previous := range b.QueryEvents(filter, 1) { for previous := range b.QueryEvents(filter, 1) {
if internal.IsOlder(previous, evt) { if nostr.IsOlder(previous, evt) {
if err := b.DeleteEvent(previous.ID); err != nil { if err := b.DeleteEvent(previous.ID); err != nil {
return fmt.Errorf("failed to delete event for replacing: %w", err) return fmt.Errorf("failed to delete event for replacing: %w", err)
} }

View File

@@ -5,7 +5,6 @@ import (
"iter" "iter"
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/internal"
"go.etcd.io/bbolt" "go.etcd.io/bbolt"
) )
@@ -36,7 +35,7 @@ func (b *BoltBackend) ReplaceEvent(evt nostr.Event) error {
shouldStore := true shouldStore := true
for previous := range results { for previous := range results {
if internal.IsOlder(previous, evt) { if nostr.IsOlder(previous, evt) {
if err := b.delete(txn, previous.ID); err != nil { if err := b.delete(txn, previous.ID); err != nil {
return fmt.Errorf("failed to delete event %s for replacing: %w", previous.ID, err) return fmt.Errorf("failed to delete event %s for replacing: %w", previous.ID, err)
} }

View File

@@ -1,17 +1,11 @@
package internal package internal
import ( import (
"bytes"
"math" "math"
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
) )
func IsOlder(previous, next nostr.Event) bool {
return previous.CreatedAt < next.CreatedAt ||
(previous.CreatedAt == next.CreatedAt && bytes.Compare(previous.ID[:], next.ID[:]) == 1)
}
func ChooseNarrowestTag(filter nostr.Filter) (key string, values []string, goodness int) { func ChooseNarrowestTag(filter nostr.Filter) (key string, values []string, goodness int) {
var tagKey string var tagKey string
var tagValues []string var tagValues []string

View File

@@ -5,7 +5,6 @@ import (
"iter" "iter"
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/internal"
"github.com/PowerDNS/lmdb-go/lmdb" "github.com/PowerDNS/lmdb-go/lmdb"
) )
@@ -37,7 +36,7 @@ func (b *LMDBBackend) ReplaceEvent(evt nostr.Event) error {
shouldStore := true shouldStore := true
for previous := range results { for previous := range results {
if internal.IsOlder(previous, evt) { if nostr.IsOlder(previous, evt) {
if err := b.delete(txn, previous.ID); err != nil { if err := b.delete(txn, previous.ID); err != nil {
return fmt.Errorf("failed to delete event %s for replacing: %w", previous.ID, err) return fmt.Errorf("failed to delete event %s for replacing: %w", previous.ID, err)
} }

View File

@@ -6,7 +6,6 @@ import (
"runtime" "runtime"
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/internal"
"github.com/PowerDNS/lmdb-go/lmdb" "github.com/PowerDNS/lmdb-go/lmdb"
) )
@@ -72,7 +71,7 @@ func (il *IndexingLayer) ReplaceEvent(evt nostr.Event) error {
var acquiredFreeRangeFromDelete *position var acquiredFreeRangeFromDelete *position
shouldStore := true shouldStore := true
for previous := range results { for previous := range results {
if internal.IsOlder(previous, evt) { if nostr.IsOlder(previous, evt) {
if pos, shouldPurge, err := il.delete(mmmtxn, iltxn, previous.ID); err != nil { if pos, shouldPurge, err := il.delete(mmmtxn, iltxn, previous.ID); err != nil {
return fmt.Errorf("failed to delete event %s for replacing: %w", previous.ID, err) return fmt.Errorf("failed to delete event %s for replacing: %w", previous.ID, err)
} else if shouldPurge { } else if shouldPurge {

View File

@@ -10,7 +10,6 @@ import (
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore" "fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/eventstore/internal"
) )
var _ eventstore.Store = (*SliceStore)(nil) var _ eventstore.Store = (*SliceStore)(nil)
@@ -134,7 +133,7 @@ func (b *SliceStore) ReplaceEvent(evt nostr.Event) error {
shouldStore := true shouldStore := true
for previous := range b.QueryEvents(filter, 1) { for previous := range b.QueryEvents(filter, 1) {
if internal.IsOlder(previous, evt) { if nostr.IsOlder(previous, evt) {
if err := b.delete(previous.ID); err != nil { if err := b.delete(previous.ID); err != nil {
return fmt.Errorf("failed to delete event for replacing: %w", err) return fmt.Errorf("failed to delete event for replacing: %w", err)
} }

View File

@@ -155,14 +155,14 @@ func (rl *Relay) StartExpirationManager(
} }
go rl.expirationManager.start(rl.ctx) go rl.expirationManager.start(rl.ctx)
rl.Info.AddSupportedNIP(40) rl.Info.AddSupportedNIP("40")
} }
func (rl *Relay) DisableExpirationManager() { func (rl *Relay) DisableExpirationManager() {
rl.expirationManager.stop() rl.expirationManager.stop()
rl.expirationManager = nil rl.expirationManager = nil
idx := slices.Index(rl.Info.SupportedNIPs, 40) idx := slices.Index(rl.Info.SupportedNIPs, "40")
if idx != -1 { if idx != -1 {
rl.Info.SupportedNIPs[idx] = rl.Info.SupportedNIPs[len(rl.Info.SupportedNIPs)-1] rl.Info.SupportedNIPs[idx] = rl.Info.SupportedNIPs[len(rl.Info.SupportedNIPs)-1]
rl.Info.SupportedNIPs = rl.Info.SupportedNIPs[0 : len(rl.Info.SupportedNIPs)-1] rl.Info.SupportedNIPs = rl.Info.SupportedNIPs[0 : len(rl.Info.SupportedNIPs)-1]

View File

@@ -31,7 +31,7 @@ func New(rl *khatru.Relay, repositoryDir string) *GraspServer {
}, },
} }
rl.Info.AddSupportedNIP(34) rl.Info.AddSupportedNIP("34")
rl.Info.SupportedGrasps = append(rl.Info.SupportedGrasps, "GRASP-01") rl.Info.SupportedGrasps = append(rl.Info.SupportedGrasps, "GRASP-01")
base := rl.Router() base := rl.Router()

View File

@@ -1,19 +1,11 @@
package khatru package khatru
import ( import (
"bytes"
"net" "net"
"net/http" "net/http"
"strings" "strings"
"fiatjaf.com/nostr"
) )
func isOlder(previous, next *nostr.Event) bool {
return previous.CreatedAt < next.CreatedAt ||
(previous.CreatedAt == next.CreatedAt && bytes.Compare(previous.ID[:], next.ID[:]) == 1)
}
var privateMasks = func() []net.IPNet { var privateMasks = func() []net.IPNet {
privateCIDRs := []string{ privateCIDRs := []string{
"127.0.0.0/8", "127.0.0.0/8",

View File

@@ -12,13 +12,13 @@ func (rl *Relay) HandleNIP11(w http.ResponseWriter, r *http.Request) {
info := *rl.Info info := *rl.Info
if nil != rl.DeleteEvent { if nil != rl.DeleteEvent {
info.AddSupportedNIP(9) info.AddSupportedNIP("9")
} }
if nil != rl.Count { if nil != rl.Count {
info.AddSupportedNIP(45) info.AddSupportedNIP("45")
} }
if rl.Negentropy { if rl.Negentropy {
info.AddSupportedNIP(77) info.AddSupportedNIP("77")
} }
// resolve relative icon and banner URLs against base URL // resolve relative icon and banner URLs against base URL

View File

@@ -30,7 +30,7 @@ func NewRelay() *Relay {
Info: &nip11.RelayInformationDocument{ Info: &nip11.RelayInformationDocument{
Software: "https://pkg.go.dev/fiatjaf.com/nostr/khatru", Software: "https://pkg.go.dev/fiatjaf.com/nostr/khatru",
Version: "n/a", Version: "n/a",
SupportedNIPs: []any{1, 11, 42, 70, 86}, SupportedNIPs: []string{"1", "11", "42", "70", "86"},
}, },
upgrader: websocket.Upgrader{ upgrader: websocket.Upgrader{

View File

@@ -9,30 +9,30 @@ import (
func TestAddSupportedNIP(t *testing.T) { func TestAddSupportedNIP(t *testing.T) {
info := RelayInformationDocument{} info := RelayInformationDocument{}
info.AddSupportedNIP(12) info.AddSupportedNIP("12")
info.AddSupportedNIP(12) info.AddSupportedNIP("12")
info.AddSupportedNIP(13) info.AddSupportedNIP("13")
info.AddSupportedNIP(1) info.AddSupportedNIP("1")
info.AddSupportedNIP(12) info.AddSupportedNIP("12")
info.AddSupportedNIP(44) info.AddSupportedNIP("44")
info.AddSupportedNIP(2) info.AddSupportedNIP("2")
info.AddSupportedNIP(13) info.AddSupportedNIP("13")
info.AddSupportedNIP(2) info.AddSupportedNIP("2")
info.AddSupportedNIP(13) info.AddSupportedNIP("13")
info.AddSupportedNIP(0) info.AddSupportedNIP("0")
info.AddSupportedNIP(17) info.AddSupportedNIP("17")
info.AddSupportedNIP(19) info.AddSupportedNIP("19")
info.AddSupportedNIP(1) info.AddSupportedNIP("1")
info.AddSupportedNIP(18) info.AddSupportedNIP("18")
assert.Contains(t, info.SupportedNIPs, 0, 1, 2, 12, 13, 17, 18, 19, 44) assert.Contains(t, info.SupportedNIPs, "0", "1", "2", "12", "13", "17", "18", "19", "44")
} }
func TestAddSupportedNIPs(t *testing.T) { func TestAddSupportedNIPs(t *testing.T) {
info := RelayInformationDocument{} info := RelayInformationDocument{}
info.AddSupportedNIPs([]int{0, 1, 2, 12, 13, 17, 18, 19, 44}) info.AddSupportedNIPs([]int{"0", "1", "2", "12", "13", "17", "18", "19", "44"})
assert.Contains(t, info.SupportedNIPs, 0, 1, 2, 12, 13, 17, 18, 19, 44) assert.Contains(t, info.SupportedNIPs, "0", "1", "2", "12", "13", "17", "18", "19", "44")
} }
func TestFetch(t *testing.T) { func TestFetch(t *testing.T) {

View File

@@ -14,7 +14,7 @@ type RelayInformationDocument struct {
PubKey *nostr.PubKey `json:"pubkey,omitempty"` PubKey *nostr.PubKey `json:"pubkey,omitempty"`
Self *nostr.PubKey `json:"self,omitempty"` Self *nostr.PubKey `json:"self,omitempty"`
Contact string `json:"contact,omitempty"` Contact string `json:"contact,omitempty"`
SupportedNIPs []any `json:"supported_nips,omitempty"` SupportedNIPs []string `json:"supported_nips,omitempty"`
Software string `json:"software,omitempty"` Software string `json:"software,omitempty"`
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
@@ -33,16 +33,16 @@ type RelayInformationDocument struct {
SupportedGrasps []string `json:"supported_grasps,omitempty"` SupportedGrasps []string `json:"supported_grasps,omitempty"`
} }
func (info *RelayInformationDocument) AddSupportedNIP(number int) { func (info *RelayInformationDocument) AddSupportedNIP(nip string) {
idx := slices.IndexFunc(info.SupportedNIPs, func(n any) bool { return n == number }) idx := slices.IndexFunc(info.SupportedNIPs, func(n string) bool { return n == nip })
if idx != -1 { if idx != -1 {
return return
} }
info.SupportedNIPs = append(info.SupportedNIPs, number) info.SupportedNIPs = append(info.SupportedNIPs, nip)
} }
func (info *RelayInformationDocument) AddSupportedNIPs(numbers []int) { func (info *RelayInformationDocument) AddSupportedNIPs(numbers []string) {
for _, n := range numbers { for _, n := range numbers {
info.AddSupportedNIP(n) info.AddSupportedNIP(n)
} }

View File

@@ -30,7 +30,9 @@ func Decode(bech32string string) (prefix string, value any, err error) {
if len(data) != 32 { if len(data) != 32 {
return prefix, nil, fmt.Errorf("note should be 32 bytes (%d)", len(data)) return prefix, nil, fmt.Errorf("note should be 32 bytes (%d)", len(data))
} }
return prefix, nostr.ID(data[0:32]), nil return prefix, nostr.EventPointer{
ID: nostr.ID(data[0:32]),
}, nil
case "npub": case "npub":
if len(data) != 32 { if len(data) != 32 {
return prefix, nil, fmt.Errorf("npub should be 32 bytes (%d)", len(data)) return prefix, nil, fmt.Errorf("npub should be 32 bytes (%d)", len(data))

View File

@@ -1,7 +1,9 @@
package nip46 package nip46
import ( import (
"errors"
"fmt" "fmt"
"slices"
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip44" "fiatjaf.com/nostr/nip44"
@@ -10,6 +12,9 @@ import (
type Session struct { type Session struct {
PublicKey nostr.PubKey PublicKey nostr.PubKey
ConversationKey [32]byte ConversationKey [32]byte
duplicatesBuf [6]string
serial int
} }
type RelayReadWrite struct { type RelayReadWrite struct {
@@ -17,7 +22,9 @@ type RelayReadWrite struct {
Write bool `json:"write"` Write bool `json:"write"`
} }
func (s Session) ParseRequest(event nostr.Event) (Request, error) { var AlreadyHandled = errors.New("already handled this request")
func (s *Session) ParseRequest(event nostr.Event) (Request, error) {
var req Request var req Request
plain, err := nip44.Decrypt(event.Content, s.ConversationKey) plain, err := nip44.Decrypt(event.Content, s.ConversationKey)
@@ -26,6 +33,15 @@ func (s Session) ParseRequest(event nostr.Event) (Request, error) {
} }
err = json.Unmarshal([]byte(plain), &req) err = json.Unmarshal([]byte(plain), &req)
// discard duplicates
if slices.Contains(s.duplicatesBuf[:], req.ID) {
return req, AlreadyHandled
}
s.duplicatesBuf[s.serial%len(s.duplicatesBuf)] = req.ID
s.serial++
return req, err return req, err
} }

View File

@@ -16,11 +16,12 @@ import (
) )
type BunkerClient struct { type BunkerClient struct {
Relays []string
serial atomic.Uint64 serial atomic.Uint64
clientSecretKey [32]byte clientSecretKey [32]byte
pool *nostr.Pool pool *nostr.Pool
target nostr.PubKey target nostr.PubKey
relays []string
conversationKey [32]byte // nip44 conversationKey [32]byte // nip44
listeners *xsync.MapOf[string, chan Response] listeners *xsync.MapOf[string, chan Response]
idPrefix string idPrefix string
@@ -116,23 +117,27 @@ func NewBunker(
clientPublicKey := nostr.GetPublicKey(clientSecretKey) clientPublicKey := nostr.GetPublicKey(clientSecretKey)
conversationKey, _ := nip44.GenerateConversationKey(targetPublicKey, clientSecretKey) conversationKey, _ := nip44.GenerateConversationKey(targetPublicKey, clientSecretKey)
now := nostr.Now()
bunker := &BunkerClient{ bunker := &BunkerClient{
pool: pool, pool: pool,
clientSecretKey: clientSecretKey, clientSecretKey: clientSecretKey,
target: targetPublicKey, target: targetPublicKey,
relays: relays, Relays: relays,
conversationKey: conversationKey, conversationKey: conversationKey,
listeners: xsync.NewMapOf[string, chan Response](), listeners: xsync.NewMapOf[string, chan Response](),
onAuth: onAuth, onAuth: onAuth,
idPrefix: "nl-" + strconv.Itoa(rand.Intn(65536)), idPrefix: "nl-" + strconv.Itoa(rand.Intn(65536)),
} }
cancellableCtx, cancel := context.WithCancel(ctx)
_ = cancel
go func() { go func() {
events := pool.SubscribeMany(ctx, relays, nostr.Filter{ events := pool.SubscribeMany(cancellableCtx, relays, nostr.Filter{
Tags: nostr.TagMap{"p": []string{clientPublicKey.Hex()}}, Tags: nostr.TagMap{"p": []string{clientPublicKey.Hex()}},
Kinds: []nostr.Kind{nostr.KindNostrConnect}, Kinds: []nostr.Kind{nostr.KindNostrConnect},
Since: nostr.Now(), Since: now,
LimitZero: true, LimitZero: true,
}, nostr.SubscriptionOptions{ }, nostr.SubscriptionOptions{
Label: "bunker46client", Label: "bunker46client",
@@ -167,6 +172,14 @@ func NewBunker(
} }
}() }()
// attempt switch_relays once every 10 times
if now%10 == 0 {
if newRelays, _ := bunker.SwitchRelays(ctx); newRelays != nil {
cancel()
bunker = NewBunker(ctx, clientSecretKey, targetPublicKey, newRelays, pool, func(string) {})
}
}
return bunker return bunker
} }
@@ -178,6 +191,15 @@ func (bunker *BunkerClient) Ping(ctx context.Context) error {
return nil return nil
} }
func (bunker *BunkerClient) SwitchRelays(ctx context.Context) ([]string, error) {
var res []string
_, err := bunker.RPC(ctx, "switch_relays", res)
if err != nil {
return nil, err
}
return res, nil
}
func (bunker *BunkerClient) GetPublicKey(ctx context.Context) (nostr.PubKey, error) { func (bunker *BunkerClient) GetPublicKey(ctx context.Context) (nostr.PubKey, error) {
if bunker.getPublicKeyResponse != nostr.ZeroPK { if bunker.getPublicKeyResponse != nostr.ZeroPK {
return bunker.getPublicKeyResponse, nil return bunker.getPublicKeyResponse, nil
@@ -283,7 +305,7 @@ func (bunker *BunkerClient) RPC(ctx context.Context, method string, params []str
relayConnectionWorked := make(chan struct{}) relayConnectionWorked := make(chan struct{})
bunkerConnectionWorked := make(chan struct{}) bunkerConnectionWorked := make(chan struct{})
for _, url := range bunker.relays { for _, url := range bunker.Relays {
go func(url string) { go func(url string) {
relay, err := bunker.pool.EnsureRelay(url) relay, err := bunker.pool.EnsureRelay(url)
if err == nil { if err == nil {

View File

@@ -14,7 +14,7 @@ var _ Signer = (*DynamicSigner)(nil)
type DynamicSigner struct { type DynamicSigner struct {
// { [handlePubkey]: {[clientKey]: Session} } // { [handlePubkey]: {[clientKey]: Session} }
sessions map[nostr.PubKey]map[nostr.PubKey]Session sessions map[nostr.PubKey]map[nostr.PubKey]*Session
// used for switch_relays call // used for switch_relays call
DefaultRelays []string DefaultRelays []string
@@ -47,7 +47,7 @@ type DynamicSigner struct {
} }
func (p *DynamicSigner) Init() { func (p *DynamicSigner) Init() {
p.sessions = make(map[nostr.PubKey]map[nostr.PubKey]Session) p.sessions = make(map[nostr.PubKey]map[nostr.PubKey]*Session)
} }
func (p *DynamicSigner) HandleRequest(ctx context.Context, event nostr.Event) ( func (p *DynamicSigner) HandleRequest(ctx context.Context, event nostr.Event) (
@@ -85,14 +85,14 @@ func (p *DynamicSigner) HandleRequest(ctx context.Context, event nostr.Event) (
handlerSessions, exists := p.sessions[handlerPubkey] handlerSessions, exists := p.sessions[handlerPubkey]
if !exists { if !exists {
handlerSessions = make(map[nostr.PubKey]Session) handlerSessions = make(map[nostr.PubKey]*Session)
p.sessions[handlerPubkey] = handlerSessions p.sessions[handlerPubkey] = handlerSessions
} }
session, exists := handlerSessions[event.PubKey] session, exists := handlerSessions[event.PubKey]
if !exists { if !exists {
// create session if it doesn't exist // create session if it doesn't exist
session = Session{} session = &Session{}
session.ConversationKey, err = nip44.GenerateConversationKey(event.PubKey, handlerSecret) session.ConversationKey, err = nip44.GenerateConversationKey(event.PubKey, handlerSecret)
if err != nil { if err != nil {

View File

@@ -5,14 +5,11 @@ import (
"crypto/rand" "crypto/rand"
"errors" "errors"
"fmt" "fmt"
mrand "math/rand"
"net/url" "net/url"
"strconv"
"strings" "strings"
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip44" "fiatjaf.com/nostr/nip44"
"github.com/puzpuzpuz/xsync/v3"
) )
var NoConnectionReceived = errors.New("relay connections ended without a bunker connection established") var NoConnectionReceived = errors.New("relay connections ended without a bunker connection established")
@@ -112,16 +109,17 @@ func NewBunkerFromNostrConnect(
if req.Result != "" { if req.Result != "" {
if req.Result == secret { if req.Result == secret {
// secret validation passed - connection established // secret validation passed - connection established
return &BunkerClient{ cancellableCtx, cancel := context.WithCancel(ctx)
pool: pool, _ = cancel
clientSecretKey: clientSecretKey, bunker := NewBunker(cancellableCtx, clientSecretKey, targetPublicKey, relayURLs, pool, func(string) {})
target: targetPublicKey,
relays: relayURLs, // attempt switch_relays
conversationKey: conversationKey, if newRelays, _ := bunker.SwitchRelays(ctx); newRelays != nil {
listeners: xsync.NewMapOf[string, chan Response](), cancel()
onAuth: func(string) {}, bunker = NewBunker(ctx, clientSecretKey, targetPublicKey, newRelays, pool, func(string) {})
idPrefix: "nl-" + strconv.Itoa(mrand.Intn(65536)), }
}, nil
return bunker, nil
} }
} }
} }

View File

@@ -3,6 +3,8 @@ package nip46
import ( import (
"context" "context"
"fmt" "fmt"
"net/url"
"strconv"
"sync" "sync"
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
@@ -14,8 +16,8 @@ import (
var _ Signer = (*StaticKeySigner)(nil) var _ Signer = (*StaticKeySigner)(nil)
type StaticKeySigner struct { type StaticKeySigner struct {
secretKey [32]byte secretKey nostr.SecretKey
sessions map[nostr.PubKey]Session sessions map[nostr.PubKey]*Session
sync.Mutex sync.Mutex
@@ -28,11 +30,11 @@ type StaticKeySigner struct {
func NewStaticKeySigner(secretKey [32]byte) StaticKeySigner { func NewStaticKeySigner(secretKey [32]byte) StaticKeySigner {
return StaticKeySigner{ return StaticKeySigner{
secretKey: secretKey, secretKey: secretKey,
sessions: make(map[nostr.PubKey]Session), sessions: make(map[nostr.PubKey]*Session),
} }
} }
func (p *StaticKeySigner) getOrCreateSession(clientPubkey nostr.PubKey) (Session, error) { func (p *StaticKeySigner) getOrCreateSession(clientPubkey nostr.PubKey) (*Session, error) {
p.Lock() p.Lock()
defer p.Unlock() defer p.Unlock()
@@ -43,21 +45,57 @@ func (p *StaticKeySigner) getOrCreateSession(clientPubkey nostr.PubKey) (Session
ck, err := nip44.GenerateConversationKey(clientPubkey, p.secretKey) ck, err := nip44.GenerateConversationKey(clientPubkey, p.secretKey)
if err != nil { if err != nil {
return Session{}, fmt.Errorf("failed to compute shared secret: %w", err) return nil, fmt.Errorf("failed to compute shared secret: %w", err)
} }
pubkey := nostr.GetPublicKey(p.secretKey) session = &Session{
session = Session{ PublicKey: p.secretKey.Public(),
PublicKey: pubkey,
ConversationKey: ck, ConversationKey: ck,
} }
// add to pool // add to pool
p.sessions[pubkey] = session p.sessions[clientPubkey] = session
return session, nil return session, nil
} }
// HandleNostrConnectURI works like HandleRequest, but takes a nostrconnect:// URI as input, as scanned/pasted
// by the user, produced by the client.
func (p *StaticKeySigner) HandleNostrConnectURI(ctx context.Context, uri *url.URL) (
resp Response,
eventResponse nostr.Event,
err error,
) {
clientPublicKey, err := nostr.PubKeyFromHex(uri.Host)
if err != nil {
return resp, eventResponse, err
}
secret := uri.Query().Get("secret")
// pretend they started with a request
conversationKey, err := nip44.GenerateConversationKey(clientPublicKey, p.secretKey)
if err != nil {
return resp, eventResponse, err
}
reqj, _ := json.Marshal(Request{
ID: "nostrconnect-" + strconv.FormatInt(int64(nostr.Now()), 10),
Method: "imagined-nostrconnect",
Params: []string{clientPublicKey.Hex(), secret},
})
ciphertext, err := nip44.Encrypt(string(reqj), conversationKey)
if err != nil {
return resp, eventResponse, err
}
_, resp, eventResponse, err = p.HandleRequest(ctx, nostr.Event{
PubKey: clientPublicKey,
Kind: nostr.KindNostrConnect,
Content: ciphertext,
})
return resp, eventResponse, err
}
func (p *StaticKeySigner) HandleRequest(_ context.Context, event nostr.Event) ( func (p *StaticKeySigner) HandleRequest(_ context.Context, event nostr.Event) (
req Request, req Request,
resp Response, resp Response,
@@ -85,6 +123,14 @@ func (p *StaticKeySigner) HandleRequest(_ context.Context, event nostr.Event) (
var resultErr error var resultErr error
switch req.Method { switch req.Method {
case "imagined-nostrconnect":
// this is a fake request we pretend has existed, but was actually just we reading the nostrconnect:// uri
if len(req.Params) < 2 || req.Params[1] == "" {
resultErr = fmt.Errorf("needs a second argument 'secret'")
break
}
result = req.Params[1]
harmless = true
case "connect": case "connect":
if len(req.Params) >= 2 { if len(req.Params) >= 2 {
secret = req.Params[1] secret = req.Params[1]

View File

@@ -1,6 +1,8 @@
package blossom package blossom
import "mime" import (
"mime"
)
func GetExtension(mimetype string) string { func GetExtension(mimetype string) string {
if mimetype == "" { if mimetype == "" {
@@ -24,6 +26,9 @@ func GetExtension(mimetype string) string {
exts, _ := mime.ExtensionsByType(mimetype) exts, _ := mime.ExtensionsByType(mimetype)
if len(exts) > 0 { if len(exts) > 0 {
if exts[0] == ".moov" {
return ".mov"
}
return exts[0] return exts[0]
} }

View File

@@ -190,9 +190,11 @@ func (r *Relay) handleMessage(message string) {
} }
r.challenge = *env.Challenge r.challenge = *env.Challenge
if r.authHandler != nil { if r.authHandler != nil {
r.Auth(r.Context(), func(ctx context.Context, evt *Event) error { go func() {
return r.authHandler(ctx, r, evt) r.Auth(r.Context(), func(ctx context.Context, evt *Event) error {
}) return r.authHandler(ctx, r, evt)
})
}()
} }
case *EventEnvelope: case *EventEnvelope:
// we already have the subscription from the pre-check above, so we can just reuse it // we already have the subscription from the pre-check above, so we can just reuse it

View File

@@ -31,14 +31,15 @@ func (db *HintDB) Save(pubkey nostr.PubKey, relay string, key hints.HintKey, ts
ts = now ts = now
} }
db.Lock()
defer db.Unlock()
relayIndex := slices.Index(db.RelayBySerial, relay) relayIndex := slices.Index(db.RelayBySerial, relay)
if relayIndex == -1 { if relayIndex == -1 {
relayIndex = len(db.RelayBySerial) relayIndex = len(db.RelayBySerial)
db.RelayBySerial = append(db.RelayBySerial, relay) db.RelayBySerial = append(db.RelayBySerial, relay)
} }
db.Lock()
defer db.Unlock()
// fmt.Println(" ", relay, "index", relayIndex, "--", "adding", hints.HintKey(key).String(), ts) // fmt.Println(" ", relay, "index", relayIndex, "--", "adding", hints.HintKey(key).String(), ts)
entries, _ := db.OrderedRelaysByPubKey[pubkey] entries, _ := db.OrderedRelaysByPubKey[pubkey]

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"slices" "slices"
"sync" "sync"
"sync/atomic"
"time" "time"
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
@@ -23,7 +24,7 @@ type TagItemWithValue[V comparable] interface {
var ( var (
genericListMutexes = [60]sync.Mutex{} genericListMutexes = [60]sync.Mutex{}
valueWasJustCached = [60]bool{} valueWasJustCached = [60]atomic.Bool{}
) )
func fetchGenericList[V comparable, I TagItemWithValue[V]]( func fetchGenericList[V comparable, I TagItemWithValue[V]](
@@ -42,10 +43,9 @@ func fetchGenericList[V comparable, I TagItemWithValue[V]](
lockIdx := (nostr.Kind(n) + actualKind) % 60 lockIdx := (nostr.Kind(n) + actualKind) % 60
genericListMutexes[lockIdx].Lock() genericListMutexes[lockIdx].Lock()
if valueWasJustCached[lockIdx] { if valueWasJustCached[lockIdx].CompareAndSwap(true, false) {
// this ensures the cache has had time to commit the values // this ensures the cache has had time to commit the values
// so we don't repeat a fetch immediately after the other // so we don't repeat a fetch immediately after the other
valueWasJustCached[lockIdx] = false
time.Sleep(time.Millisecond * 10) time.Sleep(time.Millisecond * 10)
} }
@@ -83,7 +83,7 @@ func fetchGenericList[V comparable, I TagItemWithValue[V]](
// and finally save this to cache // and finally save this to cache
cache.SetWithTTL(pubkey, v, time.Hour*6) cache.SetWithTTL(pubkey, v, time.Hour*6)
valueWasJustCached[lockIdx] = true valueWasJustCached[lockIdx].Store(true)
return v, true return v, true
} }
@@ -99,7 +99,7 @@ func fetchGenericList[V comparable, I TagItemWithValue[V]](
// save cache even if we didn't get anything // save cache even if we didn't get anything
cache.SetWithTTL(pubkey, v, time.Hour*6) cache.SetWithTTL(pubkey, v, time.Hour*6)
valueWasJustCached[lockIdx] = true valueWasJustCached[lockIdx].Store(true)
return v, false return v, false
} }

View File

@@ -12,18 +12,22 @@ type EventRef struct{ nostr.Pointer }
func (e EventRef) Value() string { return e.Pointer.AsTagReference() } func (e EventRef) Value() string { return e.Pointer.AsTagReference() }
func (sys *System) FetchBookmarkList(ctx context.Context, pubkey nostr.PubKey) GenericList[string, EventRef] { func (sys *System) FetchBookmarkList(ctx context.Context, pubkey nostr.PubKey) GenericList[string, EventRef] {
if sys.BookmarkListCache == nil { sys.bookmarkListCacheOnce.Do(func() {
sys.BookmarkListCache = cache_memory.New[GenericList[string, EventRef]](1000) if sys.BookmarkListCache == nil {
} sys.BookmarkListCache = cache_memory.New[GenericList[string, EventRef]](1000)
}
})
ml, _ := fetchGenericList(sys, ctx, pubkey, 10003, kind_10003, parseEventRef, sys.BookmarkListCache) ml, _ := fetchGenericList(sys, ctx, pubkey, 10003, kind_10003, parseEventRef, sys.BookmarkListCache)
return ml return ml
} }
func (sys *System) FetchPinList(ctx context.Context, pubkey nostr.PubKey) GenericList[string, EventRef] { func (sys *System) FetchPinList(ctx context.Context, pubkey nostr.PubKey) GenericList[string, EventRef] {
if sys.PinListCache == nil { sys.pinListCacheOnce.Do(func() {
sys.PinListCache = cache_memory.New[GenericList[string, EventRef]](1000) if sys.PinListCache == nil {
} sys.PinListCache = cache_memory.New[GenericList[string, EventRef]](1000)
}
})
ml, _ := fetchGenericList(sys, ctx, pubkey, 10001, kind_10001, parseEventRef, sys.PinListCache) ml, _ := fetchGenericList(sys, ctx, pubkey, 10001, kind_10001, parseEventRef, sys.PinListCache)
return ml return ml

View File

@@ -18,27 +18,33 @@ type ProfileRef struct {
func (f ProfileRef) Value() nostr.PubKey { return f.Pubkey } func (f ProfileRef) Value() nostr.PubKey { return f.Pubkey }
func (sys *System) FetchFollowList(ctx context.Context, pubkey nostr.PubKey) GenericList[nostr.PubKey, ProfileRef] { func (sys *System) FetchFollowList(ctx context.Context, pubkey nostr.PubKey) GenericList[nostr.PubKey, ProfileRef] {
if sys.FollowListCache == nil { sys.followListCacheOnce.Do(func() {
sys.FollowListCache = cache_memory.New[GenericList[nostr.PubKey, ProfileRef]](1000) if sys.FollowListCache == nil {
} sys.FollowListCache = cache_memory.New[GenericList[nostr.PubKey, ProfileRef]](1000)
}
})
fl, _ := fetchGenericList(sys, ctx, pubkey, 3, kind_3, parseProfileRef, sys.FollowListCache) fl, _ := fetchGenericList(sys, ctx, pubkey, 3, kind_3, parseProfileRef, sys.FollowListCache)
return fl return fl
} }
func (sys *System) FetchMuteList(ctx context.Context, pubkey nostr.PubKey) GenericList[nostr.PubKey, ProfileRef] { func (sys *System) FetchMuteList(ctx context.Context, pubkey nostr.PubKey) GenericList[nostr.PubKey, ProfileRef] {
if sys.MuteListCache == nil { sys.muteListCacheOnce.Do(func() {
sys.MuteListCache = cache_memory.New[GenericList[nostr.PubKey, ProfileRef]](1000) if sys.MuteListCache == nil {
} sys.MuteListCache = cache_memory.New[GenericList[nostr.PubKey, ProfileRef]](1000)
}
})
ml, _ := fetchGenericList(sys, ctx, pubkey, 10000, kind_10000, parseProfileRef, sys.MuteListCache) ml, _ := fetchGenericList(sys, ctx, pubkey, 10000, kind_10000, parseProfileRef, sys.MuteListCache)
return ml return ml
} }
func (sys *System) FetchFollowSets(ctx context.Context, pubkey nostr.PubKey) GenericSets[nostr.PubKey, ProfileRef] { func (sys *System) FetchFollowSets(ctx context.Context, pubkey nostr.PubKey) GenericSets[nostr.PubKey, ProfileRef] {
if sys.FollowSetsCache == nil { sys.followSetsCacheOnce.Do(func() {
sys.FollowSetsCache = cache_memory.New[GenericSets[nostr.PubKey, ProfileRef]](1000) if sys.FollowSetsCache == nil {
} sys.FollowSetsCache = cache_memory.New[GenericSets[nostr.PubKey, ProfileRef]](1000)
}
})
ml, _ := fetchGenericSets(sys, ctx, pubkey, 30000, kind_30000, parseProfileRef, sys.FollowSetsCache) ml, _ := fetchGenericSets(sys, ctx, pubkey, 30000, kind_30000, parseProfileRef, sys.FollowSetsCache)
return ml return ml

View File

@@ -25,27 +25,33 @@ func (sys *System) FetchRelayList(ctx context.Context, pubkey nostr.PubKey) Gene
} }
func (sys *System) FetchBlockedRelayList(ctx context.Context, pubkey nostr.PubKey) GenericList[string, RelayURL] { func (sys *System) FetchBlockedRelayList(ctx context.Context, pubkey nostr.PubKey) GenericList[string, RelayURL] {
if sys.BlockedRelayListCache == nil { sys.blockedRelayListCacheOnce.Do(func() {
sys.BlockedRelayListCache = cache_memory.New[GenericList[string, RelayURL]](1000) if sys.BlockedRelayListCache == nil {
} sys.BlockedRelayListCache = cache_memory.New[GenericList[string, RelayURL]](1000)
}
})
ml, _ := fetchGenericList(sys, ctx, pubkey, 10006, kind_10006, parseRelayURL, sys.BlockedRelayListCache) ml, _ := fetchGenericList(sys, ctx, pubkey, 10006, kind_10006, parseRelayURL, sys.BlockedRelayListCache)
return ml return ml
} }
func (sys *System) FetchSearchRelayList(ctx context.Context, pubkey nostr.PubKey) GenericList[string, RelayURL] { func (sys *System) FetchSearchRelayList(ctx context.Context, pubkey nostr.PubKey) GenericList[string, RelayURL] {
if sys.SearchRelayListCache == nil { sys.searchRelayListCacheOnce.Do(func() {
sys.SearchRelayListCache = cache_memory.New[GenericList[string, RelayURL]](1000) if sys.SearchRelayListCache == nil {
} sys.SearchRelayListCache = cache_memory.New[GenericList[string, RelayURL]](1000)
}
})
ml, _ := fetchGenericList(sys, ctx, pubkey, 10007, kind_10007, parseRelayURL, sys.SearchRelayListCache) ml, _ := fetchGenericList(sys, ctx, pubkey, 10007, kind_10007, parseRelayURL, sys.SearchRelayListCache)
return ml return ml
} }
func (sys *System) FetchRelaySets(ctx context.Context, pubkey nostr.PubKey) GenericSets[string, RelayURL] { func (sys *System) FetchRelaySets(ctx context.Context, pubkey nostr.PubKey) GenericSets[string, RelayURL] {
if sys.RelaySetsCache == nil { sys.relaySetsCacheOnce.Do(func() {
sys.RelaySetsCache = cache_memory.New[GenericSets[string, RelayURL]](1000) if sys.RelaySetsCache == nil {
} sys.RelaySetsCache = cache_memory.New[GenericSets[string, RelayURL]](1000)
}
})
ml, _ := fetchGenericSets(sys, ctx, pubkey, 30002, kind_30002, parseRelayURL, sys.RelaySetsCache) ml, _ := fetchGenericSets(sys, ctx, pubkey, 30002, kind_30002, parseRelayURL, sys.RelaySetsCache)
return ml return ml

View File

@@ -12,18 +12,22 @@ type Topic string
func (r Topic) Value() string { return string(r) } func (r Topic) Value() string { return string(r) }
func (sys *System) FetchTopicList(ctx context.Context, pubkey nostr.PubKey) GenericList[string, Topic] { func (sys *System) FetchTopicList(ctx context.Context, pubkey nostr.PubKey) GenericList[string, Topic] {
if sys.TopicListCache == nil { sys.topicListCacheOnce.Do(func() {
sys.TopicListCache = cache_memory.New[GenericList[string, Topic]](1000) if sys.TopicListCache == nil {
} sys.TopicListCache = cache_memory.New[GenericList[string, Topic]](1000)
}
})
ml, _ := fetchGenericList(sys, ctx, pubkey, 10015, kind_10015, parseTopicString, sys.TopicListCache) ml, _ := fetchGenericList(sys, ctx, pubkey, 10015, kind_10015, parseTopicString, sys.TopicListCache)
return ml return ml
} }
func (sys *System) FetchTopicSets(ctx context.Context, pubkey nostr.PubKey) GenericSets[string, Topic] { func (sys *System) FetchTopicSets(ctx context.Context, pubkey nostr.PubKey) GenericSets[string, Topic] {
if sys.TopicSetsCache == nil { sys.topicSetsCacheOnce.Do(func() {
sys.TopicSetsCache = cache_memory.New[GenericSets[string, Topic]](1000) if sys.TopicSetsCache == nil {
} sys.TopicSetsCache = cache_memory.New[GenericSets[string, Topic]](1000)
}
})
ml, _ := fetchGenericSets(sys, ctx, pubkey, 30015, kind_30015, parseTopicString, sys.TopicSetsCache) ml, _ := fetchGenericSets(sys, ctx, pubkey, 30015, kind_30015, parseTopicString, sys.TopicSetsCache)
return ml return ml

View File

@@ -2,12 +2,13 @@ package sdk
import ( import (
"context" "context"
"sync/atomic"
"time" "time"
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
) )
var outboxShortTermCache = [256]ostcEntry{} var outboxShortTermCache = [256]atomic.Pointer[ostcEntry]{}
type ostcEntry struct { type ostcEntry struct {
pubkey nostr.PubKey pubkey nostr.PubKey
@@ -22,7 +23,9 @@ type ostcEntry struct {
func (sys *System) FetchOutboxRelays(ctx context.Context, pubkey nostr.PubKey, n int) []string { func (sys *System) FetchOutboxRelays(ctx context.Context, pubkey nostr.PubKey, n int) []string {
ostcIndex := pubkey[7] ostcIndex := pubkey[7]
now := time.Now() now := time.Now()
if entry := outboxShortTermCache[ostcIndex]; entry.pubkey == pubkey && entry.when.Add(time.Minute*2).After(now) { if entry := outboxShortTermCache[ostcIndex].Load(); entry != nil &&
entry.pubkey == pubkey &&
entry.when.Add(time.Minute*2).After(now) {
return entry.relays return entry.relays
} }
@@ -38,7 +41,7 @@ func (sys *System) FetchOutboxRelays(ctx context.Context, pubkey nostr.PubKey, n
// we will have a reference to a thing that the caller to this function may change at will) // we will have a reference to a thing that the caller to this function may change at will)
relaysCopy := make([]string, len(relays)) relaysCopy := make([]string, len(relays))
copy(relaysCopy, relays) copy(relaysCopy, relays)
outboxShortTermCache[ostcIndex] = ostcEntry{pubkey, relaysCopy, now} outboxShortTermCache[ostcIndex].Store(&ostcEntry{pubkey, relaysCopy, now})
if len(relays) > n { if len(relays) > n {
relays = relays[0:n] relays = relays[0:n]

View File

@@ -31,10 +31,9 @@ func fetchGenericSets[V comparable, I TagItemWithValue[V]](
lockIdx := (nostr.Kind(n) + actualKind) % 60 lockIdx := (nostr.Kind(n) + actualKind) % 60
genericListMutexes[lockIdx].Lock() genericListMutexes[lockIdx].Lock()
if valueWasJustCached[lockIdx] { if valueWasJustCached[lockIdx].CompareAndSwap(true, false) {
// this ensures the cache has had time to commit the values // this ensures the cache has had time to commit the values
// so we don't repeat a fetch immediately after the other // so we don't repeat a fetch immediately after the other
valueWasJustCached[lockIdx] = false
time.Sleep(time.Millisecond * 10) time.Sleep(time.Millisecond * 10)
} }
@@ -74,7 +73,7 @@ func fetchGenericSets[V comparable, I TagItemWithValue[V]](
// and finally save this to cache // and finally save this to cache
cache.SetWithTTL(pubkey, v, time.Hour*6) cache.SetWithTTL(pubkey, v, time.Hour*6)
valueWasJustCached[lockIdx] = true valueWasJustCached[lockIdx].Store(true)
return v, true return v, true
} }
@@ -93,7 +92,7 @@ func fetchGenericSets[V comparable, I TagItemWithValue[V]](
// save cache even if we didn't get anything // save cache even if we didn't get anything
cache.SetWithTTL(pubkey, v, time.Hour*6) cache.SetWithTTL(pubkey, v, time.Hour*6)
valueWasJustCached[lockIdx] = true valueWasJustCached[lockIdx].Store(true)
return v, false return v, false
} }

View File

@@ -1,7 +1,9 @@
package sdk package sdk
import ( import (
"math/rand/v2" "math/rand"
"sync"
"sync/atomic"
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore" "fiatjaf.com/nostr/eventstore"
@@ -28,32 +30,47 @@ import (
// default they're set to in-memory stores, but ideally persisteable // default they're set to in-memory stores, but ideally persisteable
// implementations should be given (some alternatives are provided in subpackages). // implementations should be given (some alternatives are provided in subpackages).
type System struct { type System struct {
KVStore kvstore.KVStore KVStore kvstore.KVStore
MetadataCache cache.Cache32[ProfileMetadata] metadataCacheOnce sync.Once
RelayListCache cache.Cache32[GenericList[string, Relay]] MetadataCache cache.Cache32[ProfileMetadata]
FollowListCache cache.Cache32[GenericList[nostr.PubKey, ProfileRef]] relayListCacheOnce sync.Once
MuteListCache cache.Cache32[GenericList[nostr.PubKey, ProfileRef]] RelayListCache cache.Cache32[GenericList[string, Relay]]
BookmarkListCache cache.Cache32[GenericList[string, EventRef]] followListCacheOnce sync.Once
PinListCache cache.Cache32[GenericList[string, EventRef]] FollowListCache cache.Cache32[GenericList[nostr.PubKey, ProfileRef]]
BlockedRelayListCache cache.Cache32[GenericList[string, RelayURL]] muteListCacheOnce sync.Once
SearchRelayListCache cache.Cache32[GenericList[string, RelayURL]] MuteListCache cache.Cache32[GenericList[nostr.PubKey, ProfileRef]]
TopicListCache cache.Cache32[GenericList[string, Topic]] bookmarkListCacheOnce sync.Once
RelaySetsCache cache.Cache32[GenericSets[string, RelayURL]] BookmarkListCache cache.Cache32[GenericList[string, EventRef]]
FollowSetsCache cache.Cache32[GenericSets[nostr.PubKey, ProfileRef]] pinListCacheOnce sync.Once
TopicSetsCache cache.Cache32[GenericSets[string, Topic]] PinListCache cache.Cache32[GenericList[string, EventRef]]
ZapProviderCache cache.Cache32[nostr.PubKey] blockedRelayListCacheOnce sync.Once
MintKeysCache cache.Cache32[map[uint64]*btcec.PublicKey] BlockedRelayListCache cache.Cache32[GenericList[string, RelayURL]]
NutZapInfoCache cache.Cache32[NutZapInfo] searchRelayListCacheOnce sync.Once
Hints hints.HintsDB SearchRelayListCache cache.Cache32[GenericList[string, RelayURL]]
Pool *nostr.Pool topicListCacheOnce sync.Once
RelayListRelays *RelayStream TopicListCache cache.Cache32[GenericList[string, Topic]]
FollowListRelays *RelayStream relaySetsCacheOnce sync.Once
MetadataRelays *RelayStream RelaySetsCache cache.Cache32[GenericSets[string, RelayURL]]
FallbackRelays *RelayStream followSetsCacheOnce sync.Once
JustIDRelays *RelayStream FollowSetsCache cache.Cache32[GenericSets[nostr.PubKey, ProfileRef]]
UserSearchRelays *RelayStream topicSetsCacheOnce sync.Once
NoteSearchRelays *RelayStream TopicSetsCache cache.Cache32[GenericSets[string, Topic]]
Store eventstore.Store zapProviderCacheOnce sync.Once
ZapProviderCache cache.Cache32[nostr.PubKey]
mintKeysCacheOnce sync.Once
MintKeysCache cache.Cache32[map[uint64]*btcec.PublicKey]
nutZapInfoCacheOnce sync.Once
NutZapInfoCache cache.Cache32[NutZapInfo]
Hints hints.HintsDB
Pool *nostr.Pool
RelayListRelays *RelayStream
FollowListRelays *RelayStream
MetadataRelays *RelayStream
FallbackRelays *RelayStream
JustIDRelays *RelayStream
UserSearchRelays *RelayStream
NoteSearchRelays *RelayStream
Store eventstore.Store
Publisher wrappers.StorePublisher Publisher wrappers.StorePublisher
@@ -69,18 +86,20 @@ type SystemModifier func(sys *System)
// It's used to distribute requests across multiple relays. // It's used to distribute requests across multiple relays.
type RelayStream struct { type RelayStream struct {
URLs []string URLs []string
serial int serial atomic.Int32
} }
// NewRelayStream creates a new RelayStream with the provided URLs. // NewRelayStream creates a new RelayStream with the provided URLs.
func NewRelayStream(urls ...string) *RelayStream { func NewRelayStream(urls ...string) *RelayStream {
return &RelayStream{URLs: urls, serial: rand.Int()} rs := &RelayStream{URLs: urls}
rs.serial.Add(rand.Int31n(int32(len(urls))))
return rs
} }
// Next returns the next URL in the rotation. // Next returns the next URL in the rotation.
func (rs *RelayStream) Next() string { func (rs *RelayStream) Next() string {
rs.serial++ v := rs.serial.Add(1)
return rs.URLs[rs.serial%len(rs.URLs)] return rs.URLs[int(v)%len(rs.URLs)]
} }
// NewSystem creates a new System with default configuration, // NewSystem creates a new System with default configuration,
@@ -129,21 +148,31 @@ func NewSystem() *System {
PenaltyBox: true, PenaltyBox: true,
}) })
if sys.MetadataCache == nil { sys.metadataCacheOnce.Do(func() {
sys.MetadataCache = cache_memory.New[ProfileMetadata](8000) if sys.MetadataCache == nil {
} sys.MetadataCache = cache_memory.New[ProfileMetadata](8000)
if sys.RelayListCache == nil { }
sys.RelayListCache = cache_memory.New[GenericList[string, Relay]](8000) })
} sys.relayListCacheOnce.Do(func() {
if sys.ZapProviderCache == nil { if sys.RelayListCache == nil {
sys.ZapProviderCache = cache_memory.New[nostr.PubKey](8000) sys.RelayListCache = cache_memory.New[GenericList[string, Relay]](8000)
} }
if sys.MintKeysCache == nil { })
sys.MintKeysCache = cache_memory.New[map[uint64]*btcec.PublicKey](8000) sys.zapProviderCacheOnce.Do(func() {
} if sys.ZapProviderCache == nil {
if sys.NutZapInfoCache == nil { sys.ZapProviderCache = cache_memory.New[nostr.PubKey](8000)
sys.NutZapInfoCache = cache_memory.New[NutZapInfo](8000) }
} })
sys.mintKeysCacheOnce.Do(func() {
if sys.MintKeysCache == nil {
sys.MintKeysCache = cache_memory.New[map[uint64]*btcec.PublicKey](8000)
}
})
sys.nutZapInfoCacheOnce.Do(func() {
if sys.NutZapInfoCache == nil {
sys.NutZapInfoCache = cache_memory.New[NutZapInfo](8000)
}
})
if sys.Store == nil { if sys.Store == nil {
sys.Store = &nullstore.NullStore{} sys.Store = &nullstore.NullStore{}

View File

@@ -82,3 +82,8 @@ func AppendUnique[I comparable](arr []I, item ...I) []I {
} }
return arr return arr
} }
func IsOlder(previous, next Event) bool {
return previous.CreatedAt < next.CreatedAt ||
(previous.CreatedAt == next.CreatedAt && bytes.Compare(previous.ID[:], next.ID[:]) == 1)
}