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
}
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")
}

View File

@@ -5,7 +5,6 @@ import (
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/eventstore/internal"
)
func (b *BleveBackend) ReplaceEvent(evt nostr.Event) error {
@@ -19,7 +18,7 @@ func (b *BleveBackend) ReplaceEvent(evt nostr.Event) error {
shouldStore := true
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 {
return fmt.Errorf("failed to delete event for replacing: %w", err)
}

View File

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

View File

@@ -1,17 +1,11 @@
package internal
import (
"bytes"
"math"
"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) {
var tagKey string
var tagValues []string

View File

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

View File

@@ -6,7 +6,6 @@ import (
"runtime"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/internal"
"github.com/PowerDNS/lmdb-go/lmdb"
)
@@ -72,7 +71,7 @@ func (il *IndexingLayer) ReplaceEvent(evt nostr.Event) error {
var acquiredFreeRangeFromDelete *position
shouldStore := true
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 {
return fmt.Errorf("failed to delete event %s for replacing: %w", previous.ID, err)
} else if shouldPurge {

View File

@@ -10,7 +10,6 @@ import (
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/eventstore/internal"
)
var _ eventstore.Store = (*SliceStore)(nil)
@@ -134,7 +133,7 @@ func (b *SliceStore) ReplaceEvent(evt nostr.Event) error {
shouldStore := true
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 {
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)
rl.Info.AddSupportedNIP(40)
rl.Info.AddSupportedNIP("40")
}
func (rl *Relay) DisableExpirationManager() {
rl.expirationManager.stop()
rl.expirationManager = nil
idx := slices.Index(rl.Info.SupportedNIPs, 40)
idx := slices.Index(rl.Info.SupportedNIPs, "40")
if idx != -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]

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")
base := rl.Router()

View File

@@ -1,19 +1,11 @@
package khatru
import (
"bytes"
"net"
"net/http"
"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 {
privateCIDRs := []string{
"127.0.0.0/8",

View File

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

View File

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

View File

@@ -9,30 +9,30 @@ import (
func TestAddSupportedNIP(t *testing.T) {
info := RelayInformationDocument{}
info.AddSupportedNIP(12)
info.AddSupportedNIP(12)
info.AddSupportedNIP(13)
info.AddSupportedNIP(1)
info.AddSupportedNIP(12)
info.AddSupportedNIP(44)
info.AddSupportedNIP(2)
info.AddSupportedNIP(13)
info.AddSupportedNIP(2)
info.AddSupportedNIP(13)
info.AddSupportedNIP(0)
info.AddSupportedNIP(17)
info.AddSupportedNIP(19)
info.AddSupportedNIP(1)
info.AddSupportedNIP(18)
info.AddSupportedNIP("12")
info.AddSupportedNIP("12")
info.AddSupportedNIP("13")
info.AddSupportedNIP("1")
info.AddSupportedNIP("12")
info.AddSupportedNIP("44")
info.AddSupportedNIP("2")
info.AddSupportedNIP("13")
info.AddSupportedNIP("2")
info.AddSupportedNIP("13")
info.AddSupportedNIP("0")
info.AddSupportedNIP("17")
info.AddSupportedNIP("19")
info.AddSupportedNIP("1")
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) {
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) {

View File

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

View File

@@ -30,7 +30,9 @@ func Decode(bech32string string) (prefix string, value any, err error) {
if len(data) != 32 {
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":
if len(data) != 32 {
return prefix, nil, fmt.Errorf("npub should be 32 bytes (%d)", len(data))

View File

@@ -1,7 +1,9 @@
package nip46
import (
"errors"
"fmt"
"slices"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip44"
@@ -10,6 +12,9 @@ import (
type Session struct {
PublicKey nostr.PubKey
ConversationKey [32]byte
duplicatesBuf [6]string
serial int
}
type RelayReadWrite struct {
@@ -17,7 +22,9 @@ type RelayReadWrite struct {
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
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)
// 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
}

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ package nip46
import (
"context"
"fmt"
"net/url"
"strconv"
"sync"
"fiatjaf.com/nostr"
@@ -14,8 +16,8 @@ import (
var _ Signer = (*StaticKeySigner)(nil)
type StaticKeySigner struct {
secretKey [32]byte
sessions map[nostr.PubKey]Session
secretKey nostr.SecretKey
sessions map[nostr.PubKey]*Session
sync.Mutex
@@ -28,11 +30,11 @@ type StaticKeySigner struct {
func NewStaticKeySigner(secretKey [32]byte) StaticKeySigner {
return StaticKeySigner{
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()
defer p.Unlock()
@@ -43,21 +45,57 @@ func (p *StaticKeySigner) getOrCreateSession(clientPubkey nostr.PubKey) (Session
ck, err := nip44.GenerateConversationKey(clientPubkey, p.secretKey)
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{
PublicKey: pubkey,
session = &Session{
PublicKey: p.secretKey.Public(),
ConversationKey: ck,
}
// add to pool
p.sessions[pubkey] = session
p.sessions[clientPubkey] = session
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) (
req Request,
resp Response,
@@ -85,6 +123,14 @@ func (p *StaticKeySigner) HandleRequest(_ context.Context, event nostr.Event) (
var resultErr error
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":
if len(req.Params) >= 2 {
secret = req.Params[1]

View File

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

View File

@@ -190,9 +190,11 @@ func (r *Relay) handleMessage(message string) {
}
r.challenge = *env.Challenge
if r.authHandler != nil {
r.Auth(r.Context(), func(ctx context.Context, evt *Event) error {
return r.authHandler(ctx, r, evt)
})
go func() {
r.Auth(r.Context(), func(ctx context.Context, evt *Event) error {
return r.authHandler(ctx, r, evt)
})
}()
}
case *EventEnvelope:
// 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
}
db.Lock()
defer db.Unlock()
relayIndex := slices.Index(db.RelayBySerial, relay)
if relayIndex == -1 {
relayIndex = len(db.RelayBySerial)
db.RelayBySerial = append(db.RelayBySerial, relay)
}
db.Lock()
defer db.Unlock()
// fmt.Println(" ", relay, "index", relayIndex, "--", "adding", hints.HintKey(key).String(), ts)
entries, _ := db.OrderedRelaysByPubKey[pubkey]

View File

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

View File

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

View File

@@ -18,27 +18,33 @@ type ProfileRef struct {
func (f ProfileRef) Value() nostr.PubKey { return f.Pubkey }
func (sys *System) FetchFollowList(ctx context.Context, pubkey nostr.PubKey) GenericList[nostr.PubKey, ProfileRef] {
if sys.FollowListCache == nil {
sys.FollowListCache = cache_memory.New[GenericList[nostr.PubKey, ProfileRef]](1000)
}
sys.followListCacheOnce.Do(func() {
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)
return fl
}
func (sys *System) FetchMuteList(ctx context.Context, pubkey nostr.PubKey) GenericList[nostr.PubKey, ProfileRef] {
if sys.MuteListCache == nil {
sys.MuteListCache = cache_memory.New[GenericList[nostr.PubKey, ProfileRef]](1000)
}
sys.muteListCacheOnce.Do(func() {
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)
return ml
}
func (sys *System) FetchFollowSets(ctx context.Context, pubkey nostr.PubKey) GenericSets[nostr.PubKey, ProfileRef] {
if sys.FollowSetsCache == nil {
sys.FollowSetsCache = cache_memory.New[GenericSets[nostr.PubKey, ProfileRef]](1000)
}
sys.followSetsCacheOnce.Do(func() {
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)
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] {
if sys.BlockedRelayListCache == nil {
sys.BlockedRelayListCache = cache_memory.New[GenericList[string, RelayURL]](1000)
}
sys.blockedRelayListCacheOnce.Do(func() {
if sys.BlockedRelayListCache == nil {
sys.BlockedRelayListCache = cache_memory.New[GenericList[string, RelayURL]](1000)
}
})
ml, _ := fetchGenericList(sys, ctx, pubkey, 10006, kind_10006, parseRelayURL, sys.BlockedRelayListCache)
return ml
}
func (sys *System) FetchSearchRelayList(ctx context.Context, pubkey nostr.PubKey) GenericList[string, RelayURL] {
if sys.SearchRelayListCache == nil {
sys.SearchRelayListCache = cache_memory.New[GenericList[string, RelayURL]](1000)
}
sys.searchRelayListCacheOnce.Do(func() {
if sys.SearchRelayListCache == nil {
sys.SearchRelayListCache = cache_memory.New[GenericList[string, RelayURL]](1000)
}
})
ml, _ := fetchGenericList(sys, ctx, pubkey, 10007, kind_10007, parseRelayURL, sys.SearchRelayListCache)
return ml
}
func (sys *System) FetchRelaySets(ctx context.Context, pubkey nostr.PubKey) GenericSets[string, RelayURL] {
if sys.RelaySetsCache == nil {
sys.RelaySetsCache = cache_memory.New[GenericSets[string, RelayURL]](1000)
}
sys.relaySetsCacheOnce.Do(func() {
if sys.RelaySetsCache == nil {
sys.RelaySetsCache = cache_memory.New[GenericSets[string, RelayURL]](1000)
}
})
ml, _ := fetchGenericSets(sys, ctx, pubkey, 30002, kind_30002, parseRelayURL, sys.RelaySetsCache)
return ml

View File

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

View File

@@ -2,12 +2,13 @@ package sdk
import (
"context"
"sync/atomic"
"time"
"fiatjaf.com/nostr"
)
var outboxShortTermCache = [256]ostcEntry{}
var outboxShortTermCache = [256]atomic.Pointer[ostcEntry]{}
type ostcEntry struct {
pubkey nostr.PubKey
@@ -22,7 +23,9 @@ type ostcEntry struct {
func (sys *System) FetchOutboxRelays(ctx context.Context, pubkey nostr.PubKey, n int) []string {
ostcIndex := pubkey[7]
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
}
@@ -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)
relaysCopy := make([]string, len(relays))
copy(relaysCopy, relays)
outboxShortTermCache[ostcIndex] = ostcEntry{pubkey, relaysCopy, now}
outboxShortTermCache[ostcIndex].Store(&ostcEntry{pubkey, relaysCopy, now})
if len(relays) > n {
relays = relays[0:n]

View File

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

View File

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

View File

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