Compare commits
10 Commits
10318a3443
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43de47addb | ||
|
|
e17995d427 | ||
|
|
d87066c9b9 | ||
|
|
ca3730e508 | ||
|
|
3ed3592e52 | ||
|
|
0cb0d1ccb0 | ||
|
|
f1fdb0788a | ||
|
|
241959d1e3 | ||
|
|
061cf7f68f | ||
|
|
de4eff64d1 |
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
|
||||
2
relay.go
2
relay.go
@@ -190,9 +190,11 @@ func (r *Relay) handleMessage(message string) {
|
||||
}
|
||||
r.challenge = *env.Challenge
|
||||
if r.authHandler != nil {
|
||||
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
|
||||
|
||||
@@ -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]
|
||||
|
||||
10
sdk/list.go
10
sdk/list.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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] {
|
||||
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] {
|
||||
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
|
||||
|
||||
@@ -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] {
|
||||
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] {
|
||||
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] {
|
||||
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
|
||||
|
||||
@@ -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] {
|
||||
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] {
|
||||
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] {
|
||||
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
|
||||
|
||||
@@ -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] {
|
||||
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] {
|
||||
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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/eventstore"
|
||||
@@ -29,20 +31,35 @@ import (
|
||||
// implementations should be given (some alternatives are provided in subpackages).
|
||||
type System struct {
|
||||
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
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
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{}
|
||||
|
||||
Reference in New Issue
Block a user