a big bundle of conversions and other changes.

This commit is contained in:
fiatjaf
2025-04-15 17:13:57 -03:00
parent f493293be2
commit 2b5b646a62
92 changed files with 852 additions and 2136 deletions

165
README.md
View File

@@ -1,164 +1,3 @@
[![Run Tests](https://fiatjaf.com/nostrlib/actions/workflows/test.yml/badge.svg)](https://fiatjaf.com/nostrlib/actions/workflows/test.yml)
[![Go Reference](https://pkg.go.dev/badge/fiatjaf.com/nostrlib.svg)](https://pkg.go.dev/fiatjaf.com/nostrlib)
[![Go Report Card](https://goreportcard.com/badge/fiatjaf.com/nostrlib)](https://goreportcard.com/report/fiatjaf.com/nostrlib)
nostr library
<a href="https://nbd.wtf"><img align="right" height="196" src="https://user-images.githubusercontent.com/1653275/194609043-0add674b-dd40-41ed-986c-ab4a2e053092.png" /></a>
go-nostr
========
A set of useful things for [Nostr](https://github.com/nostr-protocol/nostr)-related software.
```bash
go get fiatjaf.com/nostrlib
```
### Generating a key
``` go
package main
import (
"fmt"
"fiatjaf.com/nostrlib"
"fiatjaf.com/nostrlib/nip19"
)
func main() {
sk := nostr.GeneratePrivateKey()
pk, _ := nostr.GetPublicKey(sk)
nsec, _ := nip19.EncodePrivateKey(sk)
npub, _ := nip19.EncodePublicKey(pk)
fmt.Println("sk:", sk)
fmt.Println("pk:", pk)
fmt.Println(nsec)
fmt.Println(npub)
}
```
### Subscribing to a single relay
``` go
ctx := context.Background()
relay, err := nostr.RelayConnect(ctx, "wss://relay.stoner.com")
if err != nil {
panic(err)
}
npub := "npub1422a7ws4yul24p0pf7cacn7cghqkutdnm35z075vy68ggqpqjcyswn8ekc"
var filters nostr.Filters
if _, v, err := nip19.Decode(npub); err == nil {
pub := v.(string)
filters = []nostr.Filter{{
Kinds: []int{nostr.KindTextNote},
Authors: []string{pub},
Limit: 1,
}}
} else {
panic(err)
}
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
sub, err := relay.Subscribe(ctx, filters)
if err != nil {
panic(err)
}
for ev := range sub.Events {
// handle returned event.
// channel will stay open until the ctx is cancelled (in this case, context timeout)
fmt.Println(ev.ID)
}
```
### Publishing to two relays
``` go
sk := nostr.GeneratePrivateKey()
pub, _ := nostr.GetPublicKey(sk)
ev := nostr.Event{
PubKey: pub,
CreatedAt: nostr.Now(),
Kind: nostr.KindTextNote,
Tags: nil,
Content: "Hello World!",
}
// calling Sign sets the event ID field and the event Sig field
ev.Sign(sk)
// publish the event to two relays
ctx := context.Background()
for _, url := range []string{"wss://relay.stoner.com", "wss://nostr-pub.wellorder.net"} {
relay, err := nostr.RelayConnect(ctx, url)
if err != nil {
fmt.Println(err)
continue
}
if err := relay.Publish(ctx, ev); err != nil {
fmt.Println(err)
continue
}
fmt.Printf("published to %s\n", url)
}
```
### Logging
To get more logs from the interaction with relays printed to STDOUT you can compile or run your program with `-tags debug`.
To remove the info logs completely, replace `nostr.InfoLogger` with something that prints nothing, like
``` go
nostr.InfoLogger = log.New(io.Discard, "", 0)
```
### Example script
```
go run example/example.go
```
### Using [`libsecp256k1`](https://github.com/bitcoin-core/secp256k1)
[`libsecp256k1`](https://github.com/bitcoin-core/secp256k1) is very fast:
```
goos: linux
goarch: amd64
cpu: Intel(R) Core(TM) i5-2400 CPU @ 3.10GHz
BenchmarkWithoutLibsecp256k1/sign-4 2794 434114 ns/op
BenchmarkWithoutLibsecp256k1/check-4 4352 297416 ns/op
BenchmarkWithLibsecp256k1/sign-4 12559 94607 ns/op
BenchmarkWithLibsecp256k1/check-4 13761 84595 ns/op
PASS
```
But to use it you need the host to have it installed as a shared library and CGO to be supported, so we don't compile against it by default.
To use it, use `-tags=libsecp256k1` whenever you're compiling your program that uses this library.
### Test for Wasm
Install [wasmbrowsertest](https://github.com/agnivade/wasmbrowsertest), then run tests:
```sh
GOOS=js GOARCH=wasm go test -short ./...
```
## Warning: risk of goroutine bloat (if used incorrectly)
Remember to cancel subscriptions, either by calling `.Unsub()` on them or ensuring their `context.Context` will be canceled at some point.
If you don't do that they will keep creating a new goroutine for every new event that arrives and if you have stopped listening on the
`sub.Events` channel that will cause chaos and doom in your program.
## Contributing to this repository
Use NIP-34 to send your patches to `naddr1qqyxwmeddehhxarjqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3vamnwvaz7tmjv4kxz7fwwpexjmtpdshxuet5qgsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8grqsqqqaueuwmljc`.
do not use yet

View File

@@ -25,24 +25,24 @@ func (evt Event) String() string {
}
// GetID serializes and returns the event ID as a string.
func (evt *Event) GetID() ID {
func (evt Event) GetID() ID {
return sha256.Sum256(evt.Serialize())
}
// CheckID checks if the implied ID matches the given ID more efficiently.
func (evt *Event) CheckID() bool {
func (evt Event) CheckID() bool {
return evt.GetID() == evt.ID
}
// Serialize outputs a byte array that can be hashed to produce the canonical event "id".
func (evt *Event) Serialize() []byte {
func (evt Event) Serialize() []byte {
// the serialization process is just putting everything into a JSON array
// so the order is kept. See NIP-01
dst := make([]byte, 0, 100+len(evt.Content)+len(evt.Tags)*80)
return serializeEventInto(evt, dst)
}
func serializeEventInto(evt *Event, dst []byte) []byte {
func serializeEventInto(evt Event, dst []byte) []byte {
// the header portion is easy to serialize
// [0,"pubkey",created_at,kind,[
dst = append(dst, `[0,"`...)

View File

@@ -25,7 +25,7 @@ func TestEventParsingAndVerifying(t *testing.T) {
assert.Equal(t, ev.ID, ev.GetID())
ok, _ := ev.CheckSignature()
ok := ev.VerifySignature()
assert.True(t, ok, "signature verification failed when it should have succeeded")
asJSON, err := json.Marshal(ev)

View File

@@ -1,17 +1,16 @@
package badger
import (
"context"
"encoding/binary"
"log"
"github.com/dgraph-io/badger/v4"
bin "fiatjaf.com/nostr/eventstore/internal/binary"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/codec/betterbinary"
"fiatjaf.com/nostr/nip45/hyperloglog"
"github.com/dgraph-io/badger/v4"
)
func (b *BadgerBackend) CountEvents(ctx context.Context, filter nostr.Filter) (int64, error) {
func (b *BadgerBackend) CountEvents(filter nostr.Filter) (int64, error) {
var count int64 = 0
queries, extraFilter, since, err := prepareQueries(filter)
@@ -62,8 +61,8 @@ func (b *BadgerBackend) CountEvents(ctx context.Context, filter nostr.Filter) (i
}
err = item.Value(func(val []byte) error {
evt := &nostr.Event{}
if err := bin.Unmarshal(val, evt); err != nil {
evt := nostr.Event{}
if err := betterbinary.Unmarshal(val, &evt); err != nil {
return err
}
@@ -87,7 +86,7 @@ func (b *BadgerBackend) CountEvents(ctx context.Context, filter nostr.Filter) (i
return count, err
}
func (b *BadgerBackend) CountEventsHLL(ctx context.Context, filter nostr.Filter, offset int) (int64, *hyperloglog.HyperLogLog, error) {
func (b *BadgerBackend) CountEventsHLL(filter nostr.Filter, offset int) (int64, *hyperloglog.HyperLogLog, error) {
var count int64 = 0
queries, extraFilter, since, err := prepareQueries(filter)
@@ -138,13 +137,13 @@ func (b *BadgerBackend) CountEventsHLL(ctx context.Context, filter nostr.Filter,
err = item.Value(func(val []byte) error {
if extraFilter == nil {
hll.AddBytes(val[32:64])
hll.AddBytes([32]byte(val[32:64]))
count++
return nil
}
evt := &nostr.Event{}
if err := bin.Unmarshal(val, evt); err != nil {
evt := nostr.Event{}
if err := betterbinary.Unmarshal(val, &evt); err != nil {
return err
}
if extraFilter.Matches(evt) {

View File

@@ -1,22 +1,22 @@
package badger
import (
"context"
"encoding/hex"
"fmt"
"log"
"github.com/dgraph-io/badger/v4"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/codec/betterbinary"
"github.com/dgraph-io/badger/v4"
)
var serialDelete uint32 = 0
func (b *BadgerBackend) DeleteEvent(ctx context.Context, evt *nostr.Event) error {
func (b *BadgerBackend) DeleteEvent(id nostr.ID) error {
deletionHappened := false
err := b.Update(func(txn *badger.Txn) error {
var err error
deletionHappened, err = b.delete(txn, evt)
deletionHappened, err = b.delete(txn, id)
return err
})
if err != nil {
@@ -36,22 +36,30 @@ func (b *BadgerBackend) DeleteEvent(ctx context.Context, evt *nostr.Event) error
return nil
}
func (b *BadgerBackend) delete(txn *badger.Txn, evt *nostr.Event) (bool, error) {
func (b *BadgerBackend) delete(txn *badger.Txn, id nostr.ID) (bool, error) {
idx := make([]byte, 1, 5)
idx[0] = rawEventStorePrefix
// query event by id to get its idx
idPrefix8, _ := hex.DecodeString(evt.ID[0 : 8*2])
prefix := make([]byte, 1+8)
prefix[0] = indexIdPrefix
copy(prefix[1:], idPrefix8)
copy(prefix[1:], id[0:8])
opts := badger.IteratorOptions{
PrefetchValues: false,
}
// also grab the actual event so we can calculate its indexes
var evt nostr.Event
it := txn.NewIterator(opts)
it.Seek(prefix)
if it.ValidForPrefix(prefix) {
idx = append(idx, it.Item().Key()[1+8:]...)
if err := it.Item().Value(func(val []byte) error {
return betterbinary.Unmarshal(val, &evt)
}); err != nil {
return false, fmt.Errorf("failed to unmarshal event %x to delete: %w", id[:], err)
}
}
it.Close()

View File

@@ -40,14 +40,13 @@ func getTagIndexPrefix(tagValue string) ([]byte, int) {
return k, offset
}
func (b *BadgerBackend) getIndexKeysForEvent(evt *nostr.Event, idx []byte) iter.Seq[[]byte] {
func (b *BadgerBackend) getIndexKeysForEvent(evt nostr.Event, idx []byte) iter.Seq[[]byte] {
return func(yield func([]byte) bool) {
{
// ~ by id
idPrefix8, _ := hex.DecodeString(evt.ID[0 : 8*2])
k := make([]byte, 1+8+4)
k[0] = indexIdPrefix
copy(k[1:], idPrefix8)
copy(k[1:], evt.ID[0:8])
copy(k[1+8:], idx)
if !yield(k) {
return
@@ -56,10 +55,9 @@ func (b *BadgerBackend) getIndexKeysForEvent(evt *nostr.Event, idx []byte) iter.
{
// ~ by pubkey+date
pubkeyPrefix8, _ := hex.DecodeString(evt.PubKey[0 : 8*2])
k := make([]byte, 1+8+4+4)
k[0] = indexPubkeyPrefix
copy(k[1:], pubkeyPrefix8)
copy(k[1:], evt.PubKey[0:8])
binary.BigEndian.PutUint32(k[1+8:], uint32(evt.CreatedAt))
copy(k[1+8+4:], idx)
if !yield(k) {
@@ -81,10 +79,9 @@ func (b *BadgerBackend) getIndexKeysForEvent(evt *nostr.Event, idx []byte) iter.
{
// ~ by pubkey+kind+date
pubkeyPrefix8, _ := hex.DecodeString(evt.PubKey[0 : 8*2])
k := make([]byte, 1+8+2+4+4)
k[0] = indexPubkeyKindPrefix
copy(k[1:], pubkeyPrefix8)
copy(k[1:], evt.PubKey[0:8])
binary.BigEndian.PutUint16(k[1+8:], uint16(evt.Kind))
binary.BigEndian.PutUint32(k[1+8+2:], uint32(evt.CreatedAt))
copy(k[1+8+2+4:], idx)
@@ -152,7 +149,7 @@ func getAddrTagElements(tagValue string) (kind uint16, pkb []byte, d string) {
return 0, nil, ""
}
func filterMatchesTags(ef *nostr.Filter, event *nostr.Event) bool {
func filterMatchesTags(ef nostr.Filter, event nostr.Event) bool {
for f, v := range ef.Tags {
if v != nil && !event.Tags.ContainsAny(f, v) {
return false

View File

@@ -5,9 +5,9 @@ import (
"fmt"
"sync/atomic"
"github.com/dgraph-io/badger/v4"
"fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore"
"github.com/dgraph-io/badger/v4"
)
const (
@@ -32,9 +32,9 @@ type BadgerBackend struct {
BadgerOptionsModifier func(badger.Options) badger.Options
// Experimental
SkipIndexingTag func(event *nostr.Event, tagName string, tagValue string) bool
SkipIndexingTag func(event nostr.Event, tagName string, tagValue string) bool
// Experimental
IndexLongerTag func(event *nostr.Event, tagName string, tagValue string) bool
IndexLongerTag func(event nostr.Event, tagName string, tagValue string) bool
*badger.DB

View File

@@ -2,7 +2,6 @@ package badger
import (
"encoding/binary"
"fmt"
"github.com/dgraph-io/badger/v4"
)
@@ -26,35 +25,12 @@ func (b *BadgerBackend) runMigrations() error {
// do the migrations in increasing steps (there is no rollback)
//
// the 3 first migrations go to trash because on version 3 we need to export and import all the data anyway
if version < 3 {
// if there is any data in the relay we will stop and notify the user,
// otherwise we just set version to 3 and proceed
prefix := []byte{indexIdPrefix}
it := txn.NewIterator(badger.IteratorOptions{
PrefetchValues: true,
PrefetchSize: 100,
Prefix: prefix,
})
defer it.Close()
hasAnyEntries := false
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
hasAnyEntries = true
break
}
if hasAnyEntries {
return fmt.Errorf("your database is at version %d, but in order to migrate up to version 3 you must manually export all the events and then import again: run an old version of this software, export the data, then delete the database files, run the new version, import the data back in.", version)
}
b.bumpVersion(txn, 3)
}
if version < 4 {
if version < 1 {
// ...
}
// b.bumpVersion(txn, 1)
return nil
})
}

View File

@@ -1,68 +1,54 @@
package badger
import (
"context"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"iter"
"log"
"github.com/dgraph-io/badger/v4"
"fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/eventstore/internal"
bin "fiatjaf.com/nostr/eventstore/internal/binary"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/codec/betterbinary"
"fiatjaf.com/nostr/eventstore/internal"
"github.com/dgraph-io/badger/v4"
"golang.org/x/exp/slices"
)
var batchFilled = errors.New("batch-filled")
func (b *BadgerBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
ch := make(chan *nostr.Event)
if filter.Search != "" {
close(ch)
return ch, nil
}
// max number of events we'll return
maxLimit := b.MaxLimit
var limit int
if eventstore.IsNegentropySession(ctx) {
maxLimit = b.MaxLimitNegentropy
limit = maxLimit
} else {
limit = maxLimit / 4
}
if filter.Limit > 0 && filter.Limit <= maxLimit {
limit = filter.Limit
}
if tlimit := nostr.GetTheoreticalLimit(filter); tlimit == 0 {
close(ch)
return ch, nil
} else if tlimit > 0 {
limit = tlimit
}
// fmt.Println("limit", limit)
go b.View(func(txn *badger.Txn) error {
defer close(ch)
results, err := b.query(txn, filter, limit)
if err != nil {
return err
func (b *BadgerBackend) QueryEvents(filter nostr.Filter) iter.Seq[nostr.Event] {
return func(yield func(nostr.Event) bool) {
if filter.Search != "" {
return
}
for _, evt := range results {
ch <- evt.Event
// max number of events we'll return
limit := b.MaxLimit / 4
if filter.Limit > 0 && filter.Limit <= b.MaxLimit {
limit = filter.Limit
}
if tlimit := nostr.GetTheoreticalLimit(filter); tlimit == 0 {
return
} else if tlimit > 0 {
limit = tlimit
}
return nil
})
// fmt.Println("limit", limit)
b.View(func(txn *badger.Txn) error {
results, err := b.query(txn, filter, limit)
if err != nil {
return err
}
return ch, nil
for _, evt := range results {
if !yield(evt.Event) {
return nil
}
}
return nil
})
}
}
func (b *BadgerBackend) query(txn *badger.Txn, filter nostr.Filter, limit int) ([]internal.IterEvent, error) {
@@ -81,16 +67,16 @@ func (b *BadgerBackend) query(txn *badger.Txn, filter nostr.Filter, limit int) (
// we will continue to pull from it as soon as some other iterator takes the position
oldest := internal.IterEvent{Q: -1}
secondPhase := false // after we have gathered enough events we will change the way we iterate
sndPhase := false // after we have gathered enough events we will change the way we iterate
secondBatch := make([][]internal.IterEvent, 0, len(queries)+1)
secondPhaseParticipants := make([]int, 0, len(queries)+1)
sndPhaseParticipants := make([]int, 0, len(queries)+1)
// while merging results in the second phase we will alternate between these two lists
// to avoid having to create new lists all the time
var secondPhaseResultsA []internal.IterEvent
var secondPhaseResultsB []internal.IterEvent
var secondPhaseResultsToggle bool // this is just a dummy thing we use to keep track of the alternating
var secondPhaseHasResultsPending bool
var sndPhaseResultsA []internal.IterEvent
var sndPhaseResultsB []internal.IterEvent
var sndPhaseResultsToggle bool // this is just a dummy thing we use to keep track of the alternating
var sndPhaseHasResultsPending bool
remainingUnexhausted := len(queries) // when all queries are exhausted we can finally end this thing
batchSizePerQuery := internal.BatchSizePerNumberOfQueries(limit, remainingUnexhausted)
@@ -180,26 +166,26 @@ func (b *BadgerBackend) query(txn *badger.Txn, filter nostr.Filter, limit int) (
// check it against pubkeys without decoding the entire thing
if extraFilter != nil && extraFilter.Authors != nil &&
!slices.Contains(extraFilter.Authors, hex.EncodeToString(val[32:64])) {
!nostr.ContainsPubKey(extraFilter.Authors, nostr.PubKey(val[32:64])) {
// fmt.Println(" skipped (authors)")
return nil
}
// check it against kinds without decoding the entire thing
if extraFilter != nil && extraFilter.Kinds != nil &&
!slices.Contains(extraFilter.Kinds, int(binary.BigEndian.Uint16(val[132:134]))) {
!slices.Contains(extraFilter.Kinds, binary.BigEndian.Uint16(val[132:134])) {
// fmt.Println(" skipped (kinds)")
return nil
}
event := &nostr.Event{}
if err := bin.Unmarshal(val, event); err != nil {
event := nostr.Event{}
if err := betterbinary.Unmarshal(val, &event); err != nil {
log.Printf("badger: value read error (id %x): %s\n", val[0:32], err)
return err
}
// check if this matches the other filters that were not part of the index
if extraFilter != nil && !filterMatchesTags(extraFilter, event) {
if extraFilter != nil && !filterMatchesTags(*extraFilter, event) {
// fmt.Println(" skipped (filter)", extraFilter, event)
return nil
}
@@ -208,18 +194,18 @@ func (b *BadgerBackend) query(txn *badger.Txn, filter nostr.Filter, limit int) (
evt := internal.IterEvent{Event: event, Q: q}
//
//
if secondPhase {
if sndPhase {
// do the process described below at HIWAWVRTP.
// if we've reached here this means we've already passed the `since` check.
// now we have to eliminate the event currently at the `since` threshold.
nextThreshold := firstPhaseResults[len(firstPhaseResults)-2]
if oldest.Event == nil {
if oldest.Event.ID == nostr.ZeroID {
// fmt.Println(" b1")
// BRANCH WHEN WE DON'T HAVE THE OLDEST EVENT (BWWDHTOE)
// when we don't have the oldest set, we will keep the results
// and not change the cutting point -- it's bad, but hopefully not that bad.
results[q] = append(results[q], evt)
secondPhaseHasResultsPending = true
sndPhaseHasResultsPending = true
} else if nextThreshold.CreatedAt > oldest.CreatedAt {
// fmt.Println(" b2", nextThreshold.CreatedAt, ">", oldest.CreatedAt)
// one of the events we have stored is the actual next threshold
@@ -236,7 +222,7 @@ func (b *BadgerBackend) query(txn *badger.Txn, filter nostr.Filter, limit int) (
// finally
// add this to the results to be merged later
results[q] = append(results[q], evt)
secondPhaseHasResultsPending = true
sndPhaseHasResultsPending = true
} else if nextThreshold.CreatedAt < evt.CreatedAt {
// the next last event in the firstPhaseResults is the next threshold
// fmt.Println(" b3", nextThreshold.CreatedAt, "<", oldest.CreatedAt)
@@ -246,7 +232,7 @@ func (b *BadgerBackend) query(txn *badger.Txn, filter nostr.Filter, limit int) (
// fmt.Println(" new since", since)
// add this to the results to be merged later
results[q] = append(results[q], evt)
secondPhaseHasResultsPending = true
sndPhaseHasResultsPending = true
// update the oldest event
if evt.CreatedAt < oldest.CreatedAt {
oldest = evt
@@ -265,7 +251,7 @@ func (b *BadgerBackend) query(txn *badger.Txn, filter nostr.Filter, limit int) (
firstPhaseTotalPulled++
// update the oldest event
if oldest.Event == nil || evt.CreatedAt < oldest.CreatedAt {
if oldest.Event.ID == nostr.ZeroID || evt.CreatedAt < oldest.CreatedAt {
oldest = evt
}
}
@@ -295,20 +281,20 @@ func (b *BadgerBackend) query(txn *badger.Txn, filter nostr.Filter, limit int) (
// we will do this check if we don't accumulated the requested number of events yet
// fmt.Println("oldest", oldest.Event, "from iter", oldest.Q)
if secondPhase && secondPhaseHasResultsPending && (oldest.Event == nil || remainingUnexhausted == 0) {
if sndPhase && sndPhaseHasResultsPending && (oldest.Event.ID == nostr.ZeroID || remainingUnexhausted == 0) {
// fmt.Println("second phase aggregation!")
// when we are in the second phase we will aggressively aggregate results on every iteration
//
secondBatch = secondBatch[:0]
for s := 0; s < len(secondPhaseParticipants); s++ {
q := secondPhaseParticipants[s]
for s := 0; s < len(sndPhaseParticipants); s++ {
q := sndPhaseParticipants[s]
if len(results[q]) > 0 {
secondBatch = append(secondBatch, results[q])
}
if exhausted[q] {
secondPhaseParticipants = internal.SwapDelete(secondPhaseParticipants, s)
sndPhaseParticipants = internal.SwapDelete(sndPhaseParticipants, s)
s--
}
}
@@ -316,29 +302,29 @@ func (b *BadgerBackend) query(txn *badger.Txn, filter nostr.Filter, limit int) (
// every time we get here we will alternate between these A and B lists
// combining everything we have into a new partial results list.
// after we've done that we can again set the oldest.
// fmt.Println(" xxx", secondPhaseResultsToggle)
if secondPhaseResultsToggle {
secondBatch = append(secondBatch, secondPhaseResultsB)
secondPhaseResultsA = internal.MergeSortMultiple(secondBatch, limit, secondPhaseResultsA)
oldest = secondPhaseResultsA[len(secondPhaseResultsA)-1]
// fmt.Println(" new aggregated a", len(secondPhaseResultsB))
// fmt.Println(" xxx", sndPhaseResultsToggle)
if sndPhaseResultsToggle {
secondBatch = append(secondBatch, sndPhaseResultsB)
sndPhaseResultsA = internal.MergeSortMultiple(secondBatch, limit, sndPhaseResultsA)
oldest = sndPhaseResultsA[len(sndPhaseResultsA)-1]
// fmt.Println(" new aggregated a", len(sndPhaseResultsB))
} else {
secondBatch = append(secondBatch, secondPhaseResultsA)
secondPhaseResultsB = internal.MergeSortMultiple(secondBatch, limit, secondPhaseResultsB)
oldest = secondPhaseResultsB[len(secondPhaseResultsB)-1]
// fmt.Println(" new aggregated b", len(secondPhaseResultsB))
secondBatch = append(secondBatch, sndPhaseResultsA)
sndPhaseResultsB = internal.MergeSortMultiple(secondBatch, limit, sndPhaseResultsB)
oldest = sndPhaseResultsB[len(sndPhaseResultsB)-1]
// fmt.Println(" new aggregated b", len(sndPhaseResultsB))
}
secondPhaseResultsToggle = !secondPhaseResultsToggle
sndPhaseResultsToggle = !sndPhaseResultsToggle
since = uint32(oldest.CreatedAt)
// fmt.Println(" new since", since)
// reset the `results` list so we can keep using it
results = results[:len(queries)]
for _, q := range secondPhaseParticipants {
for _, q := range sndPhaseParticipants {
results[q] = results[q][:0]
}
} else if !secondPhase && firstPhaseTotalPulled >= limit && remainingUnexhausted > 0 {
} else if !sndPhase && firstPhaseTotalPulled >= limit && remainingUnexhausted > 0 {
// fmt.Println("have enough!", firstPhaseTotalPulled, "/", limit, "remaining", remainingUnexhausted)
// we will exclude this oldest number as it is not relevant anymore
@@ -382,16 +368,16 @@ func (b *BadgerBackend) query(txn *badger.Txn, filter nostr.Filter, limit int) (
results[q] = results[q][:0]
// build this index of indexes with everybody who remains
secondPhaseParticipants = append(secondPhaseParticipants, q)
sndPhaseParticipants = append(sndPhaseParticipants, q)
}
// we create these two lists and alternate between them so we don't have to create a
// a new one every time
secondPhaseResultsA = make([]internal.IterEvent, 0, limit*2)
secondPhaseResultsB = make([]internal.IterEvent, 0, limit*2)
sndPhaseResultsA = make([]internal.IterEvent, 0, limit*2)
sndPhaseResultsB = make([]internal.IterEvent, 0, limit*2)
// from now on we won't run this block anymore
secondPhase = true
sndPhase = true
}
// fmt.Println("remaining", remainingUnexhausted)
@@ -400,27 +386,27 @@ func (b *BadgerBackend) query(txn *badger.Txn, filter nostr.Filter, limit int) (
}
}
// fmt.Println("is secondPhase?", secondPhase)
// fmt.Println("is sndPhase?", sndPhase)
var combinedResults []internal.IterEvent
if secondPhase {
if sndPhase {
// fmt.Println("ending second phase")
// when we reach this point either secondPhaseResultsA or secondPhaseResultsB will be full of stuff,
// when we reach this point either sndPhaseResultsA or sndPhaseResultsB will be full of stuff,
// the other will be empty
var secondPhaseResults []internal.IterEvent
// fmt.Println("xxx", secondPhaseResultsToggle, len(secondPhaseResultsA), len(secondPhaseResultsB))
if secondPhaseResultsToggle {
secondPhaseResults = secondPhaseResultsB
combinedResults = secondPhaseResultsA[0:limit] // reuse this
// fmt.Println(" using b", len(secondPhaseResultsA))
var sndPhaseResults []internal.IterEvent
// fmt.Println("xxx", sndPhaseResultsToggle, len(sndPhaseResultsA), len(sndPhaseResultsB))
if sndPhaseResultsToggle {
sndPhaseResults = sndPhaseResultsB
combinedResults = sndPhaseResultsA[0:limit] // reuse this
// fmt.Println(" using b", len(sndPhaseResultsA))
} else {
secondPhaseResults = secondPhaseResultsA
combinedResults = secondPhaseResultsB[0:limit] // reuse this
// fmt.Println(" using a", len(secondPhaseResultsA))
sndPhaseResults = sndPhaseResultsA
combinedResults = sndPhaseResultsB[0:limit] // reuse this
// fmt.Println(" using a", len(sndPhaseResultsA))
}
all := [][]internal.IterEvent{firstPhaseResults, secondPhaseResults}
all := [][]internal.IterEvent{firstPhaseResults, sndPhaseResults}
combinedResults = internal.MergeSortMultiple(all, limit, combinedResults)
// fmt.Println("final combinedResults", len(combinedResults), cap(combinedResults), limit)
} else {

View File

@@ -1,23 +1,22 @@
package badger
import (
"context"
"fmt"
"math"
"github.com/dgraph-io/badger/v4"
"fiatjaf.com/nostr/eventstore/internal"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/internal"
"github.com/dgraph-io/badger/v4"
)
func (b *BadgerBackend) ReplaceEvent(ctx context.Context, evt *nostr.Event) error {
func (b *BadgerBackend) ReplaceEvent(evt nostr.Event) error {
// sanity checking
if evt.CreatedAt > math.MaxUint32 || evt.Kind > math.MaxUint16 {
return fmt.Errorf("event with values out of expected boundaries")
}
return b.Update(func(txn *badger.Txn) error {
filter := nostr.Filter{Limit: 1, Kinds: []int{evt.Kind}, Authors: []string{evt.PubKey}}
filter := nostr.Filter{Limit: 1, Kinds: []uint16{evt.Kind}, Authors: []nostr.PubKey{evt.PubKey}}
if nostr.IsAddressableKind(evt.Kind) {
// when addressable, add the "d" tag to the filter
filter.Tags = nostr.TagMap{"d": []string{evt.Tags.GetD()}}
@@ -32,7 +31,7 @@ func (b *BadgerBackend) ReplaceEvent(ctx context.Context, evt *nostr.Event) erro
shouldStore := true
for _, previous := range results {
if internal.IsOlder(previous.Event, evt) {
if _, err := b.delete(txn, previous.Event); err != nil {
if _, err := b.delete(txn, previous.Event.ID); err != nil {
return fmt.Errorf("failed to delete event %s for replacing: %w", previous.Event.ID, err)
}
} else {

View File

@@ -1,18 +1,16 @@
package badger
import (
"context"
"encoding/hex"
"fmt"
"math"
"github.com/dgraph-io/badger/v4"
"fiatjaf.com/nostr/eventstore"
bin "fiatjaf.com/nostr/eventstore/internal/binary"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/eventstore/codec/betterbinary"
"github.com/dgraph-io/badger/v4"
)
func (b *BadgerBackend) SaveEvent(ctx context.Context, evt *nostr.Event) error {
func (b *BadgerBackend) SaveEvent(evt nostr.Event) error {
// sanity checking
if evt.CreatedAt > math.MaxUint32 || evt.Kind > math.MaxUint16 {
return fmt.Errorf("event with values out of expected boundaries")
@@ -20,10 +18,9 @@ func (b *BadgerBackend) SaveEvent(ctx context.Context, evt *nostr.Event) error {
return b.Update(func(txn *badger.Txn) error {
// query event by id to ensure we don't save duplicates
id, _ := hex.DecodeString(evt.ID)
prefix := make([]byte, 1+8)
prefix[0] = indexIdPrefix
copy(prefix[1:], id)
copy(prefix[1:], evt.ID[0:8])
it := txn.NewIterator(badger.IteratorOptions{})
defer it.Close()
it.Seek(prefix)
@@ -36,16 +33,16 @@ func (b *BadgerBackend) SaveEvent(ctx context.Context, evt *nostr.Event) error {
})
}
func (b *BadgerBackend) save(txn *badger.Txn, evt *nostr.Event) error {
func (b *BadgerBackend) save(txn *badger.Txn, evt nostr.Event) error {
// encode to binary
bin, err := bin.Marshal(evt)
if err != nil {
buf := make([]byte, betterbinary.Measure(evt))
if err := betterbinary.Marshal(evt, buf); err != nil {
return err
}
idx := b.Serial()
// raw event store
if err := txn.Set(idx, bin); err != nil {
if err := txn.Set(idx, buf); err != nil {
return err
}

View File

@@ -1,11 +1,9 @@
package bluge
import (
"context"
"fiatjaf.com/nostr"
)
func (b *BlugeBackend) DeleteEvent(ctx context.Context, evt *nostr.Event) error {
return b.writer.Delete(eventIdentifier(evt.ID))
func (b *BlugeBackend) DeleteEvent(id nostr.ID) error {
return b.writer.Delete(eventIdentifier(id))
}

View File

@@ -1,6 +1,8 @@
package bluge
import "encoding/hex"
import (
"fiatjaf.com/nostr"
)
const (
contentField = "c"
@@ -9,7 +11,7 @@ const (
pubkeyField = "p"
)
type eventIdentifier string
type eventIdentifier nostr.ID
const idField = "i"
@@ -18,6 +20,5 @@ func (id eventIdentifier) Field() string {
}
func (id eventIdentifier) Term() []byte {
v, _ := hex.DecodeString(string(id))
return v
return id[:]
}

View File

@@ -2,108 +2,96 @@ package bluge
import (
"context"
"encoding/hex"
"fmt"
"iter"
"strconv"
"fiatjaf.com/nostr"
"github.com/blugelabs/bluge"
"github.com/blugelabs/bluge/search"
"fiatjaf.com/nostr"
)
func (b *BlugeBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
ch := make(chan *nostr.Event)
if len(filter.Search) < 2 {
close(ch)
return ch, nil
}
reader, err := b.writer.Reader()
if err != nil {
close(ch)
return nil, fmt.Errorf("unable to open reader: %w", err)
}
searchQ := bluge.NewMatchQuery(filter.Search)
searchQ.SetField(contentField)
var q bluge.Query = searchQ
complicatedQuery := bluge.NewBooleanQuery().AddMust(searchQ)
if len(filter.Kinds) > 0 {
eitherKind := bluge.NewBooleanQuery()
eitherKind.SetMinShould(1)
for _, kind := range filter.Kinds {
kindQ := bluge.NewTermQuery(strconv.Itoa(kind))
kindQ.SetField(kindField)
eitherKind.AddShould(kindQ)
func (b *BlugeBackend) QueryEvents(filter nostr.Filter) iter.Seq[nostr.Event] {
return func(yield func(nostr.Event) bool) {
if len(filter.Search) < 2 {
return
}
complicatedQuery.AddMust(eitherKind)
q = complicatedQuery
}
if len(filter.Authors) > 0 {
eitherPubkey := bluge.NewBooleanQuery()
eitherPubkey.SetMinShould(1)
for _, pubkey := range filter.Authors {
if len(pubkey) != 64 {
continue
reader, err := b.writer.Reader()
if err != nil {
return
}
searchQ := bluge.NewMatchQuery(filter.Search)
searchQ.SetField(contentField)
var q bluge.Query = searchQ
complicatedQuery := bluge.NewBooleanQuery().AddMust(searchQ)
if len(filter.Kinds) > 0 {
eitherKind := bluge.NewBooleanQuery()
eitherKind.SetMinShould(1)
for _, kind := range filter.Kinds {
kindQ := bluge.NewTermQuery(strconv.Itoa(int(kind)))
kindQ.SetField(kindField)
eitherKind.AddShould(kindQ)
}
pubkeyQ := bluge.NewTermQuery(pubkey[56:])
pubkeyQ.SetField(pubkeyField)
eitherPubkey.AddShould(pubkeyQ)
complicatedQuery.AddMust(eitherKind)
q = complicatedQuery
}
complicatedQuery.AddMust(eitherPubkey)
q = complicatedQuery
}
if filter.Since != nil || filter.Until != nil {
min := 0.0
if filter.Since != nil {
min = float64(*filter.Since)
if len(filter.Authors) > 0 {
eitherPubkey := bluge.NewBooleanQuery()
eitherPubkey.SetMinShould(1)
for _, pubkey := range filter.Authors {
if len(pubkey) != 64 {
continue
}
pubkeyQ := bluge.NewTermQuery(pubkey.Hex()[56:])
pubkeyQ.SetField(pubkeyField)
eitherPubkey.AddShould(pubkeyQ)
}
complicatedQuery.AddMust(eitherPubkey)
q = complicatedQuery
}
max := float64(nostr.Now())
if filter.Until != nil {
max = float64(*filter.Until)
if filter.Since != nil || filter.Until != nil {
min := 0.0
if filter.Since != nil {
min = float64(*filter.Since)
}
max := float64(nostr.Now())
if filter.Until != nil {
max = float64(*filter.Until)
}
dateRangeQ := bluge.NewNumericRangeInclusiveQuery(min, max, true, true)
dateRangeQ.SetField(createdAtField)
complicatedQuery.AddMust(dateRangeQ)
q = complicatedQuery
}
dateRangeQ := bluge.NewNumericRangeInclusiveQuery(min, max, true, true)
dateRangeQ.SetField(createdAtField)
complicatedQuery.AddMust(dateRangeQ)
q = complicatedQuery
}
limit := 40
if filter.Limit != 0 {
limit = filter.Limit
if filter.Limit > 150 {
limit = 150
limit := 40
if filter.Limit != 0 {
limit = filter.Limit
if filter.Limit > 150 {
limit = 150
}
}
}
req := bluge.NewTopNSearch(limit, q)
req := bluge.NewTopNSearch(limit, q)
dmi, err := reader.Search(context.Background(), req)
if err != nil {
close(ch)
reader.Close()
return ch, fmt.Errorf("error executing search: %w", err)
}
dmi, err := reader.Search(context.Background(), req)
if err != nil {
reader.Close()
return
}
go func() {
defer reader.Close()
defer close(ch)
var next *search.DocumentMatch
for next, err = dmi.Next(); next != nil; next, err = dmi.Next() {
next.VisitStoredFields(func(field string, value []byte) bool {
id := hex.EncodeToString(value)
rawch, err := b.RawEventStore.QueryEvents(ctx, nostr.Filter{IDs: []string{id}})
if err != nil {
return false
}
for evt := range rawch {
ch <- evt
for evt := range b.RawEventStore.QueryEvents(nostr.Filter{IDs: []nostr.ID{nostr.ID(value)}}) {
yield(evt)
}
return false
})
@@ -111,7 +99,5 @@ func (b *BlugeBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (ch
if err != nil {
return
}
}()
return ch, nil
}
}

View File

@@ -4,29 +4,24 @@ import (
"context"
"fmt"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/eventstore/internal"
"fiatjaf.com/nostr"
)
func (b *BlugeBackend) ReplaceEvent(ctx context.Context, evt *nostr.Event) error {
func (b *BlugeBackend) ReplaceEvent(ctx context.Context, evt nostr.Event) error {
b.Lock()
defer b.Unlock()
filter := nostr.Filter{Limit: 1, Kinds: []int{evt.Kind}, Authors: []string{evt.PubKey}}
filter := nostr.Filter{Limit: 1, Kinds: []uint16{evt.Kind}, Authors: []nostr.PubKey{evt.PubKey}}
if nostr.IsAddressableKind(evt.Kind) {
filter.Tags = nostr.TagMap{"d": []string{evt.Tags.GetD()}}
}
ch, err := b.QueryEvents(ctx, filter)
if err != nil {
return fmt.Errorf("failed to query before replacing: %w", err)
}
shouldStore := true
for previous := range ch {
for previous := range b.QueryEvents(filter) {
if internal.IsOlder(previous, evt) {
if err := b.DeleteEvent(ctx, previous); err != nil {
if err := b.DeleteEvent(previous.ID); err != nil {
return fmt.Errorf("failed to delete event for replacing: %w", err)
}
} else {

View File

@@ -1,23 +1,22 @@
package bluge
import (
"context"
"fmt"
"strconv"
"github.com/blugelabs/bluge"
"fiatjaf.com/nostr"
"github.com/blugelabs/bluge"
)
func (b *BlugeBackend) SaveEvent(ctx context.Context, evt *nostr.Event) error {
func (b *BlugeBackend) SaveEvent(evt nostr.Event) error {
id := eventIdentifier(evt.ID)
doc := &bluge.Document{
bluge.NewKeywordFieldBytes(id.Field(), id.Term()).Sortable().StoreValue(),
}
doc.AddField(bluge.NewTextField(contentField, evt.Content))
doc.AddField(bluge.NewTextField(kindField, strconv.Itoa(evt.Kind)))
doc.AddField(bluge.NewTextField(pubkeyField, evt.PubKey[56:]))
doc.AddField(bluge.NewTextField(kindField, strconv.Itoa(int(evt.Kind))))
doc.AddField(bluge.NewTextField(pubkeyField, evt.PubKey.Hex()[56:]))
doc.AddField(bluge.NewNumericField(createdAtField, float64(evt.CreatedAt)))
if err := b.writer.Update(doc.ID(), doc); err != nil {

View File

@@ -2,7 +2,6 @@ package betterbinary
import (
"encoding/binary"
"encoding/hex"
"fmt"
"math"
@@ -50,9 +49,9 @@ func Marshal(evt nostr.Event, buf []byte) error {
}
binary.LittleEndian.PutUint32(buf[3:7], uint32(evt.CreatedAt))
hex.Decode(buf[7:39], []byte(evt.ID))
hex.Decode(buf[39:71], []byte(evt.PubKey))
hex.Decode(buf[71:135], []byte(evt.Sig))
copy(buf[7:39], evt.ID[:])
copy(buf[39:71], evt.PubKey[:])
copy(buf[71:135], evt.Sig[:])
tagBase := 135
// buf[135:137] (tagsSectionLength) will be set later when we know the absolute size of the tags section
@@ -108,11 +107,11 @@ func Unmarshal(data []byte, evt *nostr.Event) (err error) {
}
}()
evt.Kind = int(binary.LittleEndian.Uint16(data[1:3]))
evt.Kind = uint16(binary.LittleEndian.Uint16(data[1:3]))
evt.CreatedAt = nostr.Timestamp(binary.LittleEndian.Uint32(data[3:7]))
evt.ID = hex.EncodeToString(data[7:39])
evt.PubKey = hex.EncodeToString(data[39:71])
evt.Sig = hex.EncodeToString(data[71:135])
evt.ID = nostr.ID(data[7:39])
evt.PubKey = nostr.PubKey(data[39:71])
evt.Sig = [64]byte(data[71:135])
const tagbase = 135
tagsSectionLength := binary.LittleEndian.Uint16(data[tagbase:])

View File

@@ -1 +0,0 @@
decode-binary

View File

@@ -1,39 +0,0 @@
package main
import (
"bytes"
"encoding/hex"
"fmt"
"io"
"os"
"fiatjaf.com/nostr/eventstore/internal/binary"
"fiatjaf.com/nostr"
)
func main() {
b, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to read from stdin: %s\n", err)
os.Exit(1)
return
}
b = bytes.TrimSpace(b)
if bytes.HasPrefix(b, []byte("0x")) {
fromHex := make([]byte, (len(b)-2)/2)
_, err := hex.Decode(fromHex, b[2:])
if err == nil {
b = fromHex
}
}
var evt nostr.Event
err = binary.Unmarshal(b, &evt)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to decode: %s\n", err)
os.Exit(1)
return
}
fmt.Println(evt.String())
}

View File

@@ -1,103 +0,0 @@
package binary
import (
"encoding/binary"
"encoding/hex"
"fmt"
"fiatjaf.com/nostr"
)
// Deprecated -- the encoding used here is not very elegant, we'll have a better binary format later.
func Unmarshal(data []byte, evt *nostr.Event) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("failed to decode binary for event %s from %s at %d: %v", evt.ID, evt.PubKey, evt.CreatedAt, r)
}
}()
evt.ID = hex.EncodeToString(data[0:32])
evt.PubKey = hex.EncodeToString(data[32:64])
evt.Sig = hex.EncodeToString(data[64:128])
evt.CreatedAt = nostr.Timestamp(binary.BigEndian.Uint32(data[128:132]))
evt.Kind = int(binary.BigEndian.Uint16(data[132:134]))
contentLength := int(binary.BigEndian.Uint16(data[134:136]))
evt.Content = string(data[136 : 136+contentLength])
curr := 136 + contentLength
nTags := binary.BigEndian.Uint16(data[curr : curr+2])
curr++
evt.Tags = make(nostr.Tags, nTags)
for t := range evt.Tags {
curr++
nItems := int(data[curr])
tag := make(nostr.Tag, nItems)
for i := range tag {
curr = curr + 1
itemSize := int(binary.BigEndian.Uint16(data[curr : curr+2]))
itemStart := curr + 2
item := string(data[itemStart : itemStart+itemSize])
tag[i] = item
curr = itemStart + itemSize
}
evt.Tags[t] = tag
}
return err
}
// Deprecated -- the encoding used here is not very elegant, we'll have a better binary format later.
func Marshal(evt *nostr.Event) ([]byte, error) {
content := []byte(evt.Content)
buf := make([]byte, 32+32+64+4+2+2+len(content)+65536+len(evt.Tags)*40 /* blergh */)
hex.Decode(buf[0:32], []byte(evt.ID))
hex.Decode(buf[32:64], []byte(evt.PubKey))
hex.Decode(buf[64:128], []byte(evt.Sig))
if evt.CreatedAt > MaxCreatedAt {
return nil, fmt.Errorf("created_at is too big: %d", evt.CreatedAt)
}
binary.BigEndian.PutUint32(buf[128:132], uint32(evt.CreatedAt))
if evt.Kind > MaxKind {
return nil, fmt.Errorf("kind is too big: %d, max is %d", evt.Kind, MaxKind)
}
binary.BigEndian.PutUint16(buf[132:134], uint16(evt.Kind))
if contentLength := len(content); contentLength > MaxContentSize {
return nil, fmt.Errorf("content is too large: %d, max is %d", contentLength, MaxContentSize)
} else {
binary.BigEndian.PutUint16(buf[134:136], uint16(contentLength))
}
copy(buf[136:], content)
if tagCount := len(evt.Tags); tagCount > MaxTagCount {
return nil, fmt.Errorf("can't encode too many tags: %d, max is %d", tagCount, MaxTagCount)
} else {
binary.BigEndian.PutUint16(buf[136+len(content):136+len(content)+2], uint16(tagCount))
}
buf = buf[0 : 136+len(content)+2]
for _, tag := range evt.Tags {
if itemCount := len(tag); itemCount > MaxTagItemCount {
return nil, fmt.Errorf("can't encode a tag with so many items: %d, max is %d", itemCount, MaxTagItemCount)
} else {
buf = append(buf, uint8(itemCount))
}
for _, item := range tag {
itemb := []byte(item)
itemSize := len(itemb)
if itemSize > MaxTagItemSize {
return nil, fmt.Errorf("tag item is too large: %d, max is %d", itemSize, MaxTagItemSize)
}
buf = binary.BigEndian.AppendUint16(buf, uint16(itemSize))
buf = append(buf, itemb...)
buf = append(buf, 0)
}
}
return buf, nil
}

View File

@@ -1,35 +0,0 @@
package binary
import (
"math"
"fiatjaf.com/nostr"
)
const (
MaxKind = math.MaxUint16
MaxCreatedAt = math.MaxUint32
MaxContentSize = math.MaxUint16
MaxTagCount = math.MaxUint16
MaxTagItemCount = math.MaxUint8
MaxTagItemSize = math.MaxUint16
)
func EventEligibleForBinaryEncoding(event *nostr.Event) bool {
if len(event.Content) > MaxContentSize || event.Kind > MaxKind || event.CreatedAt > MaxCreatedAt || len(event.Tags) > MaxTagCount {
return false
}
for _, tag := range event.Tags {
if len(tag) > MaxTagItemCount {
return false
}
for _, item := range tag {
if len(item) > MaxTagItemSize {
return false
}
}
}
return true
}

View File

@@ -4,12 +4,7 @@ import (
"fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/eventstore/badger"
"fiatjaf.com/nostr/eventstore/bluge"
"fiatjaf.com/nostr/eventstore/edgedb"
"fiatjaf.com/nostr/eventstore/lmdb"
"fiatjaf.com/nostr/eventstore/mongo"
"fiatjaf.com/nostr/eventstore/mysql"
"fiatjaf.com/nostr/eventstore/postgresql"
"fiatjaf.com/nostr/eventstore/sqlite3"
"fiatjaf.com/nostr/eventstore/strfry"
)
@@ -17,11 +12,6 @@ import (
var (
_ eventstore.Store = (*badger.BadgerBackend)(nil)
_ eventstore.Store = (*lmdb.LMDBBackend)(nil)
_ eventstore.Store = (*edgedb.EdgeDBBackend)(nil)
_ eventstore.Store = (*postgresql.PostgresBackend)(nil)
_ eventstore.Store = (*mongo.MongoDBBackend)(nil)
_ eventstore.Store = (*sqlite3.SQLite3Backend)(nil)
_ eventstore.Store = (*strfry.StrfryBackend)(nil)
_ eventstore.Store = (*bluge.BlugeBackend)(nil)
_ eventstore.Store = (*mysql.MySQLBackend)(nil)
)

View File

@@ -1,18 +1,18 @@
package internal
import (
"bytes"
"cmp"
"math"
"slices"
"strings"
mergesortedslices "fiatjaf.com/lib/merge-sorted-slices"
"fiatjaf.com/nostr"
)
func IsOlder(previous, next *nostr.Event) bool {
func IsOlder(previous, next nostr.Event) bool {
return previous.CreatedAt < next.CreatedAt ||
(previous.CreatedAt == next.CreatedAt && previous.ID > next.ID)
(previous.CreatedAt == next.CreatedAt && bytes.Compare(previous.ID[:], next.ID[:]) == 1)
}
func ChooseNarrowestTag(filter nostr.Filter) (key string, values []string, goodness int) {
@@ -80,7 +80,7 @@ func CopyMapWithoutKey[K comparable, V any](originalMap map[K]V, key K) map[K]V
}
type IterEvent struct {
*nostr.Event
nostr.Event
Q int
}
@@ -166,18 +166,18 @@ func SwapDelete[A any](arr []A, i int) []A {
}
func compareIterEvent(a, b IterEvent) int {
if a.Event == nil {
if b.Event == nil {
if a.Event.ID == nostr.ZeroID {
if b.Event.ID == nostr.ZeroID {
return 0
} else {
return -1
}
} else if b.Event == nil {
} else if b.Event.ID == nostr.ZeroID {
return 1
}
if a.CreatedAt == b.CreatedAt {
return strings.Compare(a.ID, b.ID)
return slices.Compare(a.ID[:], b.ID[:])
}
return cmp.Compare(a.CreatedAt, b.CreatedAt)
}

View File

@@ -1,8 +0,0 @@
go test fuzz v1
uint(256)
uint(31)
uint(260)
uint(2)
uint(69)
uint(385)
uint(1)

View File

@@ -1,8 +0,0 @@
go test fuzz v1
uint(267)
uint(50)
uint(355)
uint(2)
uint(69)
uint(213)
uint(1)

View File

@@ -1,8 +0,0 @@
go test fuzz v1
uint(280)
uint(0)
uint(13)
uint(2)
uint(2)
uint(0)
uint(0)

View File

@@ -1,8 +0,0 @@
go test fuzz v1
uint(259)
uint(126)
uint(5)
uint(23)
uint(0)
uint(0)
uint(92)

View File

@@ -1,8 +0,0 @@
go test fuzz v1
uint(201)
uint(50)
uint(13)
uint(97)
uint(0)
uint(0)
uint(77)

View File

@@ -1,8 +0,0 @@
go test fuzz v1
uint(164)
uint(50)
uint(13)
uint(1)
uint(2)
uint(13)
uint(0)

View File

@@ -1,8 +0,0 @@
go test fuzz v1
uint(200)
uint(50)
uint(13)
uint(8)
uint(2)
uint(0)
uint(1)

View File

@@ -1,8 +0,0 @@
go test fuzz v1
uint(200)
uint(117)
uint(13)
uint(2)
uint(2)
uint(0)
uint(1)

View File

@@ -1,8 +0,0 @@
go test fuzz v1
uint(200)
uint(50)
uint(13)
uint(2)
uint(2)
uint(0)
uint(0)

View File

@@ -2,19 +2,18 @@ package lmdb
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"github.com/PowerDNS/lmdb-go/lmdb"
bin "fiatjaf.com/nostr/eventstore/internal/binary"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/codec/betterbinary"
"fiatjaf.com/nostr/nip45"
"fiatjaf.com/nostr/nip45/hyperloglog"
"github.com/PowerDNS/lmdb-go/lmdb"
"golang.org/x/exp/slices"
)
func (b *LMDBBackend) CountEvents(ctx context.Context, filter nostr.Filter) (int64, error) {
func (b *LMDBBackend) CountEvents(filter nostr.Filter) (int64, error) {
var count int64 = 0
queries, extraAuthors, extraKinds, extraTagKey, extraTagValues, since, err := b.prepareQueries(filter)
@@ -72,7 +71,7 @@ func (b *LMDBBackend) CountEvents(ctx context.Context, filter nostr.Filter) (int
}
evt := &nostr.Event{}
if err := bin.Unmarshal(val, evt); err != nil {
if err := betterbinary.Unmarshal(val, evt); err != nil {
it.next()
continue
}
@@ -94,8 +93,9 @@ func (b *LMDBBackend) CountEvents(ctx context.Context, filter nostr.Filter) (int
return count, err
}
// CountEventsHLL is like CountEvents, but it will build a hyperloglog value while iterating through results, following NIP-45
func (b *LMDBBackend) CountEventsHLL(ctx context.Context, filter nostr.Filter, offset int) (int64, *hyperloglog.HyperLogLog, error) {
// CountEventsHLL is like CountEvents, but it will build a hyperloglog value while iterating through results,
// following NIP-45
func (b *LMDBBackend) CountEventsHLL(filter nostr.Filter, offset int) (int64, *hyperloglog.HyperLogLog, error) {
if useCache, _ := b.EnableHLLCacheFor(filter.Kinds[0]); useCache {
return b.countEventsHLLCached(filter)
}
@@ -147,7 +147,7 @@ func (b *LMDBBackend) CountEventsHLL(ctx context.Context, filter nostr.Filter, o
if extraKinds == nil && extraTagValues == nil {
// nothing extra to check
count++
hll.AddBytes(val[32:64])
hll.AddBytes(nostr.PubKey(val[32:64]))
} else {
// check it against kinds without decoding the entire thing
if !slices.Contains(extraKinds, [2]byte(val[132:134])) {
@@ -156,7 +156,7 @@ func (b *LMDBBackend) CountEventsHLL(ctx context.Context, filter nostr.Filter, o
}
evt := &nostr.Event{}
if err := bin.Unmarshal(val, evt); err != nil {
if err := betterbinary.Unmarshal(val, evt); err != nil {
it.next()
continue
}
@@ -211,7 +211,7 @@ func (b *LMDBBackend) countEventsHLLCached(filter nostr.Filter) (int64, *hyperlo
return count, hll, err
}
func (b *LMDBBackend) updateHyperLogLogCachedValues(txn *lmdb.Txn, evt *nostr.Event) error {
func (b *LMDBBackend) updateHyperLogLogCachedValues(txn *lmdb.Txn, evt nostr.Event) error {
cacheKey := make([]byte, 2+8)
binary.BigEndian.PutUint16(cacheKey[0:2], uint16(evt.Kind))

View File

@@ -1,29 +1,39 @@
package lmdb
import (
"context"
"encoding/hex"
"fmt"
"github.com/PowerDNS/lmdb-go/lmdb"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/codec/betterbinary"
"github.com/PowerDNS/lmdb-go/lmdb"
)
func (b *LMDBBackend) DeleteEvent(ctx context.Context, evt *nostr.Event) error {
func (b *LMDBBackend) DeleteEvent(id nostr.ID) error {
return b.lmdbEnv.Update(func(txn *lmdb.Txn) error {
return b.delete(txn, evt)
return b.delete(txn, id)
})
}
func (b *LMDBBackend) delete(txn *lmdb.Txn, evt *nostr.Event) error {
idPrefix8, _ := hex.DecodeString(evt.ID[0 : 8*2])
idx, err := txn.Get(b.indexId, idPrefix8)
func (b *LMDBBackend) delete(txn *lmdb.Txn, id nostr.ID) error {
// check if we have this actually
idx, err := txn.Get(b.indexId, id[0:8])
if lmdb.IsNotFound(err) {
// we already do not have this
return nil
}
if err != nil {
return fmt.Errorf("failed to get current idx for deleting %x: %w", evt.ID[0:8*2], err)
return fmt.Errorf("failed to get current idx for deleting %x: %w", id[0:8], err)
}
// if we do, get it so we can compute the indexes
buf, err := txn.Get(b.rawEventStore, idx)
if err != nil {
return fmt.Errorf("failed to get raw event %x to delete: %w", id, err)
}
var evt nostr.Event
if err := betterbinary.Unmarshal(buf, &evt); err != nil {
return fmt.Errorf("failed to unmarshal raw event %x to delete: %w", id, err)
}
// calculate all index keys we have for this event and delete them

View File

@@ -9,8 +9,8 @@ import (
"strconv"
"strings"
"github.com/PowerDNS/lmdb-go/lmdb"
"fiatjaf.com/nostr"
"github.com/PowerDNS/lmdb-go/lmdb"
"golang.org/x/exp/slices"
)
@@ -52,13 +52,11 @@ func (b *LMDBBackend) keyName(key key) string {
return fmt.Sprintf("<dbi=%s key=%x>", b.dbiName(key.dbi), key.key)
}
func (b *LMDBBackend) getIndexKeysForEvent(evt *nostr.Event) iter.Seq[key] {
func (b *LMDBBackend) getIndexKeysForEvent(evt nostr.Event) iter.Seq[key] {
return func(yield func(key) bool) {
{
// ~ by id
k := make([]byte, 8)
hex.Decode(k[0:8], []byte(evt.ID[0:8*2]))
if !yield(key{dbi: b.indexId, key: k[0:8]}) {
if !yield(key{dbi: b.indexId, key: evt.ID[0:8]}) {
return
}
}

View File

@@ -6,8 +6,8 @@ import (
"os"
"sync/atomic"
"github.com/PowerDNS/lmdb-go/lmdb"
"fiatjaf.com/nostr/eventstore"
"github.com/PowerDNS/lmdb-go/lmdb"
)
var _ eventstore.Store = (*LMDBBackend)(nil)
@@ -34,7 +34,7 @@ type LMDBBackend struct {
indexPTagKind lmdb.DBI
hllCache lmdb.DBI
EnableHLLCacheFor func(kind int) (useCache bool, skipSavingActualEvent bool)
EnableHLLCacheFor func(kind uint16) (useCache bool, skipSavingActualEvent bool)
lastId atomic.Uint32
}

View File

@@ -3,11 +3,8 @@ package lmdb
import (
"encoding/binary"
"fmt"
"log"
"github.com/PowerDNS/lmdb-go/lmdb"
bin "fiatjaf.com/nostr/eventstore/internal/binary"
"fiatjaf.com/nostr"
)
const (
@@ -28,114 +25,18 @@ func (b *LMDBBackend) runMigrations() error {
version = binary.BigEndian.Uint16(v)
}
// all previous migrations are useless because we will just reindex everything
if version == 0 {
// if there is any data in the relay we will just set the version to the max without saying anything
cursor, err := txn.OpenCursor(b.rawEventStore)
if err != nil {
return fmt.Errorf("failed to open cursor in migration: %w", err)
}
defer cursor.Close()
hasAnyEntries := false
_, _, err = cursor.Get(nil, nil, lmdb.First)
for err == nil {
hasAnyEntries = true
break
}
if !hasAnyEntries {
b.setVersion(txn, 8)
version = 8
return nil
}
}
// do the migrations in increasing steps (there is no rollback)
//
// this is when we reindex everything
if version < 8 {
log.Println("[lmdb] migration 8: reindex everything")
if err := txn.Drop(b.indexId, false); err != nil {
return err
}
if err := txn.Drop(b.indexCreatedAt, false); err != nil {
return err
}
if err := txn.Drop(b.indexKind, false); err != nil {
return err
}
if err := txn.Drop(b.indexPTagKind, false); err != nil {
return err
}
if err := txn.Drop(b.indexPubkey, false); err != nil {
return err
}
if err := txn.Drop(b.indexPubkeyKind, false); err != nil {
return err
}
if err := txn.Drop(b.indexTag, false); err != nil {
return err
}
if err := txn.Drop(b.indexTag32, false); err != nil {
return err
}
if err := txn.Drop(b.indexTagAddr, false); err != nil {
return err
}
cursor, err := txn.OpenCursor(b.rawEventStore)
if err != nil {
return fmt.Errorf("failed to open cursor in migration 8: %w", err)
}
defer cursor.Close()
seen := make(map[[32]byte]struct{})
idx, val, err := cursor.Get(nil, nil, lmdb.First)
for err == nil {
idp := *(*[32]byte)(val[0:32])
if _, isDup := seen[idp]; isDup {
// do not index, but delete this entry
if err := txn.Del(b.rawEventStore, idx, nil); err != nil {
return err
}
// next
idx, val, err = cursor.Get(nil, nil, lmdb.Next)
continue
}
seen[idp] = struct{}{}
evt := &nostr.Event{}
if err := bin.Unmarshal(val, evt); err != nil {
return fmt.Errorf("error decoding event %x on migration 5: %w", idx, err)
}
for key := range b.getIndexKeysForEvent(evt) {
if err := txn.Put(key.dbi, key.key, idx, 0); err != nil {
return fmt.Errorf("failed to save index %s for event %s (%v) on migration 8: %w",
b.keyName(key), evt.ID, idx, err)
}
}
// next
idx, val, err = cursor.Get(nil, nil, lmdb.Next)
}
if lmdbErr, ok := err.(*lmdb.OpError); ok && lmdbErr.Errno != lmdb.NotFound {
// exited the loop with an error different from NOTFOUND
return err
}
// bump version
if err := b.setVersion(txn, 8); err != nil {
return err
}
if version < 1 {
}
// bump version
// if err := b.setVersion(txn, 1); err != nil {
// return err
// }
return nil
})
}

View File

@@ -2,59 +2,49 @@ package lmdb
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"iter"
"log"
"slices"
"github.com/PowerDNS/lmdb-go/lmdb"
"fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/eventstore/internal"
bin "fiatjaf.com/nostr/eventstore/internal/binary"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/codec/betterbinary"
"fiatjaf.com/nostr/eventstore/internal"
"github.com/PowerDNS/lmdb-go/lmdb"
)
func (b *LMDBBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
ch := make(chan *nostr.Event)
if filter.Search != "" {
close(ch)
return ch, nil
}
// max number of events we'll return
maxLimit := b.MaxLimit
var limit int
if eventstore.IsNegentropySession(ctx) {
maxLimit = b.MaxLimitNegentropy
limit = maxLimit
} else {
limit = maxLimit / 4
}
if filter.Limit > 0 && filter.Limit <= maxLimit {
limit = filter.Limit
}
if tlimit := nostr.GetTheoreticalLimit(filter); tlimit == 0 {
close(ch)
return ch, nil
} else if tlimit > 0 {
limit = tlimit
}
go b.lmdbEnv.View(func(txn *lmdb.Txn) error {
txn.RawRead = true
defer close(ch)
results, err := b.query(txn, filter, limit)
for _, ie := range results {
ch <- ie.Event
func (b *LMDBBackend) QueryEvents(filter nostr.Filter) iter.Seq[nostr.Event] {
return func(yield func(nostr.Event) bool) {
if filter.Search != "" {
return
}
return err
})
// max number of events we'll return
var limit int
limit = b.MaxLimit / 4
if filter.Limit > 0 && filter.Limit <= b.MaxLimit {
limit = filter.Limit
}
if tlimit := nostr.GetTheoreticalLimit(filter); tlimit == 0 {
return
} else if tlimit > 0 {
limit = tlimit
}
return ch, nil
b.lmdbEnv.View(func(txn *lmdb.Txn) error {
txn.RawRead = true
results, err := b.query(txn, filter, limit)
for _, ie := range results {
if !yield(ie.Event) {
break
}
}
return err
})
}
}
func (b *LMDBBackend) query(txn *lmdb.Txn, filter nostr.Filter, limit int) ([]internal.IterEvent, error) {
@@ -73,16 +63,16 @@ func (b *LMDBBackend) query(txn *lmdb.Txn, filter nostr.Filter, limit int) ([]in
// we will continue to pull from it as soon as some other iterator takes the position
oldest := internal.IterEvent{Q: -1}
secondPhase := false // after we have gathered enough events we will change the way we iterate
sndPhase := false // after we have gathered enough events we will change the way we iterate
secondBatch := make([][]internal.IterEvent, 0, len(queries)+1)
secondPhaseParticipants := make([]int, 0, len(queries)+1)
sndPhaseParticipants := make([]int, 0, len(queries)+1)
// while merging results in the second phase we will alternate between these two lists
// to avoid having to create new lists all the time
var secondPhaseResultsA []internal.IterEvent
var secondPhaseResultsB []internal.IterEvent
var secondPhaseResultsToggle bool // this is just a dummy thing we use to keep track of the alternating
var secondPhaseHasResultsPending bool
var sndPhaseResultsA []internal.IterEvent
var sndPhaseResultsB []internal.IterEvent
var sndPhaseResultsToggle bool // this is just a dummy thing we use to keep track of the alternating
var sndPhaseHasResultsPending bool
remainingUnexhausted := len(queries) // when all queries are exhausted we can finally end this thing
batchSizePerQuery := internal.BatchSizePerNumberOfQueries(limit, remainingUnexhausted)
@@ -171,8 +161,8 @@ func (b *LMDBBackend) query(txn *lmdb.Txn, filter nostr.Filter, limit int) ([]in
}
// decode the entire thing
event := &nostr.Event{}
if err := bin.Unmarshal(val, event); err != nil {
event := nostr.Event{}
if err := betterbinary.Unmarshal(val, &event); err != nil {
log.Printf("lmdb: value read error (id %x) on query prefix %x sp %x dbi %d: %s\n", val[0:32],
query.prefix, query.startingPoint, query.dbi, err)
return nil, fmt.Errorf("event read error: %w", err)
@@ -190,18 +180,18 @@ func (b *LMDBBackend) query(txn *lmdb.Txn, filter nostr.Filter, limit int) ([]in
evt := internal.IterEvent{Event: event, Q: q}
//
//
if secondPhase {
if sndPhase {
// do the process described below at HIWAWVRTP.
// if we've reached here this means we've already passed the `since` check.
// now we have to eliminate the event currently at the `since` threshold.
nextThreshold := firstPhaseResults[len(firstPhaseResults)-2]
if oldest.Event == nil {
if oldest.Event.ID == nostr.ZeroID {
// fmt.Println(" b1", evt.ID[0:8])
// BRANCH WHEN WE DON'T HAVE THE OLDEST EVENT (BWWDHTOE)
// when we don't have the oldest set, we will keep the results
// and not change the cutting point -- it's bad, but hopefully not that bad.
results[q] = append(results[q], evt)
secondPhaseHasResultsPending = true
sndPhaseHasResultsPending = true
} else if nextThreshold.CreatedAt > oldest.CreatedAt {
// fmt.Println(" b2", nextThreshold.CreatedAt, ">", oldest.CreatedAt, evt.ID[0:8])
// one of the events we have stored is the actual next threshold
@@ -218,7 +208,7 @@ func (b *LMDBBackend) query(txn *lmdb.Txn, filter nostr.Filter, limit int) ([]in
// finally
// add this to the results to be merged later
results[q] = append(results[q], evt)
secondPhaseHasResultsPending = true
sndPhaseHasResultsPending = true
} else if nextThreshold.CreatedAt < evt.CreatedAt {
// the next last event in the firstPhaseResults is the next threshold
// fmt.Println(" b3", nextThreshold.CreatedAt, "<", oldest.CreatedAt, evt.ID[0:8])
@@ -228,7 +218,7 @@ func (b *LMDBBackend) query(txn *lmdb.Txn, filter nostr.Filter, limit int) ([]in
// fmt.Println(" new since", since)
// add this to the results to be merged later
results[q] = append(results[q], evt)
secondPhaseHasResultsPending = true
sndPhaseHasResultsPending = true
// update the oldest event
if evt.CreatedAt < oldest.CreatedAt {
oldest = evt
@@ -247,7 +237,7 @@ func (b *LMDBBackend) query(txn *lmdb.Txn, filter nostr.Filter, limit int) ([]in
firstPhaseTotalPulled++
// update the oldest event
if oldest.Event == nil || evt.CreatedAt < oldest.CreatedAt {
if oldest.Event.ID == nostr.ZeroID || evt.CreatedAt < oldest.CreatedAt {
oldest = evt
}
}
@@ -273,20 +263,20 @@ func (b *LMDBBackend) query(txn *lmdb.Txn, filter nostr.Filter, limit int) ([]in
// we will do this check if we don't accumulated the requested number of events yet
// fmt.Println("oldest", oldest.Event, "from iter", oldest.Q)
if secondPhase && secondPhaseHasResultsPending && (oldest.Event == nil || remainingUnexhausted == 0) {
if sndPhase && sndPhaseHasResultsPending && (oldest.Event.ID == nostr.ZeroID || remainingUnexhausted == 0) {
// fmt.Println("second phase aggregation!")
// when we are in the second phase we will aggressively aggregate results on every iteration
//
secondBatch = secondBatch[:0]
for s := 0; s < len(secondPhaseParticipants); s++ {
q := secondPhaseParticipants[s]
for s := 0; s < len(sndPhaseParticipants); s++ {
q := sndPhaseParticipants[s]
if len(results[q]) > 0 {
secondBatch = append(secondBatch, results[q])
}
if exhausted[q] {
secondPhaseParticipants = internal.SwapDelete(secondPhaseParticipants, s)
sndPhaseParticipants = internal.SwapDelete(sndPhaseParticipants, s)
s--
}
}
@@ -294,29 +284,29 @@ func (b *LMDBBackend) query(txn *lmdb.Txn, filter nostr.Filter, limit int) ([]in
// every time we get here we will alternate between these A and B lists
// combining everything we have into a new partial results list.
// after we've done that we can again set the oldest.
// fmt.Println(" xxx", secondPhaseResultsToggle)
if secondPhaseResultsToggle {
secondBatch = append(secondBatch, secondPhaseResultsB)
secondPhaseResultsA = internal.MergeSortMultiple(secondBatch, limit, secondPhaseResultsA)
oldest = secondPhaseResultsA[len(secondPhaseResultsA)-1]
// fmt.Println(" new aggregated a", len(secondPhaseResultsB))
// fmt.Println(" xxx", sndPhaseResultsToggle)
if sndPhaseResultsToggle {
secondBatch = append(secondBatch, sndPhaseResultsB)
sndPhaseResultsA = internal.MergeSortMultiple(secondBatch, limit, sndPhaseResultsA)
oldest = sndPhaseResultsA[len(sndPhaseResultsA)-1]
// fmt.Println(" new aggregated a", len(sndPhaseResultsB))
} else {
secondBatch = append(secondBatch, secondPhaseResultsA)
secondPhaseResultsB = internal.MergeSortMultiple(secondBatch, limit, secondPhaseResultsB)
oldest = secondPhaseResultsB[len(secondPhaseResultsB)-1]
// fmt.Println(" new aggregated b", len(secondPhaseResultsB))
secondBatch = append(secondBatch, sndPhaseResultsA)
sndPhaseResultsB = internal.MergeSortMultiple(secondBatch, limit, sndPhaseResultsB)
oldest = sndPhaseResultsB[len(sndPhaseResultsB)-1]
// fmt.Println(" new aggregated b", len(sndPhaseResultsB))
}
secondPhaseResultsToggle = !secondPhaseResultsToggle
sndPhaseResultsToggle = !sndPhaseResultsToggle
since = uint32(oldest.CreatedAt)
// fmt.Println(" new since", since)
// reset the `results` list so we can keep using it
results = results[:len(queries)]
for _, q := range secondPhaseParticipants {
for _, q := range sndPhaseParticipants {
results[q] = results[q][:0]
}
} else if !secondPhase && firstPhaseTotalPulled >= limit && remainingUnexhausted > 0 {
} else if !sndPhase && firstPhaseTotalPulled >= limit && remainingUnexhausted > 0 {
// fmt.Println("have enough!", firstPhaseTotalPulled, "/", limit, "remaining", remainingUnexhausted)
// we will exclude this oldest number as it is not relevant anymore
@@ -360,16 +350,16 @@ func (b *LMDBBackend) query(txn *lmdb.Txn, filter nostr.Filter, limit int) ([]in
results[q] = results[q][:0]
// build this index of indexes with everybody who remains
secondPhaseParticipants = append(secondPhaseParticipants, q)
sndPhaseParticipants = append(sndPhaseParticipants, q)
}
// we create these two lists and alternate between them so we don't have to create a
// a new one every time
secondPhaseResultsA = make([]internal.IterEvent, 0, limit*2)
secondPhaseResultsB = make([]internal.IterEvent, 0, limit*2)
sndPhaseResultsA = make([]internal.IterEvent, 0, limit*2)
sndPhaseResultsB = make([]internal.IterEvent, 0, limit*2)
// from now on we won't run this block anymore
secondPhase = true
sndPhase = true
}
// fmt.Println("remaining", remainingUnexhausted)
@@ -378,27 +368,27 @@ func (b *LMDBBackend) query(txn *lmdb.Txn, filter nostr.Filter, limit int) ([]in
}
}
// fmt.Println("is secondPhase?", secondPhase)
// fmt.Println("is sndPhase?", sndPhase)
var combinedResults []internal.IterEvent
if secondPhase {
if sndPhase {
// fmt.Println("ending second phase")
// when we reach this point either secondPhaseResultsA or secondPhaseResultsB will be full of stuff,
// when we reach this point either sndPhaseResultsA or sndPhaseResultsB will be full of stuff,
// the other will be empty
var secondPhaseResults []internal.IterEvent
// fmt.Println("xxx", secondPhaseResultsToggle, len(secondPhaseResultsA), len(secondPhaseResultsB))
if secondPhaseResultsToggle {
secondPhaseResults = secondPhaseResultsB
combinedResults = secondPhaseResultsA[0:limit] // reuse this
// fmt.Println(" using b", len(secondPhaseResultsA))
var sndPhaseResults []internal.IterEvent
// fmt.Println("xxx", sndPhaseResultsToggle, len(sndPhaseResultsA), len(sndPhaseResultsB))
if sndPhaseResultsToggle {
sndPhaseResults = sndPhaseResultsB
combinedResults = sndPhaseResultsA[0:limit] // reuse this
// fmt.Println(" using b", len(sndPhaseResultsA))
} else {
secondPhaseResults = secondPhaseResultsA
combinedResults = secondPhaseResultsB[0:limit] // reuse this
// fmt.Println(" using a", len(secondPhaseResultsA))
sndPhaseResults = sndPhaseResultsA
combinedResults = sndPhaseResultsB[0:limit] // reuse this
// fmt.Println(" using a", len(sndPhaseResultsA))
}
all := [][]internal.IterEvent{firstPhaseResults, secondPhaseResults}
all := [][]internal.IterEvent{firstPhaseResults, sndPhaseResults}
combinedResults = internal.MergeSortMultiple(all, limit, combinedResults)
// fmt.Println("final combinedResults", len(combinedResults), cap(combinedResults), limit)
} else {

View File

@@ -1,23 +1,22 @@
package lmdb
import (
"context"
"fmt"
"math"
"github.com/PowerDNS/lmdb-go/lmdb"
"fiatjaf.com/nostr/eventstore/internal"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/internal"
"github.com/PowerDNS/lmdb-go/lmdb"
)
func (b *LMDBBackend) ReplaceEvent(ctx context.Context, evt *nostr.Event) error {
func (b *LMDBBackend) ReplaceEvent(evt nostr.Event) error {
// sanity checking
if evt.CreatedAt > math.MaxUint32 || evt.Kind > math.MaxUint16 {
return fmt.Errorf("event with values out of expected boundaries")
}
return b.lmdbEnv.Update(func(txn *lmdb.Txn) error {
filter := nostr.Filter{Limit: 1, Kinds: []int{evt.Kind}, Authors: []string{evt.PubKey}}
filter := nostr.Filter{Limit: 1, Kinds: []uint16{evt.Kind}, Authors: []nostr.PubKey{evt.PubKey}}
if nostr.IsAddressableKind(evt.Kind) {
// when addressable, add the "d" tag to the filter
filter.Tags = nostr.TagMap{"d": []string{evt.Tags.GetD()}}

View File

@@ -1,18 +1,16 @@
package lmdb
import (
"context"
"encoding/hex"
"fmt"
"math"
"github.com/PowerDNS/lmdb-go/lmdb"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore"
bin "fiatjaf.com/nostr/eventstore/internal/binary"
"fiatjaf.com/nostr"
"github.com/PowerDNS/lmdb-go/lmdb"
)
func (b *LMDBBackend) SaveEvent(ctx context.Context, evt *nostr.Event) error {
func (b *LMDBBackend) SaveEvent(evt nostr.Event) error {
// sanity checking
if evt.CreatedAt > math.MaxUint32 || evt.Kind > math.MaxUint16 {
return fmt.Errorf("event with values out of expected boundaries")
@@ -35,8 +33,7 @@ func (b *LMDBBackend) SaveEvent(ctx context.Context, evt *nostr.Event) error {
}
// check if we already have this id
id, _ := hex.DecodeString(evt.ID)
_, err := txn.Get(b.indexId, id)
_, err := txn.Get(b.indexId, evt.ID[0:8])
if operr, ok := err.(*lmdb.OpError); ok && operr.Errno != lmdb.NotFound {
// we will only proceed if we get a NotFound
return eventstore.ErrDupEvent
@@ -46,7 +43,7 @@ func (b *LMDBBackend) SaveEvent(ctx context.Context, evt *nostr.Event) error {
})
}
func (b *LMDBBackend) save(txn *lmdb.Txn, evt *nostr.Event) error {
func (b *LMDBBackend) save(txn *lmdb.Txn, evt nostr.Event) error {
// encode to binary form so we'll save it
bin, err := bin.Marshal(evt)
if err != nil {

View File

@@ -1,13 +0,0 @@
package eventstore
import "context"
var negentropySessionKey = struct{}{}
func IsNegentropySession(ctx context.Context) bool {
return ctx.Value(negentropySessionKey) != nil
}
func SetNegentropy(ctx context.Context) context.Context {
return context.WithValue(ctx, negentropySessionKey, struct{}{})
}

View File

@@ -1,14 +1,15 @@
package slicestore
import (
"context"
"bytes"
"cmp"
"fmt"
"strings"
"iter"
"sync"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/eventstore/internal"
"fiatjaf.com/nostr"
"golang.org/x/exp/slices"
)
@@ -16,13 +17,13 @@ var _ eventstore.Store = (*SliceStore)(nil)
type SliceStore struct {
sync.Mutex
internal []*nostr.Event
internal []nostr.Event
MaxLimit int
}
func (b *SliceStore) Init() error {
b.internal = make([]*nostr.Event, 0, 5000)
b.internal = make([]nostr.Event, 0, 5000)
if b.MaxLimit == 0 {
b.MaxLimit = 500
}
@@ -31,50 +32,44 @@ func (b *SliceStore) Init() error {
func (b *SliceStore) Close() {}
func (b *SliceStore) QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
ch := make(chan *nostr.Event)
if filter.Limit > b.MaxLimit || (filter.Limit == 0 && !filter.LimitZero) {
filter.Limit = b.MaxLimit
}
func (b *SliceStore) QueryEvents(filter nostr.Filter) iter.Seq[nostr.Event] {
return func(yield func(nostr.Event) bool) {
if filter.Limit > b.MaxLimit || (filter.Limit == 0 && !filter.LimitZero) {
filter.Limit = b.MaxLimit
}
// efficiently determine where to start and end
start := 0
end := len(b.internal)
if filter.Until != nil {
start, _ = slices.BinarySearchFunc(b.internal, *filter.Until, eventTimestampComparator)
}
if filter.Since != nil {
end, _ = slices.BinarySearchFunc(b.internal, *filter.Since, eventTimestampComparator)
}
// efficiently determine where to start and end
start := 0
end := len(b.internal)
if filter.Until != nil {
start, _ = slices.BinarySearchFunc(b.internal, *filter.Until, eventTimestampComparator)
}
if filter.Since != nil {
end, _ = slices.BinarySearchFunc(b.internal, *filter.Since, eventTimestampComparator)
}
// ham
if end < start {
close(ch)
return ch, nil
}
// ham
if end < start {
return
}
count := 0
go func() {
count := 0
for _, event := range b.internal[start:end] {
if count == filter.Limit {
break
}
if filter.Matches(event) {
select {
case ch <- event:
case <-ctx.Done():
if !yield(event) {
return
}
count++
}
}
close(ch)
}()
return ch, nil
}
}
func (b *SliceStore) CountEvents(ctx context.Context, filter nostr.Filter) (int64, error) {
func (b *SliceStore) CountEvents(filter nostr.Filter) (int64, error) {
var val int64
for _, event := range b.internal {
if filter.Matches(event) {
@@ -84,7 +79,7 @@ func (b *SliceStore) CountEvents(ctx context.Context, filter nostr.Filter) (int6
return val, nil
}
func (b *SliceStore) SaveEvent(ctx context.Context, evt *nostr.Event) error {
func (b *SliceStore) SaveEvent(evt nostr.Event) error {
idx, found := slices.BinarySearchFunc(b.internal, evt, eventComparator)
if found {
return eventstore.ErrDupEvent
@@ -97,8 +92,8 @@ func (b *SliceStore) SaveEvent(ctx context.Context, evt *nostr.Event) error {
return nil
}
func (b *SliceStore) DeleteEvent(ctx context.Context, evt *nostr.Event) error {
idx, found := slices.BinarySearchFunc(b.internal, evt, eventComparator)
func (b *SliceStore) DeleteEvent(id nostr.ID) error {
idx, found := slices.BinarySearchFunc(b.internal, id, eventIDComparator)
if !found {
// we don't have this event
return nil
@@ -110,24 +105,19 @@ func (b *SliceStore) DeleteEvent(ctx context.Context, evt *nostr.Event) error {
return nil
}
func (b *SliceStore) ReplaceEvent(ctx context.Context, evt *nostr.Event) error {
func (b *SliceStore) ReplaceEvent(evt nostr.Event) error {
b.Lock()
defer b.Unlock()
filter := nostr.Filter{Limit: 1, Kinds: []int{evt.Kind}, Authors: []string{evt.PubKey}}
filter := nostr.Filter{Limit: 1, Kinds: []uint16{evt.Kind}, Authors: []nostr.PubKey{evt.PubKey}}
if nostr.IsAddressableKind(evt.Kind) {
filter.Tags = nostr.TagMap{"d": []string{evt.Tags.GetD()}}
}
ch, err := b.QueryEvents(ctx, filter)
if err != nil {
return fmt.Errorf("failed to query before replacing: %w", err)
}
shouldStore := true
for previous := range ch {
for previous := range b.QueryEvents(filter) {
if internal.IsOlder(previous, evt) {
if err := b.DeleteEvent(ctx, previous); err != nil {
if err := b.DeleteEvent(previous.ID); err != nil {
return fmt.Errorf("failed to delete event for replacing: %w", err)
}
} else {
@@ -136,7 +126,7 @@ func (b *SliceStore) ReplaceEvent(ctx context.Context, evt *nostr.Event) error {
}
if shouldStore {
if err := b.SaveEvent(ctx, evt); err != nil && err != eventstore.ErrDupEvent {
if err := b.SaveEvent(evt); err != nil && err != eventstore.ErrDupEvent {
return fmt.Errorf("failed to save: %w", err)
}
}
@@ -144,14 +134,18 @@ func (b *SliceStore) ReplaceEvent(ctx context.Context, evt *nostr.Event) error {
return nil
}
func eventTimestampComparator(e *nostr.Event, t nostr.Timestamp) int {
func eventTimestampComparator(e nostr.Event, t nostr.Timestamp) int {
return int(t) - int(e.CreatedAt)
}
func eventComparator(a *nostr.Event, b *nostr.Event) int {
c := int(b.CreatedAt) - int(a.CreatedAt)
func eventIDComparator(e nostr.Event, i nostr.ID) int {
return bytes.Compare(i[:], e.ID[:])
}
func eventComparator(a nostr.Event, b nostr.Event) int {
c := cmp.Compare(b.CreatedAt, a.CreatedAt)
if c != 0 {
return c
}
return strings.Compare(b.ID, a.ID)
return bytes.Compare(b.ID[:], a.ID[:])
}

View File

@@ -1,14 +1,12 @@
package slicestore
import (
"context"
"testing"
"fiatjaf.com/nostr"
)
func TestBasicStuff(t *testing.T) {
ctx := context.Background()
ss := &SliceStore{}
ss.Init()
defer ss.Close()
@@ -22,12 +20,11 @@ func TestBasicStuff(t *testing.T) {
if i%3 == 0 {
kind = 12
}
ss.SaveEvent(ctx, &nostr.Event{CreatedAt: nostr.Timestamp(v), Kind: kind})
ss.SaveEvent(nostr.Event{CreatedAt: nostr.Timestamp(v), Kind: uint16(kind)})
}
ch, _ := ss.QueryEvents(ctx, nostr.Filter{})
list := make([]*nostr.Event, 0, 20)
for event := range ch {
list := make([]nostr.Event, 0, 20)
for event := range ss.QueryEvents(nostr.Filter{}) {
list = append(list, event)
}
@@ -39,9 +36,8 @@ func TestBasicStuff(t *testing.T) {
}
until := nostr.Timestamp(9999)
ch, _ = ss.QueryEvents(ctx, nostr.Filter{Limit: 15, Until: &until, Kinds: []int{11}})
list = make([]*nostr.Event, 0, 7)
for event := range ch {
list = make([]nostr.Event, 0, 7)
for event := range ss.QueryEvents(nostr.Filter{Limit: 15, Until: &until, Kinds: []uint16{11}}) {
list = append(list, event)
}
if len(list) != 7 {
@@ -49,9 +45,8 @@ func TestBasicStuff(t *testing.T) {
}
since := nostr.Timestamp(10009)
ch, _ = ss.QueryEvents(ctx, nostr.Filter{Since: &since})
list = make([]*nostr.Event, 0, 5)
for event := range ch {
list = make([]nostr.Event, 0, 5)
for event := range ss.QueryEvents(nostr.Filter{Since: &since}) {
list = append(list, event)
}
if len(list) != 5 {

View File

@@ -1,7 +1,7 @@
package eventstore
import (
"context"
"iter"
"fiatjaf.com/nostr"
)
@@ -15,18 +15,19 @@ type Store interface {
// Close must be called after you're done using the store, to free up resources and so on.
Close()
// QueryEvents should return a channel with the events as they're recovered from a database.
// the channel should be closed after the events are all delivered.
QueryEvents(context.Context, nostr.Filter) (chan *nostr.Event, error)
// DeleteEvent just deletes an event, no side-effects.
DeleteEvent(context.Context, *nostr.Event) error
// QueryEvents returns events that match the filter
QueryEvents(nostr.Filter) iter.Seq[nostr.Event]
// DeleteEvent deletes an event atomically by ID
DeleteEvent(nostr.ID) error
// SaveEvent just saves an event, no side-effects.
SaveEvent(context.Context, *nostr.Event) error
SaveEvent(nostr.Event) error
// ReplaceEvent atomically replaces a replaceable or addressable event.
// Conceptually it is like a Query->Delete->Save, but streamlined.
ReplaceEvent(context.Context, *nostr.Event) error
}
ReplaceEvent(nostr.Event) error
type Counter interface {
CountEvents(context.Context, nostr.Filter) (int64, error)
// CountEvents counts all events that match a given filter
CountEvents(nostr.Filter) (int64, error)
}

View File

@@ -5,13 +5,10 @@ import (
"os"
"testing"
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
"fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/eventstore/badger"
"fiatjaf.com/nostr/eventstore/lmdb"
"fiatjaf.com/nostr/eventstore/postgresql"
"fiatjaf.com/nostr/eventstore/slicestore"
"fiatjaf.com/nostr/eventstore/sqlite3"
)
const (
@@ -51,27 +48,3 @@ func TestBadger(t *testing.T) {
t.Run(test.name, func(t *testing.T) { test.run(t, &badger.BadgerBackend{Path: dbpath + "badger"}) })
}
}
func TestSQLite(t *testing.T) {
for _, test := range tests {
os.RemoveAll(dbpath + "sqlite")
t.Run(test.name, func(t *testing.T) {
test.run(t, &sqlite3.SQLite3Backend{DatabaseURL: dbpath + "sqlite", QueryLimit: 1000, QueryTagsLimit: 50, QueryAuthorsLimit: 2000})
})
}
}
func TestPostgres(t *testing.T) {
for _, test := range tests {
postgres := embeddedpostgres.NewDatabase()
err := postgres.Start()
if err != nil {
t.Fatalf("failed to start embedded postgres: %s", err)
return
}
t.Run(test.name, func(t *testing.T) {
test.run(t, &postgresql.PostgresBackend{DatabaseURL: "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable", QueryLimit: 1000, QueryTagsLimit: 50, QueryAuthorsLimit: 2000})
})
postgres.Stop()
}
}

View File

@@ -27,7 +27,7 @@ func (ef Filter) String() string {
return string(j)
}
func (ef Filter) Matches(event *Event) bool {
func (ef Filter) Matches(event Event) bool {
if !ef.MatchesIgnoringTimestampConstraints(event) {
return false
}
@@ -43,11 +43,7 @@ func (ef Filter) Matches(event *Event) bool {
return true
}
func (ef Filter) MatchesIgnoringTimestampConstraints(event *Event) bool {
if event == nil {
return false
}
func (ef Filter) MatchesIgnoringTimestampConstraints(event Event) bool {
if ef.IDs != nil && !slices.Contains(ef.IDs, event.ID) {
return false
}

View File

@@ -1,72 +0,0 @@
package nostr
import (
"context"
"errors"
"slices"
)
type RelayStore interface {
Publish(context.Context, Event) error
QueryEvents(context.Context, Filter) (chan *Event, error)
QuerySync(context.Context, Filter) ([]*Event, error)
}
var (
_ RelayStore = (*Relay)(nil)
_ RelayStore = (*MultiStore)(nil)
)
type MultiStore []RelayStore
func (multi MultiStore) Publish(ctx context.Context, event Event) error {
errs := make([]error, len(multi))
for i, s := range multi {
errs[i] = s.Publish(ctx, event)
}
return errors.Join(errs...)
}
func (multi MultiStore) QueryEvents(ctx context.Context, filter Filter) (chan *Event, error) {
multich := make(chan *Event)
errs := make([]error, len(multi))
var good bool
for i, s := range multi {
ch, err := s.QueryEvents(ctx, filter)
errs[i] = err
if err == nil {
good = true
go func(ch chan *Event) {
for evt := range ch {
multich <- evt
}
}(ch)
}
}
if good {
return multich, nil
} else {
return nil, errors.Join(errs...)
}
}
func (multi MultiStore) QuerySync(ctx context.Context, filter Filter) ([]*Event, error) {
errs := make([]error, len(multi))
events := make([]*Event, 0, max(filter.Limit, 250))
for i, s := range multi {
res, err := s.QuerySync(ctx, filter)
errs[i] = err
events = append(events, res...)
}
slices.SortFunc(events, func(a, b *Event) int {
if b.CreatedAt > a.CreatedAt {
return 1
} else if b.CreatedAt < a.CreatedAt {
return -1
}
return 0
})
return events, errors.Join(errs...)
}

View File

@@ -25,12 +25,12 @@ func NewBunkerSignerFromBunkerClient(bc *nip46.BunkerClient) BunkerSigner {
// GetPublicKey retrieves the public key from the remote bunker.
// It uses a timeout to prevent hanging indefinitely.
func (bs BunkerSigner) GetPublicKey(ctx context.Context) (string, error) {
func (bs BunkerSigner) GetPublicKey(ctx context.Context) (nostr.PubKey, error) {
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*30, errors.New("get_public_key took too long"))
defer cancel()
pk, err := bs.bunker.GetPublicKey(ctx)
if err != nil {
return "", err
return nostr.ZeroPK, err
}
return pk, nil
}
@@ -44,11 +44,11 @@ func (bs BunkerSigner) SignEvent(ctx context.Context, evt *nostr.Event) error {
}
// Encrypt encrypts a plaintext message for a recipient using the remote bunker.
func (bs BunkerSigner) Encrypt(ctx context.Context, plaintext string, recipient string) (string, error) {
func (bs BunkerSigner) Encrypt(ctx context.Context, plaintext string, recipient nostr.PubKey) (string, error) {
return bs.bunker.NIP44Encrypt(ctx, recipient, plaintext)
}
// Decrypt decrypts a base64-encoded ciphertext from a sender using the remote bunker.
func (bs BunkerSigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (plaintext string, err error) {
func (bs BunkerSigner) Decrypt(ctx context.Context, base64ciphertext string, sender nostr.PubKey) (plaintext string, err error) {
return bs.bunker.NIP44Encrypt(ctx, sender, base64ciphertext)
}

View File

@@ -16,26 +16,23 @@ var _ nostr.Keyer = (*EncryptedKeySigner)(nil)
// when needed for operations.
type EncryptedKeySigner struct {
ncryptsec string
pk string
pk nostr.PubKey
callback func(context.Context) string
}
// GetPublicKey returns the public key associated with this signer.
// If the public key is not cached, it will decrypt the private key using the password
// callback to derive the public key.
func (es *EncryptedKeySigner) GetPublicKey(ctx context.Context) (string, error) {
if es.pk != "" {
func (es *EncryptedKeySigner) GetPublicKey(ctx context.Context) (nostr.PubKey, error) {
if es.pk != nostr.ZeroPK {
return es.pk, nil
}
password := es.callback(ctx)
key, err := nip49.Decrypt(es.ncryptsec, password)
if err != nil {
return "", err
}
pk, err := nostr.GetPublicKey(key)
if err != nil {
return "", err
return nostr.ZeroPK, err
}
pk := nostr.GetPublicKey(key)
es.pk = pk
return pk, nil
}
@@ -54,7 +51,7 @@ func (es *EncryptedKeySigner) SignEvent(ctx context.Context, evt *nostr.Event) e
// Encrypt encrypts a plaintext message for a recipient using NIP-44.
// It first decrypts the private key using the password callback.
func (es EncryptedKeySigner) Encrypt(ctx context.Context, plaintext string, recipient string) (c64 string, err error) {
func (es EncryptedKeySigner) Encrypt(ctx context.Context, plaintext string, recipient nostr.PubKey) (c64 string, err error) {
password := es.callback(ctx)
sk, err := nip49.Decrypt(es.ncryptsec, password)
if err != nil {
@@ -69,7 +66,7 @@ func (es EncryptedKeySigner) Encrypt(ctx context.Context, plaintext string, reci
// Decrypt decrypts a base64-encoded ciphertext from a sender using NIP-44.
// It first decrypts the private key using the password callback.
func (es EncryptedKeySigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (plaintext string, err error) {
func (es EncryptedKeySigner) Decrypt(ctx context.Context, base64ciphertext string, sender nostr.PubKey) (plaintext string, err error) {
password := es.callback(ctx)
sk, err := nip49.Decrypt(es.ncryptsec, password)
if err != nil {

View File

@@ -53,7 +53,7 @@ type SignerOptions struct {
// The context is used for operations that may require network access.
// The pool is used for relay connections when needed.
// Options are used for additional pieces required for EncryptedKeySigner and BunkerSigner.
func New(ctx context.Context, pool *nostr.SimplePool, input string, opts *SignerOptions) (nostr.Keyer, error) {
func New(ctx context.Context, pool *nostr.Pool, input string, opts *SignerOptions) (nostr.Keyer, error) {
if opts == nil {
opts = &SignerOptions{}
}
@@ -69,7 +69,7 @@ func New(ctx context.Context, pool *nostr.SimplePool, input string, opts *Signer
}
return nil, fmt.Errorf("failed to decrypt with given password: %w", err)
}
pk, _ := nostr.GetPublicKey(sec)
pk := nostr.GetPublicKey(sec)
return KeySigner{sec, pk, xsync.NewMapOf[string, [32]byte]()}, nil
} else if nip46.IsValidBunkerURL(input) || nip05.IsValidIdentifier(input) {
bcsk := nostr.GeneratePrivateKey()

View File

@@ -14,16 +14,16 @@ var _ nostr.Keyer = (*ManualSigner)(nil)
// app wants to implement custom signing logic.
type ManualSigner struct {
// ManualGetPublicKey is called when the public key is needed
ManualGetPublicKey func(context.Context) (string, error)
ManualGetPublicKey func(context.Context) (nostr.PubKey, error)
// ManualSignEvent is called when an event needs to be signed
ManualSignEvent func(context.Context, *nostr.Event) error
// ManualEncrypt is called when a message needs to be encrypted
ManualEncrypt func(ctx context.Context, plaintext string, recipientPublicKey string) (base64ciphertext string, err error)
ManualEncrypt func(ctx context.Context, plaintext string, recipientPublicKey nostr.PubKey) (base64ciphertext string, err error)
// ManualDecrypt is called when a message needs to be decrypted
ManualDecrypt func(ctx context.Context, base64ciphertext string, senderPublicKey string) (plaintext string, err error)
ManualDecrypt func(ctx context.Context, base64ciphertext string, senderPublicKey nostr.PubKey) (plaintext string, err error)
}
// SignEvent delegates event signing to the ManualSignEvent function.
@@ -32,16 +32,16 @@ func (ms ManualSigner) SignEvent(ctx context.Context, evt *nostr.Event) error {
}
// GetPublicKey delegates public key retrieval to the ManualGetPublicKey function.
func (ms ManualSigner) GetPublicKey(ctx context.Context) (string, error) {
func (ms ManualSigner) GetPublicKey(ctx context.Context) (nostr.PubKey, error) {
return ms.ManualGetPublicKey(ctx)
}
// Encrypt delegates encryption to the ManualEncrypt function.
func (ms ManualSigner) Encrypt(ctx context.Context, plaintext string, recipient string) (c64 string, err error) {
func (ms ManualSigner) Encrypt(ctx context.Context, plaintext string, recipient nostr.PubKey) (c64 string, err error) {
return ms.ManualEncrypt(ctx, plaintext, recipient)
}
// Decrypt delegates decryption to the ManualDecrypt function.
func (ms ManualSigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (plaintext string, err error) {
func (ms ManualSigner) Decrypt(ctx context.Context, base64ciphertext string, sender nostr.PubKey) (plaintext string, err error) {
return ms.ManualDecrypt(ctx, base64ciphertext, sender)
}

View File

@@ -12,20 +12,16 @@ var _ nostr.Keyer = (*KeySigner)(nil)
// KeySigner is a signer that holds the private key in memory
type KeySigner struct {
sk string
pk string
sk [32]byte
pk nostr.PubKey
conversationKeys *xsync.MapOf[string, [32]byte]
conversationKeys *xsync.MapOf[nostr.PubKey, [32]byte]
}
// NewPlainKeySigner creates a new KeySigner from a private key.
// Returns an error if the private key is invalid.
func NewPlainKeySigner(sec string) (KeySigner, error) {
pk, err := nostr.GetPublicKey(sec)
if err != nil {
return KeySigner{}, err
}
return KeySigner{sec, pk, xsync.NewMapOf[string, [32]byte]()}, nil
func NewPlainKeySigner(sec [32]byte) (KeySigner, error) {
return KeySigner{sec, nostr.GetPublicKey(sec), xsync.NewMapOf[nostr.PubKey, [32]byte]()}, nil
}
// SignEvent signs the provided event with the signer's private key.
@@ -33,11 +29,11 @@ func NewPlainKeySigner(sec string) (KeySigner, error) {
func (ks KeySigner) SignEvent(ctx context.Context, evt *nostr.Event) error { return evt.Sign(ks.sk) }
// GetPublicKey returns the public key associated with this signer.
func (ks KeySigner) GetPublicKey(ctx context.Context) (string, error) { return ks.pk, nil }
func (ks KeySigner) GetPublicKey(ctx context.Context) (nostr.PubKey, error) { return ks.pk, nil }
// Encrypt encrypts a plaintext message for a recipient using NIP-44.
// It caches conversation keys for efficiency in repeated operations.
func (ks KeySigner) Encrypt(ctx context.Context, plaintext string, recipient string) (string, error) {
func (ks KeySigner) Encrypt(ctx context.Context, plaintext string, recipient nostr.PubKey) (string, error) {
ck, ok := ks.conversationKeys.Load(recipient)
if !ok {
var err error
@@ -52,7 +48,7 @@ func (ks KeySigner) Encrypt(ctx context.Context, plaintext string, recipient str
// Decrypt decrypts a base64-encoded ciphertext from a sender using NIP-44.
// It caches conversation keys for efficiency in repeated operations.
func (ks KeySigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (string, error) {
func (ks KeySigner) Decrypt(ctx context.Context, base64ciphertext string, sender nostr.PubKey) (string, error) {
ck, ok := ks.conversationKeys.Load(sender)
if !ok {
var err error

View File

@@ -11,8 +11,8 @@ import (
"strings"
"time"
"github.com/liamg/magic"
"fiatjaf.com/nostr"
"github.com/liamg/magic"
)
func (bs BlossomServer) handleUploadCheck(w http.ResponseWriter, r *http.Request) {
@@ -40,8 +40,8 @@ func (bs BlossomServer) handleUploadCheck(w http.ResponseWriter, r *http.Request
// get the file size from the incoming header
size, _ := strconv.Atoi(r.Header.Get("X-Content-Length"))
for _, rb := range bs.RejectUpload {
reject, reason, code := rb(r.Context(), auth, size, ext)
if bs.RejectUpload != nil {
reject, reason, code := bs.RejectUpload(r.Context(), auth, size, ext)
if reject {
blossomError(w, reason, code)
return
@@ -336,13 +336,13 @@ func (bs BlossomServer) handleReport(w http.ResponseWriter, r *http.Request) {
return
}
var evt *nostr.Event
if err := json.Unmarshal(body, evt); err != nil {
var evt nostr.Event
if err := json.Unmarshal(body, &evt); err != nil {
blossomError(w, "can't parse event", 400)
return
}
if isValid, _ := evt.CheckSignature(); !isValid {
if !evt.VerifySignature() {
blossomError(w, "invalid report event is provided", 400)
return
}
@@ -352,8 +352,8 @@ func (bs BlossomServer) handleReport(w http.ResponseWriter, r *http.Request) {
return
}
for _, rr := range bs.ReceiveReport {
if err := rr(r.Context(), evt); err != nil {
if bs.ReceiveReport != nil {
if err := bs.ReceiveReport(r.Context(), evt); err != nil {
blossomError(w, "failed to receive report: "+err.Error(), 500)
return
}

View File

@@ -6,23 +6,23 @@ import (
"net/http"
"strings"
"fiatjaf.com/nostr/khatru"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/khatru"
)
type BlossomServer struct {
ServiceURL string
Store BlobIndex
StoreBlob []func(ctx context.Context, sha256 string, body []byte) error
LoadBlob []func(ctx context.Context, sha256 string) (io.ReadSeeker, error)
DeleteBlob []func(ctx context.Context, sha256 string) error
ReceiveReport []func(ctx context.Context, reportEvt *nostr.Event) error
StoreBlob func(ctx context.Context, sha256 string, body []byte) error
LoadBlob func(ctx context.Context, sha256 string) (io.ReadSeeker, error)
DeleteBlob func(ctx context.Context, sha256 string) error
ReceiveReport func(ctx context.Context, reportEvt nostr.Event) error
RejectUpload []func(ctx context.Context, auth *nostr.Event, size int, ext string) (bool, string, int)
RejectGet []func(ctx context.Context, auth *nostr.Event, sha256 string) (bool, string, int)
RejectList []func(ctx context.Context, auth *nostr.Event, pubkey string) (bool, string, int)
RejectDelete []func(ctx context.Context, auth *nostr.Event, sha256 string) (bool, string, int)
RejectUpload func(ctx context.Context, auth *nostr.Event, size int, ext string) (bool, string, int)
RejectGet func(ctx context.Context, auth *nostr.Event, sha256 string) (bool, string, int)
RejectList func(ctx context.Context, auth *nostr.Event, pubkey string) (bool, string, int)
RejectDelete func(ctx context.Context, auth *nostr.Event, sha256 string) (bool, string, int)
}
func New(rl *khatru.Relay, serviceURL string) *BlossomServer {

View File

@@ -1,27 +0,0 @@
package main
import (
"fmt"
"net/http"
"fiatjaf.com/nostr/eventstore/elasticsearch"
"fiatjaf.com/nostr/khatru"
)
func main() {
relay := khatru.NewRelay()
db := elasticsearch.ElasticsearchStorage{URL: ""}
if err := db.Init(); err != nil {
panic(err)
}
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
relay.ReplaceEvent = append(relay.ReplaceEvent, db.ReplaceEvent)
fmt.Println("running on :3334")
http.ListenAndServe(":3334", relay)
}

View File

@@ -1,27 +0,0 @@
package main
import (
"fmt"
"net/http"
"fiatjaf.com/nostr/eventstore/postgresql"
"fiatjaf.com/nostr/khatru"
)
func main() {
relay := khatru.NewRelay()
db := postgresql.PostgresBackend{DatabaseURL: "postgresql://localhost:5432/tmp-khatru-relay"}
if err := db.Init(); err != nil {
panic(err)
}
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
relay.ReplaceEvent = append(relay.ReplaceEvent, db.ReplaceEvent)
fmt.Println("running on :3334")
http.ListenAndServe(":3334", relay)
}

View File

@@ -1,27 +0,0 @@
package main
import (
"fmt"
"net/http"
"fiatjaf.com/nostr/eventstore/sqlite3"
"fiatjaf.com/nostr/khatru"
)
func main() {
relay := khatru.NewRelay()
db := sqlite3.SQLite3Backend{DatabaseURL: "/tmp/khatru-sqlite-tmp"}
if err := db.Init(); err != nil {
panic(err)
}
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
relay.ReplaceEvent = append(relay.ReplaceEvent, db.ReplaceEvent)
fmt.Println("running on :3334")
http.ListenAndServe(":3334", relay)
}

View File

@@ -6,9 +6,9 @@ import (
"log"
"net/http"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/khatru"
"fiatjaf.com/nostr/khatru/policies"
"fiatjaf.com/nostr"
)
func main() {

View File

@@ -1,72 +0,0 @@
module github.com/fiatjaf/khatru
go 1.24.1
require (
github.com/bep/debounce v1.2.1
github.com/fasthttp/websocket v1.5.12
github.com/fiatjaf/eventstore v0.16.2
github.com/liamg/magic v0.0.1
github.com/mailru/easyjson v0.9.0
github.com/nbd-wtf/go-nostr v0.51.8
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/rs/cors v1.11.1
github.com/stretchr/testify v1.10.0
)
require (
fiatjaf.com/lib v0.2.0 // indirect
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
github.com/PowerDNS/lmdb-go v1.9.3 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/aquasecurity/esquery v0.2.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/coder/websocket v1.8.13 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dgraph-io/badger/v4 v4.5.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elastic/elastic-transport-go/v8 v8.6.0 // indirect
github.com/elastic/go-elasticsearch/v7 v7.17.10 // indirect
github.com/elastic/go-elasticsearch/v8 v8.16.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/flatbuffers v24.12.23+incompatible // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.59.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sys v0.31.0 // indirect
google.golang.org/protobuf v1.36.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,254 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
fiatjaf.com/lib v0.2.0 h1:TgIJESbbND6GjOgGHxF5jsO6EMjuAxIzZHPo5DXYexs=
fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
github.com/PowerDNS/lmdb-go v1.9.3 h1:AUMY2pZT8WRpkEv39I9Id3MuoHd+NZbTVpNhruVkPTg=
github.com/PowerDNS/lmdb-go v1.9.3/go.mod h1:TE0l+EZK8Z1B4dx070ZxkWTlp8RG1mjN0/+FkFRQMtU=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/aquasecurity/esquery v0.2.0 h1:9WWXve95TE8hbm3736WB7nS6Owl8UGDeu+0jiyE9ttA=
github.com/aquasecurity/esquery v0.2.0/go.mod h1:VU+CIFR6C+H142HHZf9RUkp4Eedpo9UrEKeCQHWf9ao=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY=
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dgraph-io/badger/v4 v4.5.0 h1:TeJE3I1pIWLBjYhIYCA1+uxrjWEoJXImFBMEBVSm16g=
github.com/dgraph-io/badger/v4 v4.5.0/go.mod h1:ysgYmIeG8dS/E8kwxT7xHyc7MkmwNYLRoYnFbr7387A=
github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I=
github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/elastic/elastic-transport-go/v8 v8.6.0 h1:Y2S/FBjx1LlCv5m6pWAF2kDJAHoSjSRSJCApolgfthA=
github.com/elastic/elastic-transport-go/v8 v8.6.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk=
github.com/elastic/go-elasticsearch/v7 v7.6.0/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
github.com/elastic/go-elasticsearch/v7 v7.17.10 h1:TCQ8i4PmIJuBunvBS6bwT2ybzVFxxUhhltAs3Gyu1yo=
github.com/elastic/go-elasticsearch/v7 v7.17.10/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
github.com/elastic/go-elasticsearch/v8 v8.16.0 h1:f7bR+iBz8GTAVhwyFO3hm4ixsz2eMaEy0QroYnXV3jE=
github.com/elastic/go-elasticsearch/v8 v8.16.0/go.mod h1:lGMlgKIbYoRvay3xWBeKahAiJOgmFDsjZC39nmO3H64=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fiatjaf/eventstore v0.16.2 h1:h4rHwSwPcqAKqWUsAbYWUhDeSgm2Kp+PBkJc3FgBYu4=
github.com/fiatjaf/eventstore v0.16.2/go.mod h1:0gU8fzYO/bG+NQAVlHtJWOlt3JKKFefh5Xjj2d1dLIs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8=
github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jgroeneveld/schema v1.0.0 h1:J0E10CrOkiSEsw6dfb1IfrDJD14pf6QLVJ3tRPl/syI=
github.com/jgroeneveld/schema v1.0.0/go.mod h1:M14lv7sNMtGvo3ops1MwslaSYgDYxrSmbzWIQ0Mr5rs=
github.com/jgroeneveld/trial v2.0.0+incompatible h1:d59ctdgor+VqdZCAiUfVN8K13s0ALDioG5DWwZNtRuQ=
github.com/jgroeneveld/trial v2.0.0+incompatible/go.mod h1:I6INLW96EN8WysNBXUFI3M4RIC8ePg9ntAc/Wy+U/+M=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM=
github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nbd-wtf/go-nostr v0.51.7 h1:dGjtaaFQ1kA3H+vF8wt9a9WYl54K8C0JmVDf4cp+a4A=
github.com/nbd-wtf/go-nostr v0.51.7/go.mod h1:d6+DfvMWYG5pA3dmNMBJd6WCHVDDhkXbHqvfljf0Gzg=
github.com/nbd-wtf/go-nostr v0.51.8 h1:CIoS+YqChcm4e1L1rfMZ3/mIwTz4CwApM2qx7MHNzmE=
github.com/nbd-wtf/go-nostr v0.51.8/go.mod h1:d6+DfvMWYG5pA3dmNMBJd6WCHVDDhkXbHqvfljf0Gzg=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@@ -12,8 +12,6 @@ import (
"time"
"unsafe"
"github.com/bep/debounce"
"github.com/fasthttp/websocket"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip42"
"fiatjaf.com/nostr/nip45"
@@ -21,6 +19,8 @@ import (
"fiatjaf.com/nostr/nip70"
"fiatjaf.com/nostr/nip77"
"fiatjaf.com/nostr/nip77/negentropy"
"github.com/bep/debounce"
"github.com/fasthttp/websocket"
"github.com/puzpuzpuz/xsync/v3"
"github.com/rs/cors"
)
@@ -53,8 +53,8 @@ func (rl *Relay) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
for _, reject := range rl.RejectConnection {
if reject(r) {
if rl.RejectConnection == nil {
if rl.RejectConnection(r) {
w.WriteHeader(429) // Too many requests
return
}
@@ -92,8 +92,8 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
)
kill := func() {
for _, ondisconnect := range rl.OnDisconnect {
ondisconnect(ctx)
if rl.OnDisconnect == nil {
rl.OnDisconnect(ctx)
}
ticker.Stop()
@@ -114,8 +114,8 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
return nil
})
for _, onconnect := range rl.OnConnect {
onconnect(ctx)
if rl.OnConnect == nil {
rl.OnConnect(ctx)
}
smp := nostr.NewMessageParser()
@@ -169,10 +169,7 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
}
// check signature
if ok, err := env.Event.CheckSignature(); err != nil {
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "error: failed to verify signature"})
return
} else if !ok {
if !env.Event.VerifySignature() {
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "invalid: signature is invalid"})
return
}
@@ -228,9 +225,6 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
var reason string
if writeErr == nil {
ok = true
for _, ovw := range srl.OverwriteResponseEvent {
ovw(ctx, &env.Event)
}
if !skipBroadcast {
n := srl.notifyListeners(&env.Event)

View File

@@ -20,13 +20,13 @@ import (
type RelayManagementAPI struct {
RejectAPICall []func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string)
BanPubKey func(ctx context.Context, pubkey string, reason string) error
BanPubKey func(ctx context.Context, pubkey nostr.PubKey, reason string) error
ListBannedPubKeys func(ctx context.Context) ([]nip86.PubKeyReason, error)
AllowPubKey func(ctx context.Context, pubkey string, reason string) error
AllowPubKey func(ctx context.Context, pubkey nostr.PubKey, reason string) error
ListAllowedPubKeys func(ctx context.Context) ([]nip86.PubKeyReason, error)
ListEventsNeedingModeration func(ctx context.Context) ([]nip86.IDReason, error)
AllowEvent func(ctx context.Context, id string, reason string) error
BanEvent func(ctx context.Context, id string, reason string) error
AllowEvent func(ctx context.Context, id nostr.ID, reason string) error
BanEvent func(ctx context.Context, id nostr.ID, reason string) error
ListBannedEvents func(ctx context.Context) ([]nip86.IDReason, error)
ListAllowedEvents func(ctx context.Context) ([]nip86.IDReason, error)
ChangeRelayName func(ctx context.Context, name string) error
@@ -40,8 +40,8 @@ type RelayManagementAPI struct {
UnblockIP func(ctx context.Context, ip net.IP, reason string) error
ListBlockedIPs func(ctx context.Context) ([]nip86.IPReason, error)
Stats func(ctx context.Context) (nip86.Response, error)
GrantAdmin func(ctx context.Context, pubkey string, methods []string) error
RevokeAdmin func(ctx context.Context, pubkey string, methods []string) error
GrantAdmin func(ctx context.Context, pubkey nostr.PubKey, methods []string) error
RevokeAdmin func(ctx context.Context, pubkey nostr.PubKey, methods []string) error
Generic func(ctx context.Context, request nip86.Request) (nip86.Response, error)
}
@@ -81,7 +81,7 @@ func (rl *Relay) HandleNIP86(w http.ResponseWriter, r *http.Request) {
resp.Error = "invalid auth event json"
goto respond
}
if ok, _ := evt.CheckSignature(); !ok {
if !evt.VerifySignature() {
resp.Error = "invalid auth event"
goto respond
}

View File

@@ -10,10 +10,10 @@ import (
"sync"
"time"
"github.com/fasthttp/websocket"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip11"
"fiatjaf.com/nostr/nip45/hyperloglog"
"github.com/fasthttp/websocket"
)
func NewRelay() *Relay {
@@ -56,25 +56,23 @@ type Relay struct {
ServiceURL string
// hooks that will be called at various times
RejectEvent []func(ctx context.Context, event *nostr.Event) (reject bool, msg string)
OverwriteDeletionOutcome []func(ctx context.Context, target *nostr.Event, deletion *nostr.Event) (acceptDeletion bool, msg string)
StoreEvent []func(ctx context.Context, event *nostr.Event) error
ReplaceEvent []func(ctx context.Context, event *nostr.Event) error
DeleteEvent []func(ctx context.Context, event *nostr.Event) error
OnEventSaved []func(ctx context.Context, event *nostr.Event)
OnEphemeralEvent []func(ctx context.Context, event *nostr.Event)
RejectFilter []func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
RejectCountFilter []func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
OverwriteFilter []func(ctx context.Context, filter *nostr.Filter)
QueryEvents []func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)
CountEvents []func(ctx context.Context, filter nostr.Filter) (int64, error)
CountEventsHLL []func(ctx context.Context, filter nostr.Filter, offset int) (int64, *hyperloglog.HyperLogLog, error)
RejectConnection []func(r *http.Request) bool
OnConnect []func(ctx context.Context)
OnDisconnect []func(ctx context.Context)
OverwriteRelayInformation []func(ctx context.Context, r *http.Request, info nip11.RelayInformationDocument) nip11.RelayInformationDocument
OverwriteResponseEvent []func(ctx context.Context, event *nostr.Event)
PreventBroadcast []func(ws *WebSocket, event *nostr.Event) bool
RejectEvent func(ctx context.Context, event *nostr.Event) (reject bool, msg string)
OverwriteDeletionOutcome func(ctx context.Context, target *nostr.Event, deletion *nostr.Event) (acceptDeletion bool, msg string)
StoreEvent func(ctx context.Context, event *nostr.Event) error
ReplaceEvent func(ctx context.Context, event *nostr.Event) error
DeleteEvent func(ctx context.Context, event *nostr.Event) error
OnEventSaved func(ctx context.Context, event *nostr.Event)
OnEphemeralEvent func(ctx context.Context, event *nostr.Event)
RejectFilter func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
RejectCountFilter func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
QueryEvents func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)
CountEvents func(ctx context.Context, filter nostr.Filter) (int64, error)
CountEventsHLL func(ctx context.Context, filter nostr.Filter, offset int) (int64, *hyperloglog.HyperLogLog, error)
RejectConnection func(r *http.Request) bool
OnConnect func(ctx context.Context)
OnDisconnect func(ctx context.Context)
OverwriteRelayInformation func(ctx context.Context, r *http.Request, info nip11.RelayInformationDocument) nip11.RelayInformationDocument
PreventBroadcast func(ws *WebSocket, event *nostr.Event) bool
// these are used when this relays acts as a router
routes []Route

View File

@@ -12,12 +12,6 @@ import (
func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGroup, ws *WebSocket, filter nostr.Filter) error {
defer eose.Done()
// overwrite the filter (for example, to eliminate some kinds or
// that we know we don't support)
for _, ovw := range rl.OverwriteFilter {
ovw(ctx, &filter)
}
if filter.LimitZero {
// don't do any queries, just subscribe to future events
return nil
@@ -27,31 +21,24 @@ func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGr
// because we may, for example, remove some things from the incoming filters
// that we know we don't support, and then if the end result is an empty
// filter we can just reject it)
for _, reject := range rl.RejectFilter {
if reject, msg := reject(ctx, filter); reject {
if rl.RejectFilter != nil {
if reject, msg := rl.RejectFilter(ctx, filter); reject {
return errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
}
}
// run the functions to query events (generally just one,
// but we might be fetching stuff from multiple places)
eose.Add(len(rl.QueryEvents))
for _, query := range rl.QueryEvents {
ch, err := query(ctx, filter)
// run the function to query events
if rl.QueryEvents != nil {
ch, err := rl.QueryEvents(ctx, filter)
if err != nil {
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
eose.Done()
continue
} else if ch == nil {
eose.Done()
continue
}
go func(ch chan *nostr.Event) {
for event := range ch {
for _, ovw := range rl.OverwriteResponseEvent {
ovw(ctx, event)
}
ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &id, Event: *event})
}
eose.Done()
@@ -63,8 +50,8 @@ func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGr
func (rl *Relay) handleCountRequest(ctx context.Context, ws *WebSocket, filter nostr.Filter) int64 {
// check if we'll reject this filter
for _, reject := range rl.RejectCountFilter {
if rejecting, msg := reject(ctx, filter); rejecting {
if rl.RejectCountFilter != nil {
if rejecting, msg := rl.RejectCountFilter(ctx, filter); rejecting {
ws.WriteJSON(nostr.NoticeEnvelope(msg))
return 0
}
@@ -72,8 +59,8 @@ func (rl *Relay) handleCountRequest(ctx context.Context, ws *WebSocket, filter n
// run the functions to count (generally it will be just one)
var subtotal int64 = 0
for _, count := range rl.CountEvents {
res, err := count(ctx, filter)
if rl.CountEvents != nil {
res, err := rl.CountEvents(ctx, filter)
if err != nil {
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
}
@@ -90,8 +77,8 @@ func (rl *Relay) handleCountRequestWithHLL(
offset int,
) (int64, *hyperloglog.HyperLogLog) {
// check if we'll reject this filter
for _, reject := range rl.RejectCountFilter {
if rejecting, msg := reject(ctx, filter); rejecting {
if rl.RejectCountFilter != nil {
if rejecting, msg := rl.RejectCountFilter(ctx, filter); rejecting {
ws.WriteJSON(nostr.NoticeEnvelope(msg))
return 0, nil
}
@@ -100,8 +87,8 @@ func (rl *Relay) handleCountRequestWithHLL(
// run the functions to count (generally it will be just one)
var subtotal int64 = 0
var hll *hyperloglog.HyperLogLog
for _, countHLL := range rl.CountEventsHLL {
res, fhll, err := countHLL(ctx, filter, offset)
if rl.CountEventsHLL != nil {
res, fhll, err := rl.CountEventsHLL(ctx, filter, offset)
if err != nil {
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
}

View File

@@ -31,14 +31,14 @@ func GetConnection(ctx context.Context) *WebSocket {
return nil
}
func GetAuthed(ctx context.Context) string {
func GetAuthed(ctx context.Context) (nostr.PubKey, bool) {
if conn := GetConnection(ctx); conn != nil {
return conn.AuthedPublicKey
return conn.AuthedPublicKey, true
}
if nip86Auth := ctx.Value(nip86HeaderAuthKey); nip86Auth != nil {
return nip86Auth.(string)
return nip86Auth.(nostr.PubKey), true
}
return ""
return nostr.ZeroPK, false
}
// IsInternalCall returns true when a call to QueryEvents, for example, is being made because of a deletion

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"sync"
"fiatjaf.com/nostr"
"github.com/fasthttp/websocket"
"github.com/puzpuzpuz/xsync/v3"
)
@@ -22,7 +23,7 @@ type WebSocket struct {
// nip42
Challenge string
AuthedPublicKey string
AuthedPublicKey nostr.PubKey
Authed chan struct{}
// nip77

4
log.go
View File

@@ -7,8 +7,8 @@ import (
var (
// call SetOutput on InfoLogger to enable info logging
InfoLogger = log.New(os.Stderr, "[go-nostr][info] ", log.LstdFlags)
InfoLogger = log.New(os.Stderr, "[nl][info] ", log.LstdFlags)
// call SetOutput on DebugLogger to enable debug logging
DebugLogger = log.New(os.Stderr, "[go-nostr][debug] ", log.LstdFlags)
DebugLogger = log.New(os.Stderr, "[nl][debug] ", log.LstdFlags)
)

View File

@@ -37,11 +37,11 @@ func CommittedDifficulty(event *nostr.Event) int {
}
// Difficulty counts the number of leading zero bits in an event ID.
func Difficulty(id string) int {
func Difficulty(id nostr.ID) int {
var zeros int
var b [1]byte
for i := 0; i < 64; i += 2 {
if id[i:i+2] == "00" {
for i := 0; i < 32; i += 2 {
if id[i] == 0 {
zeros += 8
continue
}
@@ -70,8 +70,8 @@ func difficultyBytes(id [32]byte) int {
// Check reports whether the event ID demonstrates a sufficient proof of work difficulty.
// Note that Check performs no validation other than counting leading zero bits
// in an event ID. It is up to the callers to verify the event with other methods,
// such as [nostr.Event.CheckSignature].
func Check(id string, minDifficulty int) error {
// such as [nostr.Event.VerifySignature].
func Check(id nostr.ID, minDifficulty int) error {
if Difficulty(id) < minDifficulty {
return ErrDifficultyTooLow
}
@@ -82,7 +82,7 @@ func Check(id string, minDifficulty int) error {
// nonce (as a nostr.Tag) that yields the required work.
// Returns an error if the context expires before that.
func DoWork(ctx context.Context, event nostr.Event, targetDifficulty int) (nostr.Tag, error) {
if event.PubKey == "" {
if event.PubKey == nostr.ZeroPK {
return nil, ErrMissingPubKey
}

View File

@@ -9,7 +9,7 @@ import (
"fiatjaf.com/nostr/nip59"
)
func GetDMRelays(ctx context.Context, pubkey string, pool *nostr.SimplePool, relaysToQuery []string) []string {
func GetDMRelays(ctx context.Context, pubkey string, pool *nostr.Pool, relaysToQuery []string) []string {
ie := pool.QuerySingle(ctx, relaysToQuery, nostr.Filter{
Authors: []string{pubkey},
Kinds: []int{nostr.KindDMRelayList},
@@ -35,7 +35,7 @@ func PublishMessage(
ctx context.Context,
content string,
tags nostr.Tags,
pool *nostr.SimplePool,
pool *nostr.Pool,
ourRelays []string,
theirRelays []string,
kr nostr.Keyer,
@@ -137,7 +137,7 @@ func PrepareMessage(
// ListenForMessages returns a channel with the rumors already decrypted and checked
func ListenForMessages(
ctx context.Context,
pool *nostr.SimplePool,
pool *nostr.Pool,
kr nostr.Keyer,
ourRelays []string,
since nostr.Timestamp,

View File

@@ -10,7 +10,7 @@ import (
// CreateUnsignedAuthEvent creates an event which should be sent via an "AUTH" command.
// If the authentication succeeds, the user will be authenticated as pubkey.
func CreateUnsignedAuthEvent(challenge, pubkey, relayURL string) nostr.Event {
func CreateUnsignedAuthEvent(challenge string, pubkey nostr.PubKey, relayURL string) nostr.Event {
return nostr.Event{
PubKey: pubkey,
CreatedAt: nostr.Now(),
@@ -34,40 +34,40 @@ func parseURL(input string) (*url.URL, error) {
// ValidateAuthEvent checks whether event is a valid NIP-42 event for given challenge and relayURL.
// The result of the validation is encoded in the ok bool.
func ValidateAuthEvent(event *nostr.Event, challenge string, relayURL string) (pubkey string, ok bool) {
func ValidateAuthEvent(event nostr.Event, challenge string, relayURL string) (pubkey nostr.PubKey, ok bool) {
if event.Kind != nostr.KindClientAuthentication {
return "", false
return nostr.ZeroPK, false
}
if event.Tags.FindWithValue("challenge", challenge) == nil {
return "", false
return nostr.ZeroPK, false
}
expected, err := parseURL(relayURL)
if err != nil {
return "", false
return nostr.ZeroPK, false
}
found, err := parseURL(event.Tags.GetFirst([]string{"relay", ""}).Value())
found, err := parseURL(event.Tags.Find("relay")[1])
if err != nil {
return "", false
return nostr.ZeroPK, false
}
if expected.Scheme != found.Scheme ||
expected.Host != found.Host ||
expected.Path != found.Path {
return "", false
return nostr.ZeroPK, false
}
now := time.Now()
if event.CreatedAt.Time().After(now.Add(10*time.Minute)) || event.CreatedAt.Time().Before(now.Add(-10*time.Minute)) {
return "", false
return nostr.ZeroPK, false
}
// save for last, as it is most expensive operation
// no need to check returned error, since ok == true implies err == nil.
if ok, _ := event.CheckSignature(); !ok {
return "", false
if !event.VerifySignature() {
return nostr.ZeroPK, false
}
return event.PubKey, true

View File

@@ -7,7 +7,7 @@ import (
"fiatjaf.com/nostr"
)
func HyperLogLogEventPubkeyOffsetsAndReferencesForEvent(evt *nostr.Event) iter.Seq2[string, int] {
func HyperLogLogEventPubkeyOffsetsAndReferencesForEvent(evt nostr.Event) iter.Seq2[string, int] {
return func(yield func(string, int) bool) {
switch evt.Kind {
case 3:

View File

@@ -2,7 +2,6 @@ package hyperloglog
import (
"encoding/binary"
"encoding/hex"
"fmt"
)
@@ -51,8 +50,8 @@ func (hll *HyperLogLog) Clear() {
}
// Add takes a Nostr event pubkey which will be used as the item "key" (that combined with the offset)
func (hll *HyperLogLog) Add(pubkey string) {
x, _ := hex.DecodeString(pubkey[hll.offset*2 : hll.offset*2+8*2])
func (hll *HyperLogLog) Add(pubkey [32]byte) {
x := pubkey[hll.offset : hll.offset+8]
j := x[0] // register address (first 8 bits, i.e. first byte)
w := binary.BigEndian.Uint64(x) // number that we will use
@@ -64,7 +63,7 @@ func (hll *HyperLogLog) Add(pubkey string) {
}
// AddBytes is like Add, but takes pubkey as bytes instead of as string
func (hll *HyperLogLog) AddBytes(pubkey []byte) {
func (hll *HyperLogLog) AddBytes(pubkey [32]byte) {
x := pubkey[hll.offset : hll.offset+8]
j := x[0] // register address (first 8 bits, i.e. first byte)

View File

@@ -2,14 +2,15 @@ package nip46
import (
"context"
"encoding/hex"
"fmt"
"math/rand"
"net/url"
"strconv"
"sync/atomic"
"unsafe"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip04"
"fiatjaf.com/nostr/nip44"
"github.com/mailru/easyjson"
"github.com/puzpuzpuz/xsync/v3"
@@ -17,9 +18,9 @@ import (
type BunkerClient struct {
serial atomic.Uint64
clientSecretKey string
pool *nostr.SimplePool
target string
clientSecretKey [32]byte
pool *nostr.Pool
target nostr.PubKey
relays []string
conversationKey [32]byte // nip44
listeners *xsync.MapOf[string, chan Response]
@@ -28,7 +29,7 @@ type BunkerClient struct {
onAuth func(string)
// memoized
getPublicKeyResponse string
getPublicKeyResponse nostr.PubKey
// SkipSignatureCheck can be set if you don't want to double-check incoming signatures
SkipSignatureCheck bool
@@ -40,7 +41,7 @@ func ConnectBunker(
ctx context.Context,
clientSecretKey nostr.PubKey,
bunkerURLOrNIP05 string,
pool *nostr.SimplePool,
pool *nostr.Pool,
onAuth func(string),
) (*BunkerClient, error) {
parsed, err := url.Parse(bunkerURLOrNIP05)
@@ -79,7 +80,7 @@ func ConnectBunker(
pool,
onAuth,
)
_, err = bunker.RPC(ctx, "connect", []string{targetPublicKey, secret})
_, err = bunker.RPC(ctx, "connect", []string{hex.EncodeToString(targetPublicKey[:]), secret})
return bunker, err
}
@@ -88,11 +89,11 @@ func NewBunker(
clientSecretKey [32]byte,
targetPublicKey nostr.PubKey,
relays []string,
pool *nostr.SimplePool,
pool *nostr.Pool,
onAuth func(string),
) *BunkerClient {
if pool == nil {
pool = nostr.NewSimplePool(ctx)
pool = nostr.NewPool(nostr.PoolOptions{})
}
clientPublicKey := nostr.GetPublicKey(clientSecretKey)
@@ -113,11 +114,13 @@ func NewBunker(
go func() {
now := nostr.Now()
events := pool.SubscribeMany(ctx, relays, nostr.Filter{
Tags: nostr.TagMap{"p": []string{clientPublicKey}},
Kinds: []int{nostr.KindNostrConnect},
Tags: nostr.TagMap{"p": []string{clientPublicKey.Hex()}},
Kinds: []uint16{nostr.KindNostrConnect},
Since: &now,
LimitZero: true,
}, nostr.WithLabel("bunker46client"))
}, nostr.SubscriptionOptions{
Label: "bunker46client",
})
for ie := range events {
if ie.Kind != nostr.KindNostrConnect {
continue
@@ -126,10 +129,7 @@ func NewBunker(
var resp Response
plain, err := nip44.Decrypt(ie.Content, conversationKey)
if err != nil {
plain, err = nip04.Decrypt(ie.Content, sharedSecret)
if err != nil {
continue
}
continue
}
err = json.Unmarshal([]byte(plain), &resp)
@@ -164,13 +164,22 @@ func (bunker *BunkerClient) Ping(ctx context.Context) error {
return nil
}
func (bunker *BunkerClient) GetPublicKey(ctx context.Context) (string, error) {
if bunker.getPublicKeyResponse != "" {
func (bunker *BunkerClient) GetPublicKey(ctx context.Context) (nostr.PubKey, error) {
if bunker.getPublicKeyResponse != nostr.ZeroPK {
return bunker.getPublicKeyResponse, nil
}
resp, err := bunker.RPC(ctx, "get_public_key", []string{})
bunker.getPublicKeyResponse = resp
return resp, err
if err != nil {
return nostr.ZeroPK, err
}
pk, err := nostr.PubKeyFromHex(resp)
if err != nil {
return nostr.ZeroPK, err
}
bunker.getPublicKeyResponse = pk
return pk, nil
}
func (bunker *BunkerClient) SignEvent(ctx context.Context, evt *nostr.Event) error {
@@ -179,7 +188,7 @@ func (bunker *BunkerClient) SignEvent(ctx context.Context, evt *nostr.Event) err
return err
}
err = easyjson.Unmarshal([]byte(resp), evt)
err = easyjson.Unmarshal(unsafe.Slice(unsafe.StringData(resp), len(resp)), evt)
if err != nil {
return err
}
@@ -188,7 +197,7 @@ func (bunker *BunkerClient) SignEvent(ctx context.Context, evt *nostr.Event) err
if ok := evt.CheckID(); !ok {
return fmt.Errorf("sign_event response from bunker has invalid id")
}
if ok, _ := evt.CheckSignature(); !ok {
if !evt.VerifySignature() {
return fmt.Errorf("sign_event response from bunker has invalid signature")
}
}
@@ -198,34 +207,34 @@ func (bunker *BunkerClient) SignEvent(ctx context.Context, evt *nostr.Event) err
func (bunker *BunkerClient) NIP44Encrypt(
ctx context.Context,
targetPublicKey string,
targetPublicKey nostr.PubKey,
plaintext string,
) (string, error) {
return bunker.RPC(ctx, "nip44_encrypt", []string{targetPublicKey, plaintext})
return bunker.RPC(ctx, "nip44_encrypt", []string{targetPublicKey.Hex(), plaintext})
}
func (bunker *BunkerClient) NIP44Decrypt(
ctx context.Context,
targetPublicKey string,
targetPublicKey nostr.PubKey,
ciphertext string,
) (string, error) {
return bunker.RPC(ctx, "nip44_decrypt", []string{targetPublicKey, ciphertext})
return bunker.RPC(ctx, "nip44_decrypt", []string{targetPublicKey.Hex(), ciphertext})
}
func (bunker *BunkerClient) NIP04Encrypt(
ctx context.Context,
targetPublicKey string,
targetPublicKey nostr.PubKey,
plaintext string,
) (string, error) {
return bunker.RPC(ctx, "nip04_encrypt", []string{targetPublicKey, plaintext})
return bunker.RPC(ctx, "nip04_encrypt", []string{targetPublicKey.Hex(), plaintext})
}
func (bunker *BunkerClient) NIP04Decrypt(
ctx context.Context,
targetPublicKey string,
targetPublicKey nostr.PubKey,
ciphertext string,
) (string, error) {
return bunker.RPC(ctx, "nip04_decrypt", []string{targetPublicKey, ciphertext})
return bunker.RPC(ctx, "nip04_decrypt", []string{targetPublicKey.Hex(), ciphertext})
}
func (bunker *BunkerClient) RPC(ctx context.Context, method string, params []string) (string, error) {
@@ -248,7 +257,7 @@ func (bunker *BunkerClient) RPC(ctx context.Context, method string, params []str
Content: content,
CreatedAt: nostr.Now(),
Kind: nostr.KindNostrConnect,
Tags: nostr.Tags{{"p", bunker.target}},
Tags: nostr.Tags{{"p", bunker.target.Hex()}},
}
if err := evt.Sign(bunker.clientSecretKey); err != nil {
return "", fmt.Errorf("failed to sign request event: %w", err)

View File

@@ -2,7 +2,6 @@ package nip49
import (
"crypto/rand"
"encoding/hex"
"fmt"
"math"
@@ -21,15 +20,7 @@ const (
ClientDoesNotTrackThisData KeySecurityByte = 0x02
)
func Encrypt(secretKey string, password string, logn uint8, ksb KeySecurityByte) (b32code string, err error) {
skb, err := hex.DecodeString(secretKey)
if err != nil || len(skb) != 32 {
return "", fmt.Errorf("invalid secret key")
}
return EncryptBytes(skb, password, logn, ksb)
}
func EncryptBytes(secretKey []byte, password string, logn uint8, ksb KeySecurityByte) (b32code string, err error) {
func Encrypt(secretKey [32]byte, password string, logn uint8, ksb KeySecurityByte) (b32code string, err error) {
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
return "", fmt.Errorf("failed to read salt: %w", err)
@@ -53,7 +44,7 @@ func EncryptBytes(secretKey []byte, password string, logn uint8, ksb KeySecurity
if err != nil {
return "", fmt.Errorf("failed to start xchacha20poly1305: %w", err)
}
ciphertext := c2p1.Seal(nil, concat[2+16:2+16+24], secretKey, ad)
ciphertext := c2p1.Seal(nil, concat[2+16:2+16+24], secretKey[:], ad)
copy(concat[2+16+24+1:], ciphertext)
bits5, err := bech32.ConvertBits(concat, 8, 5, true)
@@ -63,9 +54,9 @@ func EncryptBytes(secretKey []byte, password string, logn uint8, ksb KeySecurity
return bech32.Encode("ncryptsec", bits5)
}
func Decrypt(bech32string string, password string) (secretKey string, err error) {
func Decrypt(bech32string string, password string) (secretKey [32]byte, err error) {
secb, err := DecryptToBytes(bech32string, password)
return hex.EncodeToString(secb), err
return [32]byte(secb), err
}
func DecryptToBytes(bech32string string, password string) (secretKey []byte, err error) {

View File

@@ -4,21 +4,21 @@ import (
"fmt"
"math/rand"
"github.com/mailru/easyjson"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip44"
"github.com/mailru/easyjson"
)
// GiftWrap takes a 'rumor', encrypts it with our own key, making a 'seal', then encrypts that with a nonce key and
// signs that (after potentially applying a modify function, which can be nil otherwise), yielding a 'gift-wrap'.
func GiftWrap(
rumor nostr.Event,
recipient string,
recipient nostr.PubKey,
encrypt func(plaintext string) (string, error),
sign func(*nostr.Event) error,
modify func(*nostr.Event),
) (nostr.Event, error) {
rumor.Sig = ""
rumor.Sig = [64]byte{}
rumorCiphertext, err := encrypt(rumor.String())
if err != nil {
@@ -51,7 +51,7 @@ func GiftWrap(
Content: sealCiphertext,
CreatedAt: nostr.Now() - nostr.Timestamp(60*rand.Int63n(600) /* up to 6 hours in the past */),
Tags: nostr.Tags{
nostr.Tag{"p", recipient},
nostr.Tag{"p", recipient.Hex()},
},
}
if modify != nil {
@@ -66,7 +66,7 @@ func GiftWrap(
func GiftUnwrap(
gw nostr.Event,
decrypt func(otherpubkey, ciphertext string) (string, error),
decrypt func(otherpubkey nostr.PubKey, ciphertext string) (string, error),
) (rumor nostr.Event, err error) {
jseal, err := decrypt(gw.PubKey, gw.Content)
if err != nil {
@@ -79,7 +79,7 @@ func GiftUnwrap(
return rumor, fmt.Errorf("seal is invalid json: %w", err)
}
if ok, _ := seal.CheckSignature(); !ok {
if !seal.VerifySignature() {
return rumor, fmt.Errorf("seal signature is invalid")
}

View File

@@ -50,7 +50,7 @@ type Wallet struct {
func LoadWallet(
ctx context.Context,
kr nostr.Keyer,
pool *nostr.SimplePool,
pool *nostr.Pool,
relays []string,
) *Wallet {
return loadWalletFromPool(ctx, kr, pool, relays, false)
@@ -59,7 +59,7 @@ func LoadWallet(
func LoadWalletWithHistory(
ctx context.Context,
kr nostr.Keyer,
pool *nostr.SimplePool,
pool *nostr.Pool,
relays []string,
) *Wallet {
return loadWalletFromPool(ctx, kr, pool, relays, true)
@@ -68,7 +68,7 @@ func LoadWalletWithHistory(
func loadWalletFromPool(
ctx context.Context,
kr nostr.Keyer,
pool *nostr.SimplePool,
pool *nostr.Pool,
relays []string,
withHistory bool,
) *Wallet {

View File

@@ -19,7 +19,7 @@ func SendNutzap(
ctx context.Context,
kr nostr.Keyer,
w *nip60.Wallet,
pool *nostr.SimplePool,
pool *nostr.Pool,
targetUserPublickey string,
getUserReadRelays func(context.Context, string, int) []string,
relays []string,

View File

@@ -14,13 +14,13 @@ func FetchIDsOnly(
url string,
filter nostr.Filter,
) (<-chan nostr.ID, error) {
id := "go-nostr-tmp" // for now we can't have more than one subscription in the same connection
id := "nl-tmp" // for now we can't have more than one subscription in the same connection
neg := negentropy.New(empty.Empty{}, 1024*1024)
result := make(chan error)
var r *nostr.Relay
r, err := nostr.RelayConnect(ctx, url, nostr.WithCustomHandler(func(data string) {
r, err := nostr.RelayConnect(ctx, url, nostr.RelayOptions{CustomHandler: func(data string) {
envelope := ParseNegMessage(data)
if envelope == nil {
return
@@ -44,7 +44,7 @@ func FetchIDsOnly(
r.Write(msgb)
}
}
}))
}})
if err != nil {
return nil, err
}

View File

@@ -16,10 +16,15 @@ func DecodeRequest(req Request) (MethodParams, error) {
if len(req.Params) == 0 {
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
}
pk, ok := req.Params[0].(string)
if !ok || !nostr.IsValidPublicKey(pk) {
pkh, ok := req.Params[0].(string)
if !ok {
return nil, fmt.Errorf("missing pubkey param for '%s'", req.Method)
}
pk, err := nostr.PubKeyFromHex(pkh)
if err != nil {
return nil, fmt.Errorf("invalid pubkey param for '%s'", req.Method)
}
var reason string
if len(req.Params) >= 2 {
reason, _ = req.Params[1].(string)
@@ -31,10 +36,15 @@ func DecodeRequest(req Request) (MethodParams, error) {
if len(req.Params) == 0 {
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
}
pk, ok := req.Params[0].(string)
if !ok || !nostr.IsValidPublicKey(pk) {
pkh, ok := req.Params[0].(string)
if !ok {
return nil, fmt.Errorf("missing pubkey param for '%s'", req.Method)
}
pk, err := nostr.PubKeyFromHex(pkh)
if err != nil {
return nil, fmt.Errorf("invalid pubkey param for '%s'", req.Method)
}
var reason string
if len(req.Params) >= 2 {
reason, _ = req.Params[1].(string)
@@ -48,10 +58,15 @@ func DecodeRequest(req Request) (MethodParams, error) {
if len(req.Params) == 0 {
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
}
id, ok := req.Params[0].(string)
if !ok || !nostr.IsValid32ByteHex(id) {
idh, ok := req.Params[0].(string)
if !ok {
return nil, fmt.Errorf("missing id param for '%s'", req.Method)
}
id, err := nostr.IDFromHex(idh)
if err != nil {
return nil, fmt.Errorf("invalid id param for '%s'", req.Method)
}
var reason string
if len(req.Params) >= 2 {
reason, _ = req.Params[1].(string)
@@ -61,10 +76,15 @@ func DecodeRequest(req Request) (MethodParams, error) {
if len(req.Params) == 0 {
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
}
id, ok := req.Params[0].(string)
if !ok || !nostr.IsValid32ByteHex(id) {
idh, ok := req.Params[0].(string)
if !ok {
return nil, fmt.Errorf("missing id param for '%s'", req.Method)
}
id, err := nostr.IDFromHex(idh)
if err != nil {
return nil, fmt.Errorf("invalid id param for '%s'", req.Method)
}
var reason string
if len(req.Params) >= 2 {
reason, _ = req.Params[1].(string)
@@ -149,11 +169,19 @@ func DecodeRequest(req Request) (MethodParams, error) {
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
}
pubkey := req.Params[0].(string)
pkh, ok := req.Params[0].(string)
if !ok {
return nil, fmt.Errorf("missing pubkey param for '%s'", req.Method)
}
pk, err := nostr.PubKeyFromHex(pkh)
if err != nil {
return nil, fmt.Errorf("invalid pubkey param for '%s'", req.Method)
}
allowedMethods := req.Params[1].([]string)
return GrantAdmin{
Pubkey: pubkey,
Pubkey: pk,
AllowMethods: allowedMethods,
}, nil
case "revokeadmin":
@@ -161,11 +189,19 @@ func DecodeRequest(req Request) (MethodParams, error) {
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
}
pubkey := req.Params[0].(string)
pkh, ok := req.Params[0].(string)
if !ok {
return nil, fmt.Errorf("missing pubkey param for '%s'", req.Method)
}
pk, err := nostr.PubKeyFromHex(pkh)
if err != nil {
return nil, fmt.Errorf("invalid pubkey param for '%s'", req.Method)
}
disallowedMethods := req.Params[1].([]string)
return RevokeAdmin{
Pubkey: pubkey,
Pubkey: pk,
DisallowMethods: disallowedMethods,
}, nil
case "stats":
@@ -210,7 +246,7 @@ type SupportedMethods struct{}
func (SupportedMethods) MethodName() string { return "supportedmethods" }
type BanPubKey struct {
PubKey string
PubKey nostr.PubKey
Reason string
}
@@ -221,7 +257,7 @@ type ListBannedPubKeys struct{}
func (ListBannedPubKeys) MethodName() string { return "listbannedpubkeys" }
type AllowPubKey struct {
PubKey string
PubKey nostr.PubKey
Reason string
}
@@ -236,14 +272,14 @@ type ListEventsNeedingModeration struct{}
func (ListEventsNeedingModeration) MethodName() string { return "listeventsneedingmoderation" }
type AllowEvent struct {
ID string
ID nostr.ID
Reason string
}
func (AllowEvent) MethodName() string { return "allowevent" }
type BanEvent struct {
ID string
ID nostr.ID
Reason string
}
@@ -314,14 +350,14 @@ type ListDisallowedKinds struct{}
func (ListDisallowedKinds) MethodName() string { return "listdisallowedkinds" }
type GrantAdmin struct {
Pubkey string
Pubkey nostr.PubKey
AllowMethods []string
}
func (GrantAdmin) MethodName() string { return "grantadmin" }
type RevokeAdmin struct {
Pubkey string
Pubkey nostr.PubKey
DisallowMethods []string
}

View File

@@ -7,7 +7,7 @@ import (
"time"
)
func (pool *SimplePool) PaginatorWithInterval(
func (pool *Pool) PaginatorWithInterval(
interval time.Duration,
) func(ctx context.Context, urls []string, filter Filter, opts ...SubscriptionOption) chan RelayEvent {
return func(ctx context.Context, urls []string, filter Filter, opts ...SubscriptionOption) chan RelayEvent {

265
pool.go
View File

@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"math"
"net/http"
"slices"
"strings"
"sync"
@@ -20,22 +19,22 @@ const (
seenAlreadyDropTick = time.Minute
)
// SimplePool manages connections to multiple relays, ensures they are reopened when necessary and not duplicated.
type SimplePool struct {
// Pool manages connections to multiple relays, ensures they are reopened when necessary and not duplicated.
type Pool struct {
Relays *xsync.MapOf[string, *Relay]
Context context.Context
authHandler func(context.Context, RelayEvent) error
authHandler func(context.Context, *Event) error
cancel context.CancelCauseFunc
eventMiddleware func(RelayEvent)
duplicateMiddleware func(relay string, id ID)
queryMiddleware func(relay string, pubkey PubKey, kind uint16)
relayOptions RelayOptions
// custom things not often used
penaltyBoxMu sync.Mutex
penaltyBox map[string][2]float64
relayOptions []RelayOption
}
// DirectedFilter combines a Filter with a specific relay URL.
@@ -44,64 +43,58 @@ type DirectedFilter struct {
Relay string
}
// RelayEvent represents an event received from a specific relay.
type RelayEvent struct {
*Event
Relay *Relay
}
func (ie RelayEvent) String() string { return fmt.Sprintf("[%s] >> %s", ie.Relay.URL, ie.Event) }
// PoolOption is an interface for options that can be applied to a SimplePool.
type PoolOption interface {
ApplyPoolOption(*SimplePool)
}
// NewPool creates a new Pool with the given context and options.
func NewPool(opts PoolOptions) *Pool {
ctx, cancel := context.WithCancelCause(context.Background())
// NewSimplePool creates a new SimplePool with the given context and options.
func NewSimplePool(ctx context.Context, opts ...PoolOption) *SimplePool {
ctx, cancel := context.WithCancelCause(ctx)
pool := &SimplePool{
pool := &Pool{
Relays: xsync.NewMapOf[string, *Relay](),
Context: ctx,
cancel: cancel,
authHandler: opts.AuthHandler,
eventMiddleware: opts.EventMiddleware,
duplicateMiddleware: opts.DuplicateMiddleware,
queryMiddleware: opts.AuthorKindQueryMiddleware,
relayOptions: opts.RelayOptions,
}
for _, opt := range opts {
opt.ApplyPoolOption(pool)
if opts.PenaltyBox {
go pool.startPenaltyBox()
}
return pool
}
// WithRelayOptions sets options that will be used on every relay instance created by this pool.
func WithRelayOptions(ropts ...RelayOption) withRelayOptionsOpt {
return ropts
type PoolOptions struct {
// AuthHandler, if given, must be a function that signs the auth event when called.
// it will be called whenever any relay in the pool returns a `CLOSED` message
// with the "auth-required:" prefix, only once for each relay
AuthHandler func(context.Context, *Event) error
// PenaltyBox just sets the penalty box mechanism so relays that fail to connect
// or that disconnect will be ignored for a while and we won't attempt to connect again.
PenaltyBox bool
// EventMiddleware is a function that will be called with all events received.
EventMiddleware func(RelayEvent)
// DuplicateMiddleware is a function that will be called with all duplicate ids received.
DuplicateMiddleware func(relay string, id ID)
// AuthorKindQueryMiddleware is a function that will be called with every combination of
// relay+pubkey+kind queried in a .SubscribeMany*() call -- when applicable (i.e. when the query
// contains a pubkey and a kind).
AuthorKindQueryMiddleware func(relay string, pubkey PubKey, kind uint16)
// RelayOptions are any options that should be passed to Relays instantiated by this pool
RelayOptions RelayOptions
}
type withRelayOptionsOpt []RelayOption
func (h withRelayOptionsOpt) ApplyPoolOption(pool *SimplePool) {
pool.relayOptions = h
}
// WithAuthHandler must be a function that signs the auth event when called.
// it will be called whenever any relay in the pool returns a `CLOSED` message
// with the "auth-required:" prefix, only once for each relay
type WithAuthHandler func(ctx context.Context, authEvent RelayEvent) error
func (h WithAuthHandler) ApplyPoolOption(pool *SimplePool) {
pool.authHandler = h
}
// WithPenaltyBox just sets the penalty box mechanism so relays that fail to connect
// or that disconnect will be ignored for a while and we won't attempt to connect again.
func WithPenaltyBox() withPenaltyBoxOpt { return withPenaltyBoxOpt{} }
type withPenaltyBoxOpt struct{}
func (h withPenaltyBoxOpt) ApplyPoolOption(pool *SimplePool) {
func (pool *Pool) startPenaltyBox() {
pool.penaltyBox = make(map[string][2]float64)
go func() {
sleep := 30.0
@@ -131,38 +124,9 @@ func (h withPenaltyBoxOpt) ApplyPoolOption(pool *SimplePool) {
}()
}
// WithEventMiddleware is a function that will be called with all events received.
type WithEventMiddleware func(RelayEvent)
func (h WithEventMiddleware) ApplyPoolOption(pool *SimplePool) {
pool.eventMiddleware = h
}
// WithDuplicateMiddleware is a function that will be called with all duplicate ids received.
type WithDuplicateMiddleware func(relay string, id ID)
func (h WithDuplicateMiddleware) ApplyPoolOption(pool *SimplePool) {
pool.duplicateMiddleware = h
}
// WithAuthorKindQueryMiddleware is a function that will be called with every combination of relay+pubkey+kind queried
// in a .SubMany*() call -- when applicable (i.e. when the query contains a pubkey and a kind).
type WithAuthorKindQueryMiddleware func(relay string, pubkey PubKey, kind uint16)
func (h WithAuthorKindQueryMiddleware) ApplyPoolOption(pool *SimplePool) {
pool.queryMiddleware = h
}
var (
_ PoolOption = (WithAuthHandler)(nil)
_ PoolOption = (WithEventMiddleware)(nil)
_ PoolOption = WithPenaltyBox()
_ PoolOption = WithRelayOptions(WithRequestHeader(http.Header{}))
)
// EnsureRelay ensures that a relay connection exists and is active.
// If the relay is not connected, it attempts to connect.
func (pool *SimplePool) EnsureRelay(url string) (*Relay, error) {
func (pool *Pool) EnsureRelay(url string) (*Relay, error) {
nm := NormalizeURL(url)
defer namedLock(nm)()
@@ -190,7 +154,7 @@ func (pool *SimplePool) EnsureRelay(url string) (*Relay, error) {
)
defer cancel()
relay = NewRelay(context.Background(), url, pool.relayOptions...)
relay = NewRelay(pool.Context, url, pool.relayOptions)
if err := relay.Connect(ctx); err != nil {
if pool.penaltyBox != nil {
// putting relay in penalty box
@@ -214,7 +178,7 @@ type PublishResult struct {
}
// PublishMany publishes an event to multiple relays and returns a channel of results emitted as they're received.
func (pool *SimplePool) PublishMany(ctx context.Context, urls []string, evt Event) chan PublishResult {
func (pool *Pool) PublishMany(ctx context.Context, urls []string, evt Event) chan PublishResult {
ch := make(chan PublishResult, len(urls))
wg := sync.WaitGroup{}
@@ -235,9 +199,7 @@ func (pool *SimplePool) PublishMany(ctx context.Context, urls []string, evt Even
ch <- PublishResult{nil, url, relay}
} else if strings.HasPrefix(err.Error(), "msg: auth-required:") && pool.authHandler != nil {
// try to authenticate if we can
if authErr := relay.Auth(ctx, func(event *Event) error {
return pool.authHandler(ctx, RelayEvent{Event: event, Relay: relay})
}); authErr == nil {
if authErr := relay.Auth(ctx, pool.authHandler); authErr == nil {
if err := relay.Publish(ctx, evt); err == nil {
// success after auth
ch <- PublishResult{nil, url, relay}
@@ -265,36 +227,46 @@ func (pool *SimplePool) PublishMany(ctx context.Context, urls []string, evt Even
// SubscribeMany opens a subscription with the given filter to multiple relays
// the subscriptions ends when the context is canceled or when all relays return a CLOSED.
func (pool *SimplePool) SubscribeMany(
func (pool *Pool) SubscribeMany(
ctx context.Context,
urls []string,
filter Filter,
opts ...SubscriptionOption,
opts SubscriptionOptions,
) chan RelayEvent {
return pool.subMany(ctx, urls, filter, nil, opts...)
return pool.subMany(ctx, urls, filter, nil, opts)
}
// FetchMany opens a subscription, much like SubscribeMany, but it ends as soon as all Relays
// return an EOSE message.
func (pool *SimplePool) FetchMany(
func (pool *Pool) FetchMany(
ctx context.Context,
urls []string,
filter Filter,
opts ...SubscriptionOption,
opts SubscriptionOptions,
) chan RelayEvent {
return pool.SubManyEose(ctx, urls, filter, opts...)
seenAlready := xsync.NewMapOf[ID, struct{}]()
opts.CheckDuplicate = func(id ID, relay string) bool {
_, exists := seenAlready.LoadOrStore(id, struct{}{})
if exists && pool.duplicateMiddleware != nil {
pool.duplicateMiddleware(relay, id)
}
return exists
}
return pool.subManyEoseNonOverwriteCheckDuplicate(ctx, urls, filter, opts)
}
// SubscribeManyNotifyEOSE is like SubscribeMany, but takes a channel that is closed when
// all subscriptions have received an EOSE
func (pool *SimplePool) SubscribeManyNotifyEOSE(
func (pool *Pool) SubscribeManyNotifyEOSE(
ctx context.Context,
urls []string,
filter Filter,
eoseChan chan struct{},
opts ...SubscriptionOption,
opts SubscriptionOptions,
) chan RelayEvent {
return pool.subMany(ctx, urls, filter, eoseChan, opts...)
return pool.subMany(ctx, urls, filter, eoseChan, opts)
}
type ReplaceableKey struct {
@@ -304,21 +276,21 @@ type ReplaceableKey struct {
// FetchManyReplaceable is like FetchMany, but deduplicates replaceable and addressable events and returns
// only the latest for each "d" tag.
func (pool *SimplePool) FetchManyReplaceable(
func (pool *Pool) FetchManyReplaceable(
ctx context.Context,
urls []string,
filter Filter,
opts ...SubscriptionOption,
) *xsync.MapOf[ReplaceableKey, *Event] {
opts SubscriptionOptions,
) *xsync.MapOf[ReplaceableKey, Event] {
ctx, cancel := context.WithCancelCause(ctx)
results := xsync.NewMapOf[ReplaceableKey, *Event]()
results := xsync.NewMapOf[ReplaceableKey, Event]()
wg := sync.WaitGroup{}
wg.Add(len(urls))
seenAlreadyLatest := xsync.NewMapOf[ReplaceableKey, Timestamp]()
opts = append(opts, WithCheckDuplicateReplaceable(func(rk ReplaceableKey, ts Timestamp) bool {
opts.CheckDuplicateReplaceable = func(rk ReplaceableKey, ts Timestamp) bool {
updated := false
seenAlreadyLatest.Compute(rk, func(latest Timestamp, _ bool) (newValue Timestamp, delete bool) {
if ts > latest {
@@ -328,7 +300,7 @@ func (pool *SimplePool) FetchManyReplaceable(
return latest, false // the one we had was already more recent
})
return updated
}))
}
for _, url := range urls {
go func(nm string) {
@@ -353,7 +325,7 @@ func (pool *SimplePool) FetchManyReplaceable(
hasAuthed := false
subscribe:
sub, err := relay.Subscribe(ctx, filter, opts...)
sub, err := relay.Subscribe(ctx, filter, opts)
if err != nil {
debugLogf("error subscribing to %s with %v: %s", relay, filter, err)
return
@@ -368,9 +340,7 @@ func (pool *SimplePool) FetchManyReplaceable(
case reason := <-sub.ClosedReason:
if strings.HasPrefix(reason, "auth-required:") && pool.authHandler != nil && !hasAuthed {
// relay is requesting auth. if we can we will perform auth and try again
err := relay.Auth(ctx, func(event *Event) error {
return pool.authHandler(ctx, RelayEvent{Event: event, Relay: relay})
})
err := relay.Auth(ctx, pool.authHandler)
if err == nil {
hasAuthed = true // so we don't keep doing AUTH again and again
goto subscribe
@@ -401,12 +371,12 @@ func (pool *SimplePool) FetchManyReplaceable(
return results
}
func (pool *SimplePool) subMany(
func (pool *Pool) subMany(
ctx context.Context,
urls []string,
filter Filter,
eoseChan chan struct{},
opts ...SubscriptionOption,
opts SubscriptionOptions,
) chan RelayEvent {
ctx, cancel := context.WithCancelCause(ctx)
_ = cancel // do this so `go vet` will stop complaining
@@ -423,6 +393,14 @@ func (pool *SimplePool) subMany(
}()
}
opts.CheckDuplicate = func(id ID, relay string) bool {
_, exists := seenAlready.Load(id)
if exists && pool.duplicateMiddleware != nil {
pool.duplicateMiddleware(relay, id)
}
return exists
}
pending := xsync.NewCounter()
pending.Add(int64(len(urls)))
for i, url := range urls {
@@ -485,15 +463,7 @@ func (pool *SimplePool) subMany(
hasAuthed = false
subscribe:
sub, err = relay.Subscribe(ctx, filter, append(opts,
WithCheckDuplicate(func(id ID, relay string) bool {
_, exists := seenAlready.Load(id)
if exists && pool.duplicateMiddleware != nil {
pool.duplicateMiddleware(relay, id)
}
return exists
}),
)...)
sub, err = relay.Subscribe(ctx, filter, opts)
if err != nil {
debugLogf("%s reconnecting because subscription died\n", nm)
goto reconnect
@@ -546,9 +516,7 @@ func (pool *SimplePool) subMany(
case reason := <-sub.ClosedReason:
if strings.HasPrefix(reason, "auth-required:") && pool.authHandler != nil && !hasAuthed {
// relay is requesting auth. if we can we will perform auth and try again
err := relay.Auth(ctx, func(event *Event) error {
return pool.authHandler(ctx, RelayEvent{Event: event, Relay: relay})
})
err := relay.Auth(ctx, pool.authHandler)
if err == nil {
hasAuthed = true // so we don't keep doing AUTH again and again
goto subscribe
@@ -575,32 +543,11 @@ func (pool *SimplePool) subMany(
return events
}
// Deprecated: SubManyEose is deprecated: use FetchMany instead.
func (pool *SimplePool) SubManyEose(
func (pool *Pool) subManyEoseNonOverwriteCheckDuplicate(
ctx context.Context,
urls []string,
filter Filter,
opts ...SubscriptionOption,
) chan RelayEvent {
seenAlready := xsync.NewMapOf[ID, struct{}]()
return pool.subManyEoseNonOverwriteCheckDuplicate(ctx, urls, filter,
WithCheckDuplicate(func(id ID, relay string) bool {
_, exists := seenAlready.LoadOrStore(id, struct{}{})
if exists && pool.duplicateMiddleware != nil {
pool.duplicateMiddleware(relay, id)
}
return exists
}),
opts...,
)
}
func (pool *SimplePool) subManyEoseNonOverwriteCheckDuplicate(
ctx context.Context,
urls []string,
filter Filter,
wcd WithCheckDuplicate,
opts ...SubscriptionOption,
opts SubscriptionOptions,
) chan RelayEvent {
ctx, cancel := context.WithCancelCause(ctx)
@@ -608,8 +555,6 @@ func (pool *SimplePool) subManyEoseNonOverwriteCheckDuplicate(
wg := sync.WaitGroup{}
wg.Add(len(urls))
opts = append(opts, wcd)
go func() {
// this will happen when all subscriptions get an eose (or when they die)
wg.Wait()
@@ -640,7 +585,7 @@ func (pool *SimplePool) subManyEoseNonOverwriteCheckDuplicate(
hasAuthed := false
subscribe:
sub, err := relay.Subscribe(ctx, filter, opts...)
sub, err := relay.Subscribe(ctx, filter, opts)
if err != nil {
debugLogf("error subscribing to %s with %v: %s", relay, filter, err)
return
@@ -655,9 +600,7 @@ func (pool *SimplePool) subManyEoseNonOverwriteCheckDuplicate(
case reason := <-sub.ClosedReason:
if strings.HasPrefix(reason, "auth-required:") && pool.authHandler != nil && !hasAuthed {
// relay is requesting auth. if we can we will perform auth and try again
err := relay.Auth(ctx, func(event *Event) error {
return pool.authHandler(ctx, RelayEvent{Event: event, Relay: relay})
})
err := relay.Auth(ctx, pool.authHandler)
if err == nil {
hasAuthed = true // so we don't keep doing AUTH again and again
goto subscribe
@@ -689,11 +632,11 @@ func (pool *SimplePool) subManyEoseNonOverwriteCheckDuplicate(
}
// CountMany aggregates count results from multiple relays using NIP-45 HyperLogLog
func (pool *SimplePool) CountMany(
func (pool *Pool) CountMany(
ctx context.Context,
urls []string,
filter Filter,
opts []SubscriptionOption,
opts SubscriptionOptions,
) int {
hll := hyperloglog.New(0) // offset is irrelevant here
@@ -706,7 +649,7 @@ func (pool *SimplePool) CountMany(
if err != nil {
return
}
ce, err := relay.countInternal(ctx, filter, opts...)
ce, err := relay.countInternal(ctx, filter, opts)
if err != nil {
return
}
@@ -722,14 +665,14 @@ func (pool *SimplePool) CountMany(
}
// QuerySingle returns the first event returned by the first relay, cancels everything else.
func (pool *SimplePool) QuerySingle(
func (pool *Pool) QuerySingle(
ctx context.Context,
urls []string,
filter Filter,
opts ...SubscriptionOption,
opts SubscriptionOptions,
) *RelayEvent {
ctx, cancel := context.WithCancelCause(ctx)
for ievt := range pool.SubManyEose(ctx, urls, filter, opts...) {
for ievt := range pool.FetchMany(ctx, urls, filter, opts) {
cancel(errors.New("got the first event and ended successfully"))
return &ievt
}
@@ -738,28 +681,30 @@ func (pool *SimplePool) QuerySingle(
}
// BatchedSubManyEose performs batched subscriptions to multiple relays with different filters.
func (pool *SimplePool) BatchedSubManyEose(
func (pool *Pool) BatchedSubManyEose(
ctx context.Context,
dfs []DirectedFilter,
opts ...SubscriptionOption,
opts SubscriptionOptions,
) chan RelayEvent {
res := make(chan RelayEvent)
wg := sync.WaitGroup{}
wg.Add(len(dfs))
seenAlready := xsync.NewMapOf[ID, struct{}]()
opts.CheckDuplicate = func(id ID, relay string) bool {
_, exists := seenAlready.LoadOrStore(id, struct{}{})
if exists && pool.duplicateMiddleware != nil {
pool.duplicateMiddleware(relay, id)
}
return exists
}
for _, df := range dfs {
go func(df DirectedFilter) {
for ie := range pool.subManyEoseNonOverwriteCheckDuplicate(ctx,
[]string{df.Relay},
df.Filter,
WithCheckDuplicate(func(id ID, relay string) bool {
_, exists := seenAlready.LoadOrStore(id, struct{}{})
if exists && pool.duplicateMiddleware != nil {
pool.duplicateMiddleware(relay, id)
}
return exists
}), opts...,
opts,
) {
select {
case res <- ie:
@@ -781,6 +726,6 @@ func (pool *SimplePool) BatchedSubManyEose(
}
// Close closes the pool with the given reason.
func (pool *SimplePool) Close(reason string) {
func (pool *Pool) Close(reason string) {
pool.cancel(fmt.Errorf("pool closed with reason: '%s'", reason))
}

150
relay.go
View File

@@ -51,7 +51,7 @@ type writeRequest struct {
}
// NewRelay returns a new relay. It takes a context that, when canceled, will close the relay connection.
func NewRelay(ctx context.Context, url string, opts ...RelayOption) *Relay {
func NewRelay(ctx context.Context, url string, opts RelayOptions) *Relay {
ctx, cancel := context.WithCancelCause(ctx)
r := &Relay{
URL: NormalizeURL(url),
@@ -64,10 +64,6 @@ func NewRelay(ctx context.Context, url string, opts ...RelayOption) *Relay {
requestHeader: nil,
}
for _, opt := range opts {
opt.ApplyRelayOption(r)
}
return r
}
@@ -77,44 +73,23 @@ func NewRelay(ctx context.Context, url string, opts ...RelayOption) *Relay {
//
// The ongoing relay connection uses a background context. To close the connection, call r.Close().
// If you need fine grained long-term connection contexts, use NewRelay() instead.
func RelayConnect(ctx context.Context, url string, opts ...RelayOption) (*Relay, error) {
r := NewRelay(context.Background(), url, opts...)
func RelayConnect(ctx context.Context, url string, opts RelayOptions) (*Relay, error) {
r := NewRelay(context.Background(), url, opts)
err := r.Connect(ctx)
return r, err
}
// RelayOption is the type of the argument passed when instantiating relay connections.
type RelayOption interface {
ApplyRelayOption(*Relay)
}
type RelayOptions struct {
// NoticeHandler just takes notices and is expected to do something with them.
// When not given defaults to logging the notices.
NoticeHandler func(notice string)
var (
_ RelayOption = (WithNoticeHandler)(nil)
_ RelayOption = (WithCustomHandler)(nil)
_ RelayOption = (WithRequestHeader)(nil)
)
// CustomHandler, if given, must be a function that handles any relay message
// that couldn't be parsed as a standard envelope.
CustomHandler func(data string)
// WithNoticeHandler just takes notices and is expected to do something with them.
// when not given, defaults to logging the notices.
type WithNoticeHandler func(notice string)
func (nh WithNoticeHandler) ApplyRelayOption(r *Relay) {
r.noticeHandler = nh
}
// WithCustomHandler must be a function that handles any relay message that couldn't be
// parsed as a standard envelope.
type WithCustomHandler func(data string)
func (ch WithCustomHandler) ApplyRelayOption(r *Relay) {
r.customHandler = ch
}
// WithRequestHeader sets the HTTP request header of the websocket preflight request.
type WithRequestHeader http.Header
func (ch WithRequestHeader) ApplyRelayOption(r *Relay) {
r.requestHeader = http.Header(ch)
// RequestHeader sets the HTTP request header of the websocket preflight request
RequestHeader http.Header
}
// String just returns the relay URL.
@@ -273,21 +248,21 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
continue
} else {
// check if the event matches the desired filter, ignore otherwise
if !sub.match(&env.Event) {
InfoLogger.Printf("{%s} filter does not match: %v ~ %v\n", r.URL, sub.Filters, env.Event)
if !sub.match(env.Event) {
InfoLogger.Printf("{%s} filter does not match: %v ~ %v\n", r.URL, sub.Filter, env.Event)
continue
}
// check signature, ignore invalid, except from trusted (AssumeValid) relays
if !r.AssumeValid {
if ok, _ := env.Event.CheckSignature(); !ok {
if !env.Event.VerifySignature() {
InfoLogger.Printf("{%s} bad signature on %s\n", r.URL, env.Event.ID)
continue
}
}
// dispatch this to the internal .events channel of the subscription
sub.dispatchEvent(&env.Event)
sub.dispatchEvent(env.Event)
}
case *EOSEEnvelope:
if subscription, ok := r.Subscriptions.Load(subIdToSerial(string(*env))); ok {
@@ -334,7 +309,7 @@ func (r *Relay) Publish(ctx context.Context, event Event) error {
//
// You don't have to build the AUTH event yourself, this function takes a function to which the
// event that must be signed will be passed, so it's only necessary to sign that.
func (r *Relay) Auth(ctx context.Context, sign func(event *Event) error) error {
func (r *Relay) Auth(ctx context.Context, sign func(context.Context, *Event) error) error {
authEvent := Event{
CreatedAt: Now(),
Kind: KindClientAuthentication,
@@ -344,7 +319,7 @@ func (r *Relay) Auth(ctx context.Context, sign func(event *Event) error) error {
},
Content: "",
}
if err := sign(&authEvent); err != nil {
if err := sign(ctx, &authEvent); err != nil {
return fmt.Errorf("error signing auth event: %w", err)
}
@@ -404,15 +379,15 @@ func (r *Relay) publish(ctx context.Context, id ID, env Envelope) error {
//
// Remember to cancel subscriptions, either by calling `.Unsub()` on them or ensuring their `context.Context` will be canceled at some point.
// Failure to do that will result in a huge number of halted goroutines being created.
func (r *Relay) Subscribe(ctx context.Context, filters Filters, opts ...SubscriptionOption) (*Subscription, error) {
sub := r.PrepareSubscription(ctx, filters, opts...)
func (r *Relay) Subscribe(ctx context.Context, filter Filter, opts SubscriptionOptions) (*Subscription, error) {
sub := r.PrepareSubscription(ctx, filter, opts)
if r.Connection == nil {
return nil, fmt.Errorf("not connected to %s", r.URL)
}
if err := sub.Fire(); err != nil {
return nil, fmt.Errorf("couldn't subscribe to %v at %s: %w", filters, r.URL, err)
return nil, fmt.Errorf("couldn't subscribe to %v at %s: %w", filter, r.URL, err)
}
return sub, nil
@@ -422,7 +397,7 @@ func (r *Relay) Subscribe(ctx context.Context, filters Filters, opts ...Subscrip
//
// Remember to cancel subscriptions, either by calling `.Unsub()` on them or ensuring their `context.Context` will be canceled at some point.
// Failure to do that will result in a huge number of halted goroutines being created.
func (r *Relay) PrepareSubscription(ctx context.Context, filters Filters, opts ...SubscriptionOption) *Subscription {
func (r *Relay) PrepareSubscription(ctx context.Context, filter Filter, opts SubscriptionOptions) *Subscription {
current := subscriptionIDCounter.Add(1)
ctx, cancel := context.WithCancelCause(ctx)
@@ -431,30 +406,21 @@ func (r *Relay) PrepareSubscription(ctx context.Context, filters Filters, opts .
Context: ctx,
cancel: cancel,
counter: current,
Events: make(chan *Event),
Events: make(chan Event),
EndOfStoredEvents: make(chan struct{}, 1),
ClosedReason: make(chan string, 1),
Filters: filters,
match: filters.Match,
Filter: filter,
match: filter.Matches,
}
label := ""
for _, opt := range opts {
switch o := opt.(type) {
case WithLabel:
label = string(o)
case WithCheckDuplicate:
sub.checkDuplicate = o
case WithCheckDuplicateReplaceable:
sub.checkDuplicateReplaceable = o
}
}
sub.checkDuplicate = opts.CheckDuplicate
sub.checkDuplicateReplaceable = opts.CheckDuplicateReplaceable
// subscription id computation
buf := subIdPool.Get().([]byte)[:0]
buf = strconv.AppendInt(buf, sub.counter, 10)
buf = append(buf, ':')
buf = append(buf, label...)
buf = append(buf, opts.Label...)
defer subIdPool.Put(buf)
sub.id = string(buf)
@@ -467,63 +433,13 @@ func (r *Relay) PrepareSubscription(ctx context.Context, filters Filters, opts .
return sub
}
// QueryEvents subscribes to events matching the given filter and returns a channel of events.
//
// In most cases it's better to use SimplePool instead of this method.
func (r *Relay) QueryEvents(ctx context.Context, filter Filter) (chan *Event, error) {
sub, err := r.Subscribe(ctx, Filters{filter})
if err != nil {
return nil, err
}
go func() {
for {
select {
case <-sub.ClosedReason:
case <-sub.EndOfStoredEvents:
case <-ctx.Done():
case <-r.Context().Done():
}
sub.unsub(errors.New("QueryEvents() ended"))
return
}
}()
return sub.Events, nil
}
// QuerySync subscribes to events matching the given filter and returns a slice of events.
// This method blocks until all events are received or the context is canceled.
//
// In most cases it's better to use SimplePool instead of this method.
func (r *Relay) QuerySync(ctx context.Context, filter Filter) ([]*Event, error) {
if _, ok := ctx.Deadline(); !ok {
// if no timeout is set, force it to 7 seconds
var cancel context.CancelFunc
ctx, cancel = context.WithTimeoutCause(ctx, 7*time.Second, errors.New("QuerySync() took too long"))
defer cancel()
}
events := make([]*Event, 0, max(filter.Limit, 250))
ch, err := r.QueryEvents(ctx, filter)
if err != nil {
return nil, err
}
for evt := range ch {
events = append(events, evt)
}
return events, nil
}
// Count sends a "COUNT" command to the relay and returns the count of events matching the filters.
func (r *Relay) Count(
ctx context.Context,
filters Filters,
opts ...SubscriptionOption,
filter Filter,
opts SubscriptionOptions,
) (int64, []byte, error) {
v, err := r.countInternal(ctx, filters, opts...)
v, err := r.countInternal(ctx, filter, opts)
if err != nil {
return 0, nil, err
}
@@ -531,8 +447,8 @@ func (r *Relay) Count(
return *v.Count, v.HyperLogLog, nil
}
func (r *Relay) countInternal(ctx context.Context, filters Filters, opts ...SubscriptionOption) (CountEnvelope, error) {
sub := r.PrepareSubscription(ctx, filters, opts...)
func (r *Relay) countInternal(ctx context.Context, filter Filter, opts SubscriptionOptions) (CountEnvelope, error) {
sub := r.PrepareSubscription(ctx, filter, opts)
sub.countResult = make(chan CountEnvelope)
if err := sub.Fire(); err != nil {

View File

@@ -41,7 +41,7 @@ type System struct {
FollowSetsCache cache.Cache32[GenericSets[ProfileRef]]
TopicSetsCache cache.Cache32[GenericSets[Topic]]
Hints hints.HintsDB
Pool *nostr.SimplePool
Pool *nostr.Pool
RelayListRelays *RelayStream
FollowListRelays *RelayStream
MetadataRelays *RelayStream
@@ -118,7 +118,7 @@ func NewSystem(mods ...SystemModifier) *System {
Hints: memoryh.NewHintDB(),
}
sys.Pool = nostr.NewSimplePool(context.Background(),
sys.Pool = nostr.NewPool(context.Background(),
nostr.WithAuthorKindQueryMiddleware(sys.TrackQueryAttempts),
nostr.WithEventMiddleware(sys.TrackEventHintsAndRelays),
nostr.WithDuplicateMiddleware(sys.TrackEventRelaysD),

View File

@@ -4,31 +4,30 @@ package nostr
import (
"crypto/sha256"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
)
// CheckSignature checks if the event signature is valid for the given event.
// Verify checks if the event signature is valid for the given event.
// It won't look at the ID field, instead it will recompute the id from the entire event body.
// If the signature is invalid bool will be false and err will be set.
func (evt Event) CheckSignature() (bool, error) {
// Returns true if the signature is valid, false otherwise.
func (evt Event) VerifySignature() bool {
// read and check pubkey
pubkey, err := schnorr.ParsePubKey(evt.PubKey[:])
if err != nil {
return false, fmt.Errorf("event has invalid pubkey '%s': %w", evt.PubKey, err)
return false
}
// read signature
sig, err := schnorr.ParseSignature(evt.Sig[:])
if err != nil {
return false, fmt.Errorf("failed to parse signature: %w", err)
return false
}
// check signature
hash := sha256.Sum256(evt.Serialize())
return sig.Verify(hash[:], pubkey), nil
return sig.Verify(hash[:], pubkey)
}
// Sign signs an event with a given privateKey.

View File

@@ -21,7 +21,7 @@ type Subscription struct {
// the Events channel emits all EVENTs that come in a Subscription
// will be closed when the subscription ends
Events chan *Event
Events chan Event
mu sync.Mutex
// the EndOfStoredEvents channel gets closed when an EOSE comes for that subscription
@@ -41,7 +41,7 @@ type Subscription struct {
// if it returns true that event will not be processed further.
checkDuplicateReplaceable func(rk ReplaceableKey, ts Timestamp) bool
match func(*Event) bool // this will be either Filters.Match or Filters.MatchIgnoringTimestampConstraints
match func(Event) bool // this will be either Filters.Match or Filters.MatchIgnoringTimestampConstraints
live atomic.Bool
eosed atomic.Bool
cancel context.CancelCauseFunc
@@ -51,33 +51,19 @@ type Subscription struct {
storedwg sync.WaitGroup
}
// SubscriptionOption is the type of the argument passed when instantiating relay connections.
// Some examples are WithLabel.
type SubscriptionOption interface {
IsSubscriptionOption()
// All SubscriptionOptions fields are optional
type SubscriptionOptions struct {
// Label puts a label on the subscription (it is prepended to the automatic id) that is sent to relays.
Label string
// CheckDuplicate is a function that, when present, is ran on events before they're parsed.
// if it returns true the event will be discarded and not processed further.
CheckDuplicate func(id ID, relay string) bool
// CheckDuplicateReplaceable is like CheckDuplicate, but runs on replaceable/addressable events
CheckDuplicateReplaceable func(rk ReplaceableKey, ts Timestamp) bool
}
// WithLabel puts a label on the subscription (it is prepended to the automatic id) that is sent to relays.
type WithLabel string
func (_ WithLabel) IsSubscriptionOption() {}
// WithCheckDuplicate sets checkDuplicate on the subscription
type WithCheckDuplicate func(id ID, relay string) bool
func (_ WithCheckDuplicate) IsSubscriptionOption() {}
// WithCheckDuplicateReplaceable sets checkDuplicateReplaceable on the subscription
type WithCheckDuplicateReplaceable func(rk ReplaceableKey, ts Timestamp) bool
func (_ WithCheckDuplicateReplaceable) IsSubscriptionOption() {}
var (
_ SubscriptionOption = (WithLabel)("")
_ SubscriptionOption = (WithCheckDuplicate)(nil)
_ SubscriptionOption = (WithCheckDuplicateReplaceable)(nil)
)
func (sub *Subscription) start() {
<-sub.Context.Done()
@@ -93,7 +79,7 @@ func (sub *Subscription) start() {
// GetID returns the subscription ID.
func (sub *Subscription) GetID() string { return sub.id }
func (sub *Subscription) dispatchEvent(evt *Event) {
func (sub *Subscription) dispatchEvent(evt Event) {
added := false
if !sub.eosed.Load() {
sub.storedwg.Add(1)

View File

@@ -6,6 +6,12 @@ import (
"unsafe"
)
// RelayEvent represents an event received from a specific relay.
type RelayEvent struct {
Event
Relay *Relay
}
var (
ZeroID = [32]byte{}
ZeroPK = [32]byte{}
@@ -14,6 +20,7 @@ var (
type PubKey [32]byte
func (pk PubKey) String() string { return hex.EncodeToString(pk[:]) }
func (pk PubKey) Hex() string { return hex.EncodeToString(pk[:]) }
func PubKeyFromHex(pkh string) (PubKey, error) {
pk := PubKey{}
@@ -49,9 +56,19 @@ func MustPubKeyFromHex(pkh string) PubKey {
return pk
}
func ContainsPubKey(haystack []PubKey, needle PubKey) bool {
for _, cand := range haystack {
if cand == needle {
return true
}
}
return false
}
type ID [32]byte
func (id ID) String() string { return hex.EncodeToString(id[:]) }
func (id ID) Hex() string { return hex.EncodeToString(id[:]) }
func IDFromHex(idh string) (ID, error) {
id := ID{}

View File

@@ -46,39 +46,3 @@ func CompareEventReverse(b, a Event) int {
}
return cmp.Compare(a.CreatedAt, b.CreatedAt)
}
// CompareEventPtr is meant to to be used with slices.Sort
func CompareEventPtr(a, b *Event) int {
if a == nil {
if b == nil {
return 0
} else {
return -1
}
} else if b == nil {
return 1
}
if a.CreatedAt == b.CreatedAt {
return bytes.Compare(a.ID[:], b.ID[:])
}
return cmp.Compare(a.CreatedAt, b.CreatedAt)
}
// CompareEventPtrReverse is meant to to be used with slices.Sort
func CompareEventPtrReverse(b, a *Event) int {
if a == nil {
if b == nil {
return 0
} else {
return -1
}
} else if b == nil {
return 1
}
if a.CreatedAt == b.CreatedAt {
return bytes.Compare(a.ID[:], b.ID[:])
}
return cmp.Compare(a.CreatedAt, b.CreatedAt)
}

View File

@@ -53,23 +53,23 @@ func TestEventsCompare(t *testing.T) {
}
func TestEventsComparePtr(t *testing.T) {
list := []*Event{
list := []Event{
{CreatedAt: 12},
{CreatedAt: 8},
{CreatedAt: 26},
{CreatedAt: 1},
}
slices.SortFunc(list, CompareEventPtr)
require.Equal(t, []*Event{
slices.SortFunc(list, CompareEvent)
require.Equal(t, []Event{
{CreatedAt: 1},
{CreatedAt: 8},
{CreatedAt: 12},
{CreatedAt: 26},
}, list)
slices.SortFunc(list, CompareEventPtrReverse)
require.Equal(t, []*Event{
slices.SortFunc(list, CompareEventReverse)
require.Equal(t, []Event{
{CreatedAt: 26},
{CreatedAt: 12},
{CreatedAt: 8},