a big bundle of conversions and other changes.
This commit is contained in:
165
README.md
165
README.md
@@ -1,164 +1,3 @@
|
|||||||
[](https://fiatjaf.com/nostrlib/actions/workflows/test.yml)
|
nostr library
|
||||||
[](https://pkg.go.dev/fiatjaf.com/nostrlib)
|
|
||||||
[](https://goreportcard.com/report/fiatjaf.com/nostrlib)
|
|
||||||
|
|
||||||
<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>
|
do not use yet
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|||||||
8
event.go
8
event.go
@@ -25,24 +25,24 @@ func (evt Event) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetID serializes and returns the event ID as a 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())
|
return sha256.Sum256(evt.Serialize())
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckID checks if the implied ID matches the given ID more efficiently.
|
// 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
|
return evt.GetID() == evt.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize outputs a byte array that can be hashed to produce the canonical event "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
|
// the serialization process is just putting everything into a JSON array
|
||||||
// so the order is kept. See NIP-01
|
// so the order is kept. See NIP-01
|
||||||
dst := make([]byte, 0, 100+len(evt.Content)+len(evt.Tags)*80)
|
dst := make([]byte, 0, 100+len(evt.Content)+len(evt.Tags)*80)
|
||||||
return serializeEventInto(evt, dst)
|
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
|
// the header portion is easy to serialize
|
||||||
// [0,"pubkey",created_at,kind,[
|
// [0,"pubkey",created_at,kind,[
|
||||||
dst = append(dst, `[0,"`...)
|
dst = append(dst, `[0,"`...)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func TestEventParsingAndVerifying(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, ev.ID, ev.GetID())
|
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")
|
assert.True(t, ok, "signature verification failed when it should have succeeded")
|
||||||
|
|
||||||
asJSON, err := json.Marshal(ev)
|
asJSON, err := json.Marshal(ev)
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
package badger
|
package badger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v4"
|
|
||||||
bin "fiatjaf.com/nostr/eventstore/internal/binary"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/eventstore/codec/betterbinary"
|
||||||
"fiatjaf.com/nostr/nip45/hyperloglog"
|
"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
|
var count int64 = 0
|
||||||
|
|
||||||
queries, extraFilter, since, err := prepareQueries(filter)
|
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 {
|
err = item.Value(func(val []byte) error {
|
||||||
evt := &nostr.Event{}
|
evt := nostr.Event{}
|
||||||
if err := bin.Unmarshal(val, evt); err != nil {
|
if err := betterbinary.Unmarshal(val, &evt); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +86,7 @@ func (b *BadgerBackend) CountEvents(ctx context.Context, filter nostr.Filter) (i
|
|||||||
return count, err
|
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
|
var count int64 = 0
|
||||||
|
|
||||||
queries, extraFilter, since, err := prepareQueries(filter)
|
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 {
|
err = item.Value(func(val []byte) error {
|
||||||
if extraFilter == nil {
|
if extraFilter == nil {
|
||||||
hll.AddBytes(val[32:64])
|
hll.AddBytes([32]byte(val[32:64]))
|
||||||
count++
|
count++
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
evt := &nostr.Event{}
|
evt := nostr.Event{}
|
||||||
if err := bin.Unmarshal(val, evt); err != nil {
|
if err := betterbinary.Unmarshal(val, &evt); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if extraFilter.Matches(evt) {
|
if extraFilter.Matches(evt) {
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
package badger
|
package badger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"fmt"
|
||||||
"encoding/hex"
|
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v4"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/eventstore/codec/betterbinary"
|
||||||
|
"github.com/dgraph-io/badger/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
var serialDelete uint32 = 0
|
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
|
deletionHappened := false
|
||||||
|
|
||||||
err := b.Update(func(txn *badger.Txn) error {
|
err := b.Update(func(txn *badger.Txn) error {
|
||||||
var err error
|
var err error
|
||||||
deletionHappened, err = b.delete(txn, evt)
|
deletionHappened, err = b.delete(txn, id)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -36,22 +36,30 @@ func (b *BadgerBackend) DeleteEvent(ctx context.Context, evt *nostr.Event) error
|
|||||||
return nil
|
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 := make([]byte, 1, 5)
|
||||||
idx[0] = rawEventStorePrefix
|
idx[0] = rawEventStorePrefix
|
||||||
|
|
||||||
// query event by id to get its idx
|
// query event by id to get its idx
|
||||||
idPrefix8, _ := hex.DecodeString(evt.ID[0 : 8*2])
|
|
||||||
prefix := make([]byte, 1+8)
|
prefix := make([]byte, 1+8)
|
||||||
prefix[0] = indexIdPrefix
|
prefix[0] = indexIdPrefix
|
||||||
copy(prefix[1:], idPrefix8)
|
copy(prefix[1:], id[0:8])
|
||||||
opts := badger.IteratorOptions{
|
opts := badger.IteratorOptions{
|
||||||
PrefetchValues: false,
|
PrefetchValues: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// also grab the actual event so we can calculate its indexes
|
||||||
|
var evt nostr.Event
|
||||||
|
|
||||||
it := txn.NewIterator(opts)
|
it := txn.NewIterator(opts)
|
||||||
it.Seek(prefix)
|
it.Seek(prefix)
|
||||||
if it.ValidForPrefix(prefix) {
|
if it.ValidForPrefix(prefix) {
|
||||||
idx = append(idx, it.Item().Key()[1+8:]...)
|
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()
|
it.Close()
|
||||||
|
|
||||||
|
|||||||
@@ -40,14 +40,13 @@ func getTagIndexPrefix(tagValue string) ([]byte, int) {
|
|||||||
return k, offset
|
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) {
|
return func(yield func([]byte) bool) {
|
||||||
{
|
{
|
||||||
// ~ by id
|
// ~ by id
|
||||||
idPrefix8, _ := hex.DecodeString(evt.ID[0 : 8*2])
|
|
||||||
k := make([]byte, 1+8+4)
|
k := make([]byte, 1+8+4)
|
||||||
k[0] = indexIdPrefix
|
k[0] = indexIdPrefix
|
||||||
copy(k[1:], idPrefix8)
|
copy(k[1:], evt.ID[0:8])
|
||||||
copy(k[1+8:], idx)
|
copy(k[1+8:], idx)
|
||||||
if !yield(k) {
|
if !yield(k) {
|
||||||
return
|
return
|
||||||
@@ -56,10 +55,9 @@ func (b *BadgerBackend) getIndexKeysForEvent(evt *nostr.Event, idx []byte) iter.
|
|||||||
|
|
||||||
{
|
{
|
||||||
// ~ by pubkey+date
|
// ~ by pubkey+date
|
||||||
pubkeyPrefix8, _ := hex.DecodeString(evt.PubKey[0 : 8*2])
|
|
||||||
k := make([]byte, 1+8+4+4)
|
k := make([]byte, 1+8+4+4)
|
||||||
k[0] = indexPubkeyPrefix
|
k[0] = indexPubkeyPrefix
|
||||||
copy(k[1:], pubkeyPrefix8)
|
copy(k[1:], evt.PubKey[0:8])
|
||||||
binary.BigEndian.PutUint32(k[1+8:], uint32(evt.CreatedAt))
|
binary.BigEndian.PutUint32(k[1+8:], uint32(evt.CreatedAt))
|
||||||
copy(k[1+8+4:], idx)
|
copy(k[1+8+4:], idx)
|
||||||
if !yield(k) {
|
if !yield(k) {
|
||||||
@@ -81,10 +79,9 @@ func (b *BadgerBackend) getIndexKeysForEvent(evt *nostr.Event, idx []byte) iter.
|
|||||||
|
|
||||||
{
|
{
|
||||||
// ~ by pubkey+kind+date
|
// ~ by pubkey+kind+date
|
||||||
pubkeyPrefix8, _ := hex.DecodeString(evt.PubKey[0 : 8*2])
|
|
||||||
k := make([]byte, 1+8+2+4+4)
|
k := make([]byte, 1+8+2+4+4)
|
||||||
k[0] = indexPubkeyKindPrefix
|
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.PutUint16(k[1+8:], uint16(evt.Kind))
|
||||||
binary.BigEndian.PutUint32(k[1+8+2:], uint32(evt.CreatedAt))
|
binary.BigEndian.PutUint32(k[1+8+2:], uint32(evt.CreatedAt))
|
||||||
copy(k[1+8+2+4:], idx)
|
copy(k[1+8+2+4:], idx)
|
||||||
@@ -152,7 +149,7 @@ func getAddrTagElements(tagValue string) (kind uint16, pkb []byte, d string) {
|
|||||||
return 0, nil, ""
|
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 {
|
for f, v := range ef.Tags {
|
||||||
if v != nil && !event.Tags.ContainsAny(f, v) {
|
if v != nil && !event.Tags.ContainsAny(f, v) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v4"
|
|
||||||
"fiatjaf.com/nostr/eventstore"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/eventstore"
|
||||||
|
"github.com/dgraph-io/badger/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -32,9 +32,9 @@ type BadgerBackend struct {
|
|||||||
BadgerOptionsModifier func(badger.Options) badger.Options
|
BadgerOptionsModifier func(badger.Options) badger.Options
|
||||||
|
|
||||||
// Experimental
|
// Experimental
|
||||||
SkipIndexingTag func(event *nostr.Event, tagName string, tagValue string) bool
|
SkipIndexingTag func(event nostr.Event, tagName string, tagValue string) bool
|
||||||
// Experimental
|
// Experimental
|
||||||
IndexLongerTag func(event *nostr.Event, tagName string, tagValue string) bool
|
IndexLongerTag func(event nostr.Event, tagName string, tagValue string) bool
|
||||||
|
|
||||||
*badger.DB
|
*badger.DB
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package badger
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v4"
|
"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)
|
// 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 < 1 {
|
||||||
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 {
|
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// b.bumpVersion(txn, 1)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,54 @@
|
|||||||
package badger
|
package badger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"iter"
|
||||||
"log"
|
"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"
|
||||||
|
"fiatjaf.com/nostr/eventstore/codec/betterbinary"
|
||||||
|
"fiatjaf.com/nostr/eventstore/internal"
|
||||||
|
"github.com/dgraph-io/badger/v4"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
var batchFilled = errors.New("batch-filled")
|
var batchFilled = errors.New("batch-filled")
|
||||||
|
|
||||||
func (b *BadgerBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
func (b *BadgerBackend) QueryEvents(filter nostr.Filter) iter.Seq[nostr.Event] {
|
||||||
ch := make(chan *nostr.Event)
|
return func(yield func(nostr.Event) bool) {
|
||||||
|
|
||||||
if filter.Search != "" {
|
if filter.Search != "" {
|
||||||
close(ch)
|
return
|
||||||
return ch, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// max number of events we'll return
|
// max number of events we'll return
|
||||||
maxLimit := b.MaxLimit
|
limit := b.MaxLimit / 4
|
||||||
var limit int
|
if filter.Limit > 0 && filter.Limit <= b.MaxLimit {
|
||||||
if eventstore.IsNegentropySession(ctx) {
|
|
||||||
maxLimit = b.MaxLimitNegentropy
|
|
||||||
limit = maxLimit
|
|
||||||
} else {
|
|
||||||
limit = maxLimit / 4
|
|
||||||
}
|
|
||||||
if filter.Limit > 0 && filter.Limit <= maxLimit {
|
|
||||||
limit = filter.Limit
|
limit = filter.Limit
|
||||||
}
|
}
|
||||||
if tlimit := nostr.GetTheoreticalLimit(filter); tlimit == 0 {
|
if tlimit := nostr.GetTheoreticalLimit(filter); tlimit == 0 {
|
||||||
close(ch)
|
return
|
||||||
return ch, nil
|
|
||||||
} else if tlimit > 0 {
|
} else if tlimit > 0 {
|
||||||
limit = tlimit
|
limit = tlimit
|
||||||
}
|
}
|
||||||
|
|
||||||
// fmt.Println("limit", limit)
|
// fmt.Println("limit", limit)
|
||||||
|
b.View(func(txn *badger.Txn) error {
|
||||||
go b.View(func(txn *badger.Txn) error {
|
|
||||||
defer close(ch)
|
|
||||||
|
|
||||||
results, err := b.query(txn, filter, limit)
|
results, err := b.query(txn, filter, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, evt := range results {
|
for _, evt := range results {
|
||||||
ch <- evt.Event
|
if !yield(evt.Event) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
}
|
||||||
return ch, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BadgerBackend) query(txn *badger.Txn, filter nostr.Filter, limit int) ([]internal.IterEvent, error) {
|
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
|
// we will continue to pull from it as soon as some other iterator takes the position
|
||||||
oldest := internal.IterEvent{Q: -1}
|
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)
|
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
|
// while merging results in the second phase we will alternate between these two lists
|
||||||
// to avoid having to create new lists all the time
|
// to avoid having to create new lists all the time
|
||||||
var secondPhaseResultsA []internal.IterEvent
|
var sndPhaseResultsA []internal.IterEvent
|
||||||
var secondPhaseResultsB []internal.IterEvent
|
var sndPhaseResultsB []internal.IterEvent
|
||||||
var secondPhaseResultsToggle bool // this is just a dummy thing we use to keep track of the alternating
|
var sndPhaseResultsToggle bool // this is just a dummy thing we use to keep track of the alternating
|
||||||
var secondPhaseHasResultsPending bool
|
var sndPhaseHasResultsPending bool
|
||||||
|
|
||||||
remainingUnexhausted := len(queries) // when all queries are exhausted we can finally end this thing
|
remainingUnexhausted := len(queries) // when all queries are exhausted we can finally end this thing
|
||||||
batchSizePerQuery := internal.BatchSizePerNumberOfQueries(limit, remainingUnexhausted)
|
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
|
// check it against pubkeys without decoding the entire thing
|
||||||
if extraFilter != nil && extraFilter.Authors != nil &&
|
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)")
|
// fmt.Println(" skipped (authors)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// check it against kinds without decoding the entire thing
|
// check it against kinds without decoding the entire thing
|
||||||
if extraFilter != nil && extraFilter.Kinds != nil &&
|
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)")
|
// fmt.Println(" skipped (kinds)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
event := &nostr.Event{}
|
event := nostr.Event{}
|
||||||
if err := bin.Unmarshal(val, event); err != nil {
|
if err := betterbinary.Unmarshal(val, &event); err != nil {
|
||||||
log.Printf("badger: value read error (id %x): %s\n", val[0:32], err)
|
log.Printf("badger: value read error (id %x): %s\n", val[0:32], err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if this matches the other filters that were not part of the index
|
// 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)
|
// fmt.Println(" skipped (filter)", extraFilter, event)
|
||||||
return nil
|
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}
|
evt := internal.IterEvent{Event: event, Q: q}
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
if secondPhase {
|
if sndPhase {
|
||||||
// do the process described below at HIWAWVRTP.
|
// do the process described below at HIWAWVRTP.
|
||||||
// if we've reached here this means we've already passed the `since` check.
|
// 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.
|
// now we have to eliminate the event currently at the `since` threshold.
|
||||||
nextThreshold := firstPhaseResults[len(firstPhaseResults)-2]
|
nextThreshold := firstPhaseResults[len(firstPhaseResults)-2]
|
||||||
if oldest.Event == nil {
|
if oldest.Event.ID == nostr.ZeroID {
|
||||||
// fmt.Println(" b1")
|
// fmt.Println(" b1")
|
||||||
// BRANCH WHEN WE DON'T HAVE THE OLDEST EVENT (BWWDHTOE)
|
// BRANCH WHEN WE DON'T HAVE THE OLDEST EVENT (BWWDHTOE)
|
||||||
// when we don't have the oldest set, we will keep the results
|
// 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.
|
// and not change the cutting point -- it's bad, but hopefully not that bad.
|
||||||
results[q] = append(results[q], evt)
|
results[q] = append(results[q], evt)
|
||||||
secondPhaseHasResultsPending = true
|
sndPhaseHasResultsPending = true
|
||||||
} else if nextThreshold.CreatedAt > oldest.CreatedAt {
|
} else if nextThreshold.CreatedAt > oldest.CreatedAt {
|
||||||
// fmt.Println(" b2", nextThreshold.CreatedAt, ">", oldest.CreatedAt)
|
// fmt.Println(" b2", nextThreshold.CreatedAt, ">", oldest.CreatedAt)
|
||||||
// one of the events we have stored is the actual next threshold
|
// 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
|
// finally
|
||||||
// add this to the results to be merged later
|
// add this to the results to be merged later
|
||||||
results[q] = append(results[q], evt)
|
results[q] = append(results[q], evt)
|
||||||
secondPhaseHasResultsPending = true
|
sndPhaseHasResultsPending = true
|
||||||
} else if nextThreshold.CreatedAt < evt.CreatedAt {
|
} else if nextThreshold.CreatedAt < evt.CreatedAt {
|
||||||
// the next last event in the firstPhaseResults is the next threshold
|
// the next last event in the firstPhaseResults is the next threshold
|
||||||
// fmt.Println(" b3", nextThreshold.CreatedAt, "<", oldest.CreatedAt)
|
// 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)
|
// fmt.Println(" new since", since)
|
||||||
// add this to the results to be merged later
|
// add this to the results to be merged later
|
||||||
results[q] = append(results[q], evt)
|
results[q] = append(results[q], evt)
|
||||||
secondPhaseHasResultsPending = true
|
sndPhaseHasResultsPending = true
|
||||||
// update the oldest event
|
// update the oldest event
|
||||||
if evt.CreatedAt < oldest.CreatedAt {
|
if evt.CreatedAt < oldest.CreatedAt {
|
||||||
oldest = evt
|
oldest = evt
|
||||||
@@ -265,7 +251,7 @@ func (b *BadgerBackend) query(txn *badger.Txn, filter nostr.Filter, limit int) (
|
|||||||
firstPhaseTotalPulled++
|
firstPhaseTotalPulled++
|
||||||
|
|
||||||
// update the oldest event
|
// update the oldest event
|
||||||
if oldest.Event == nil || evt.CreatedAt < oldest.CreatedAt {
|
if oldest.Event.ID == nostr.ZeroID || evt.CreatedAt < oldest.CreatedAt {
|
||||||
oldest = evt
|
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
|
// 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)
|
// 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!")
|
// fmt.Println("second phase aggregation!")
|
||||||
// when we are in the second phase we will aggressively aggregate results on every iteration
|
// when we are in the second phase we will aggressively aggregate results on every iteration
|
||||||
//
|
//
|
||||||
secondBatch = secondBatch[:0]
|
secondBatch = secondBatch[:0]
|
||||||
for s := 0; s < len(secondPhaseParticipants); s++ {
|
for s := 0; s < len(sndPhaseParticipants); s++ {
|
||||||
q := secondPhaseParticipants[s]
|
q := sndPhaseParticipants[s]
|
||||||
|
|
||||||
if len(results[q]) > 0 {
|
if len(results[q]) > 0 {
|
||||||
secondBatch = append(secondBatch, results[q])
|
secondBatch = append(secondBatch, results[q])
|
||||||
}
|
}
|
||||||
|
|
||||||
if exhausted[q] {
|
if exhausted[q] {
|
||||||
secondPhaseParticipants = internal.SwapDelete(secondPhaseParticipants, s)
|
sndPhaseParticipants = internal.SwapDelete(sndPhaseParticipants, s)
|
||||||
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
|
// every time we get here we will alternate between these A and B lists
|
||||||
// combining everything we have into a new partial results list.
|
// combining everything we have into a new partial results list.
|
||||||
// after we've done that we can again set the oldest.
|
// after we've done that we can again set the oldest.
|
||||||
// fmt.Println(" xxx", secondPhaseResultsToggle)
|
// fmt.Println(" xxx", sndPhaseResultsToggle)
|
||||||
if secondPhaseResultsToggle {
|
if sndPhaseResultsToggle {
|
||||||
secondBatch = append(secondBatch, secondPhaseResultsB)
|
secondBatch = append(secondBatch, sndPhaseResultsB)
|
||||||
secondPhaseResultsA = internal.MergeSortMultiple(secondBatch, limit, secondPhaseResultsA)
|
sndPhaseResultsA = internal.MergeSortMultiple(secondBatch, limit, sndPhaseResultsA)
|
||||||
oldest = secondPhaseResultsA[len(secondPhaseResultsA)-1]
|
oldest = sndPhaseResultsA[len(sndPhaseResultsA)-1]
|
||||||
// fmt.Println(" new aggregated a", len(secondPhaseResultsB))
|
// fmt.Println(" new aggregated a", len(sndPhaseResultsB))
|
||||||
} else {
|
} else {
|
||||||
secondBatch = append(secondBatch, secondPhaseResultsA)
|
secondBatch = append(secondBatch, sndPhaseResultsA)
|
||||||
secondPhaseResultsB = internal.MergeSortMultiple(secondBatch, limit, secondPhaseResultsB)
|
sndPhaseResultsB = internal.MergeSortMultiple(secondBatch, limit, sndPhaseResultsB)
|
||||||
oldest = secondPhaseResultsB[len(secondPhaseResultsB)-1]
|
oldest = sndPhaseResultsB[len(sndPhaseResultsB)-1]
|
||||||
// fmt.Println(" new aggregated b", len(secondPhaseResultsB))
|
// fmt.Println(" new aggregated b", len(sndPhaseResultsB))
|
||||||
}
|
}
|
||||||
secondPhaseResultsToggle = !secondPhaseResultsToggle
|
sndPhaseResultsToggle = !sndPhaseResultsToggle
|
||||||
|
|
||||||
since = uint32(oldest.CreatedAt)
|
since = uint32(oldest.CreatedAt)
|
||||||
// fmt.Println(" new since", since)
|
// fmt.Println(" new since", since)
|
||||||
|
|
||||||
// reset the `results` list so we can keep using it
|
// reset the `results` list so we can keep using it
|
||||||
results = results[:len(queries)]
|
results = results[:len(queries)]
|
||||||
for _, q := range secondPhaseParticipants {
|
for _, q := range sndPhaseParticipants {
|
||||||
results[q] = results[q][:0]
|
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)
|
// fmt.Println("have enough!", firstPhaseTotalPulled, "/", limit, "remaining", remainingUnexhausted)
|
||||||
|
|
||||||
// we will exclude this oldest number as it is not relevant anymore
|
// 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]
|
results[q] = results[q][:0]
|
||||||
|
|
||||||
// build this index of indexes with everybody who remains
|
// 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
|
// we create these two lists and alternate between them so we don't have to create a
|
||||||
// a new one every time
|
// a new one every time
|
||||||
secondPhaseResultsA = make([]internal.IterEvent, 0, limit*2)
|
sndPhaseResultsA = make([]internal.IterEvent, 0, limit*2)
|
||||||
secondPhaseResultsB = make([]internal.IterEvent, 0, limit*2)
|
sndPhaseResultsB = make([]internal.IterEvent, 0, limit*2)
|
||||||
|
|
||||||
// from now on we won't run this block anymore
|
// from now on we won't run this block anymore
|
||||||
secondPhase = true
|
sndPhase = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// fmt.Println("remaining", remainingUnexhausted)
|
// 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
|
var combinedResults []internal.IterEvent
|
||||||
|
|
||||||
if secondPhase {
|
if sndPhase {
|
||||||
// fmt.Println("ending second phase")
|
// 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
|
// the other will be empty
|
||||||
var secondPhaseResults []internal.IterEvent
|
var sndPhaseResults []internal.IterEvent
|
||||||
// fmt.Println("xxx", secondPhaseResultsToggle, len(secondPhaseResultsA), len(secondPhaseResultsB))
|
// fmt.Println("xxx", sndPhaseResultsToggle, len(sndPhaseResultsA), len(sndPhaseResultsB))
|
||||||
if secondPhaseResultsToggle {
|
if sndPhaseResultsToggle {
|
||||||
secondPhaseResults = secondPhaseResultsB
|
sndPhaseResults = sndPhaseResultsB
|
||||||
combinedResults = secondPhaseResultsA[0:limit] // reuse this
|
combinedResults = sndPhaseResultsA[0:limit] // reuse this
|
||||||
// fmt.Println(" using b", len(secondPhaseResultsA))
|
// fmt.Println(" using b", len(sndPhaseResultsA))
|
||||||
} else {
|
} else {
|
||||||
secondPhaseResults = secondPhaseResultsA
|
sndPhaseResults = sndPhaseResultsA
|
||||||
combinedResults = secondPhaseResultsB[0:limit] // reuse this
|
combinedResults = sndPhaseResultsB[0:limit] // reuse this
|
||||||
// fmt.Println(" using a", len(secondPhaseResultsA))
|
// fmt.Println(" using a", len(sndPhaseResultsA))
|
||||||
}
|
}
|
||||||
|
|
||||||
all := [][]internal.IterEvent{firstPhaseResults, secondPhaseResults}
|
all := [][]internal.IterEvent{firstPhaseResults, sndPhaseResults}
|
||||||
combinedResults = internal.MergeSortMultiple(all, limit, combinedResults)
|
combinedResults = internal.MergeSortMultiple(all, limit, combinedResults)
|
||||||
// fmt.Println("final combinedResults", len(combinedResults), cap(combinedResults), limit)
|
// fmt.Println("final combinedResults", len(combinedResults), cap(combinedResults), limit)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
package badger
|
package badger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v4"
|
|
||||||
"fiatjaf.com/nostr/eventstore/internal"
|
|
||||||
"fiatjaf.com/nostr"
|
"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
|
// sanity checking
|
||||||
if evt.CreatedAt > math.MaxUint32 || evt.Kind > math.MaxUint16 {
|
if evt.CreatedAt > math.MaxUint32 || evt.Kind > math.MaxUint16 {
|
||||||
return fmt.Errorf("event with values out of expected boundaries")
|
return fmt.Errorf("event with values out of expected boundaries")
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.Update(func(txn *badger.Txn) error {
|
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) {
|
if nostr.IsAddressableKind(evt.Kind) {
|
||||||
// when addressable, add the "d" tag to the filter
|
// when addressable, add the "d" tag to the filter
|
||||||
filter.Tags = nostr.TagMap{"d": []string{evt.Tags.GetD()}}
|
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
|
shouldStore := true
|
||||||
for _, previous := range results {
|
for _, previous := range results {
|
||||||
if internal.IsOlder(previous.Event, evt) {
|
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)
|
return fmt.Errorf("failed to delete event %s for replacing: %w", previous.Event.ID, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
package badger
|
package badger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v4"
|
|
||||||
"fiatjaf.com/nostr/eventstore"
|
|
||||||
bin "fiatjaf.com/nostr/eventstore/internal/binary"
|
|
||||||
"fiatjaf.com/nostr"
|
"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
|
// sanity checking
|
||||||
if evt.CreatedAt > math.MaxUint32 || evt.Kind > math.MaxUint16 {
|
if evt.CreatedAt > math.MaxUint32 || evt.Kind > math.MaxUint16 {
|
||||||
return fmt.Errorf("event with values out of expected boundaries")
|
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 {
|
return b.Update(func(txn *badger.Txn) error {
|
||||||
// query event by id to ensure we don't save duplicates
|
// query event by id to ensure we don't save duplicates
|
||||||
id, _ := hex.DecodeString(evt.ID)
|
|
||||||
prefix := make([]byte, 1+8)
|
prefix := make([]byte, 1+8)
|
||||||
prefix[0] = indexIdPrefix
|
prefix[0] = indexIdPrefix
|
||||||
copy(prefix[1:], id)
|
copy(prefix[1:], evt.ID[0:8])
|
||||||
it := txn.NewIterator(badger.IteratorOptions{})
|
it := txn.NewIterator(badger.IteratorOptions{})
|
||||||
defer it.Close()
|
defer it.Close()
|
||||||
it.Seek(prefix)
|
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
|
// encode to binary
|
||||||
bin, err := bin.Marshal(evt)
|
buf := make([]byte, betterbinary.Measure(evt))
|
||||||
if err != nil {
|
if err := betterbinary.Marshal(evt, buf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
idx := b.Serial()
|
idx := b.Serial()
|
||||||
// raw event store
|
// raw event store
|
||||||
if err := txn.Set(idx, bin); err != nil {
|
if err := txn.Set(idx, buf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package bluge
|
package bluge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *BlugeBackend) DeleteEvent(ctx context.Context, evt *nostr.Event) error {
|
func (b *BlugeBackend) DeleteEvent(id nostr.ID) error {
|
||||||
return b.writer.Delete(eventIdentifier(evt.ID))
|
return b.writer.Delete(eventIdentifier(id))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package bluge
|
package bluge
|
||||||
|
|
||||||
import "encoding/hex"
|
import (
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
contentField = "c"
|
contentField = "c"
|
||||||
@@ -9,7 +11,7 @@ const (
|
|||||||
pubkeyField = "p"
|
pubkeyField = "p"
|
||||||
)
|
)
|
||||||
|
|
||||||
type eventIdentifier string
|
type eventIdentifier nostr.ID
|
||||||
|
|
||||||
const idField = "i"
|
const idField = "i"
|
||||||
|
|
||||||
@@ -18,6 +20,5 @@ func (id eventIdentifier) Field() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (id eventIdentifier) Term() []byte {
|
func (id eventIdentifier) Term() []byte {
|
||||||
v, _ := hex.DecodeString(string(id))
|
return id[:]
|
||||||
return v
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,27 +2,23 @@ package bluge
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"iter"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
"github.com/blugelabs/bluge"
|
"github.com/blugelabs/bluge"
|
||||||
"github.com/blugelabs/bluge/search"
|
"github.com/blugelabs/bluge/search"
|
||||||
"fiatjaf.com/nostr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *BlugeBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
func (b *BlugeBackend) QueryEvents(filter nostr.Filter) iter.Seq[nostr.Event] {
|
||||||
ch := make(chan *nostr.Event)
|
return func(yield func(nostr.Event) bool) {
|
||||||
|
|
||||||
if len(filter.Search) < 2 {
|
if len(filter.Search) < 2 {
|
||||||
close(ch)
|
return
|
||||||
return ch, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reader, err := b.writer.Reader()
|
reader, err := b.writer.Reader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
close(ch)
|
return
|
||||||
return nil, fmt.Errorf("unable to open reader: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searchQ := bluge.NewMatchQuery(filter.Search)
|
searchQ := bluge.NewMatchQuery(filter.Search)
|
||||||
@@ -35,7 +31,7 @@ func (b *BlugeBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (ch
|
|||||||
eitherKind := bluge.NewBooleanQuery()
|
eitherKind := bluge.NewBooleanQuery()
|
||||||
eitherKind.SetMinShould(1)
|
eitherKind.SetMinShould(1)
|
||||||
for _, kind := range filter.Kinds {
|
for _, kind := range filter.Kinds {
|
||||||
kindQ := bluge.NewTermQuery(strconv.Itoa(kind))
|
kindQ := bluge.NewTermQuery(strconv.Itoa(int(kind)))
|
||||||
kindQ.SetField(kindField)
|
kindQ.SetField(kindField)
|
||||||
eitherKind.AddShould(kindQ)
|
eitherKind.AddShould(kindQ)
|
||||||
}
|
}
|
||||||
@@ -50,7 +46,7 @@ func (b *BlugeBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (ch
|
|||||||
if len(pubkey) != 64 {
|
if len(pubkey) != 64 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pubkeyQ := bluge.NewTermQuery(pubkey[56:])
|
pubkeyQ := bluge.NewTermQuery(pubkey.Hex()[56:])
|
||||||
pubkeyQ.SetField(pubkeyField)
|
pubkeyQ.SetField(pubkeyField)
|
||||||
eitherPubkey.AddShould(pubkeyQ)
|
eitherPubkey.AddShould(pubkeyQ)
|
||||||
}
|
}
|
||||||
@@ -85,25 +81,17 @@ func (b *BlugeBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (ch
|
|||||||
|
|
||||||
dmi, err := reader.Search(context.Background(), req)
|
dmi, err := reader.Search(context.Background(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
close(ch)
|
|
||||||
reader.Close()
|
reader.Close()
|
||||||
return ch, fmt.Errorf("error executing search: %w", err)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
defer close(ch)
|
|
||||||
|
|
||||||
var next *search.DocumentMatch
|
var next *search.DocumentMatch
|
||||||
for next, err = dmi.Next(); next != nil; next, err = dmi.Next() {
|
for next, err = dmi.Next(); next != nil; next, err = dmi.Next() {
|
||||||
next.VisitStoredFields(func(field string, value []byte) bool {
|
next.VisitStoredFields(func(field string, value []byte) bool {
|
||||||
id := hex.EncodeToString(value)
|
for evt := range b.RawEventStore.QueryEvents(nostr.Filter{IDs: []nostr.ID{nostr.ID(value)}}) {
|
||||||
rawch, err := b.RawEventStore.QueryEvents(ctx, nostr.Filter{IDs: []string{id}})
|
yield(evt)
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for evt := range rawch {
|
|
||||||
ch <- evt
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -111,7 +99,5 @@ func (b *BlugeBackend) QueryEvents(ctx context.Context, filter nostr.Filter) (ch
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
|
||||||
return ch, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,29 +4,24 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/eventstore"
|
"fiatjaf.com/nostr/eventstore"
|
||||||
"fiatjaf.com/nostr/eventstore/internal"
|
"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()
|
b.Lock()
|
||||||
defer b.Unlock()
|
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) {
|
if nostr.IsAddressableKind(evt.Kind) {
|
||||||
filter.Tags = nostr.TagMap{"d": []string{evt.Tags.GetD()}}
|
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
|
shouldStore := true
|
||||||
for previous := range ch {
|
for previous := range b.QueryEvents(filter) {
|
||||||
if internal.IsOlder(previous, evt) {
|
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)
|
return fmt.Errorf("failed to delete event for replacing: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
package bluge
|
package bluge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/blugelabs/bluge"
|
|
||||||
"fiatjaf.com/nostr"
|
"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)
|
id := eventIdentifier(evt.ID)
|
||||||
doc := &bluge.Document{
|
doc := &bluge.Document{
|
||||||
bluge.NewKeywordFieldBytes(id.Field(), id.Term()).Sortable().StoreValue(),
|
bluge.NewKeywordFieldBytes(id.Field(), id.Term()).Sortable().StoreValue(),
|
||||||
}
|
}
|
||||||
|
|
||||||
doc.AddField(bluge.NewTextField(contentField, evt.Content))
|
doc.AddField(bluge.NewTextField(contentField, evt.Content))
|
||||||
doc.AddField(bluge.NewTextField(kindField, strconv.Itoa(evt.Kind)))
|
doc.AddField(bluge.NewTextField(kindField, strconv.Itoa(int(evt.Kind))))
|
||||||
doc.AddField(bluge.NewTextField(pubkeyField, evt.PubKey[56:]))
|
doc.AddField(bluge.NewTextField(pubkeyField, evt.PubKey.Hex()[56:]))
|
||||||
doc.AddField(bluge.NewNumericField(createdAtField, float64(evt.CreatedAt)))
|
doc.AddField(bluge.NewNumericField(createdAtField, float64(evt.CreatedAt)))
|
||||||
|
|
||||||
if err := b.writer.Update(doc.ID(), doc); err != nil {
|
if err := b.writer.Update(doc.ID(), doc); err != nil {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package betterbinary
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
@@ -50,9 +49,9 @@ func Marshal(evt nostr.Event, buf []byte) error {
|
|||||||
}
|
}
|
||||||
binary.LittleEndian.PutUint32(buf[3:7], uint32(evt.CreatedAt))
|
binary.LittleEndian.PutUint32(buf[3:7], uint32(evt.CreatedAt))
|
||||||
|
|
||||||
hex.Decode(buf[7:39], []byte(evt.ID))
|
copy(buf[7:39], evt.ID[:])
|
||||||
hex.Decode(buf[39:71], []byte(evt.PubKey))
|
copy(buf[39:71], evt.PubKey[:])
|
||||||
hex.Decode(buf[71:135], []byte(evt.Sig))
|
copy(buf[71:135], evt.Sig[:])
|
||||||
|
|
||||||
tagBase := 135
|
tagBase := 135
|
||||||
// buf[135:137] (tagsSectionLength) will be set later when we know the absolute size of the tags section
|
// 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.CreatedAt = nostr.Timestamp(binary.LittleEndian.Uint32(data[3:7]))
|
||||||
evt.ID = hex.EncodeToString(data[7:39])
|
evt.ID = nostr.ID(data[7:39])
|
||||||
evt.PubKey = hex.EncodeToString(data[39:71])
|
evt.PubKey = nostr.PubKey(data[39:71])
|
||||||
evt.Sig = hex.EncodeToString(data[71:135])
|
evt.Sig = [64]byte(data[71:135])
|
||||||
|
|
||||||
const tagbase = 135
|
const tagbase = 135
|
||||||
tagsSectionLength := binary.LittleEndian.Uint16(data[tagbase:])
|
tagsSectionLength := binary.LittleEndian.Uint16(data[tagbase:])
|
||||||
@@ -1 +0,0 @@
|
|||||||
decode-binary
|
|
||||||
@@ -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())
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,7 @@ import (
|
|||||||
"fiatjaf.com/nostr/eventstore"
|
"fiatjaf.com/nostr/eventstore"
|
||||||
"fiatjaf.com/nostr/eventstore/badger"
|
"fiatjaf.com/nostr/eventstore/badger"
|
||||||
"fiatjaf.com/nostr/eventstore/bluge"
|
"fiatjaf.com/nostr/eventstore/bluge"
|
||||||
"fiatjaf.com/nostr/eventstore/edgedb"
|
|
||||||
"fiatjaf.com/nostr/eventstore/lmdb"
|
"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"
|
"fiatjaf.com/nostr/eventstore/strfry"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,11 +12,6 @@ import (
|
|||||||
var (
|
var (
|
||||||
_ eventstore.Store = (*badger.BadgerBackend)(nil)
|
_ eventstore.Store = (*badger.BadgerBackend)(nil)
|
||||||
_ eventstore.Store = (*lmdb.LMDBBackend)(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 = (*strfry.StrfryBackend)(nil)
|
||||||
_ eventstore.Store = (*bluge.BlugeBackend)(nil)
|
_ eventstore.Store = (*bluge.BlugeBackend)(nil)
|
||||||
_ eventstore.Store = (*mysql.MySQLBackend)(nil)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"cmp"
|
"cmp"
|
||||||
"math"
|
"math"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
|
|
||||||
mergesortedslices "fiatjaf.com/lib/merge-sorted-slices"
|
mergesortedslices "fiatjaf.com/lib/merge-sorted-slices"
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsOlder(previous, next *nostr.Event) bool {
|
func IsOlder(previous, next nostr.Event) bool {
|
||||||
return previous.CreatedAt < next.CreatedAt ||
|
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) {
|
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 {
|
type IterEvent struct {
|
||||||
*nostr.Event
|
nostr.Event
|
||||||
Q int
|
Q int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,18 +166,18 @@ func SwapDelete[A any](arr []A, i int) []A {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func compareIterEvent(a, b IterEvent) int {
|
func compareIterEvent(a, b IterEvent) int {
|
||||||
if a.Event == nil {
|
if a.Event.ID == nostr.ZeroID {
|
||||||
if b.Event == nil {
|
if b.Event.ID == nostr.ZeroID {
|
||||||
return 0
|
return 0
|
||||||
} else {
|
} else {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
} else if b.Event == nil {
|
} else if b.Event.ID == nostr.ZeroID {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.CreatedAt == b.CreatedAt {
|
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)
|
return cmp.Compare(a.CreatedAt, b.CreatedAt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
uint(256)
|
|
||||||
uint(31)
|
|
||||||
uint(260)
|
|
||||||
uint(2)
|
|
||||||
uint(69)
|
|
||||||
uint(385)
|
|
||||||
uint(1)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
uint(267)
|
|
||||||
uint(50)
|
|
||||||
uint(355)
|
|
||||||
uint(2)
|
|
||||||
uint(69)
|
|
||||||
uint(213)
|
|
||||||
uint(1)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
uint(280)
|
|
||||||
uint(0)
|
|
||||||
uint(13)
|
|
||||||
uint(2)
|
|
||||||
uint(2)
|
|
||||||
uint(0)
|
|
||||||
uint(0)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
uint(259)
|
|
||||||
uint(126)
|
|
||||||
uint(5)
|
|
||||||
uint(23)
|
|
||||||
uint(0)
|
|
||||||
uint(0)
|
|
||||||
uint(92)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
uint(201)
|
|
||||||
uint(50)
|
|
||||||
uint(13)
|
|
||||||
uint(97)
|
|
||||||
uint(0)
|
|
||||||
uint(0)
|
|
||||||
uint(77)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
uint(164)
|
|
||||||
uint(50)
|
|
||||||
uint(13)
|
|
||||||
uint(1)
|
|
||||||
uint(2)
|
|
||||||
uint(13)
|
|
||||||
uint(0)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
uint(200)
|
|
||||||
uint(50)
|
|
||||||
uint(13)
|
|
||||||
uint(8)
|
|
||||||
uint(2)
|
|
||||||
uint(0)
|
|
||||||
uint(1)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
uint(200)
|
|
||||||
uint(117)
|
|
||||||
uint(13)
|
|
||||||
uint(2)
|
|
||||||
uint(2)
|
|
||||||
uint(0)
|
|
||||||
uint(1)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
uint(200)
|
|
||||||
uint(50)
|
|
||||||
uint(13)
|
|
||||||
uint(2)
|
|
||||||
uint(2)
|
|
||||||
uint(0)
|
|
||||||
uint(0)
|
|
||||||
@@ -2,19 +2,18 @@ package lmdb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
|
||||||
"github.com/PowerDNS/lmdb-go/lmdb"
|
|
||||||
bin "fiatjaf.com/nostr/eventstore/internal/binary"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/eventstore/codec/betterbinary"
|
||||||
"fiatjaf.com/nostr/nip45"
|
"fiatjaf.com/nostr/nip45"
|
||||||
"fiatjaf.com/nostr/nip45/hyperloglog"
|
"fiatjaf.com/nostr/nip45/hyperloglog"
|
||||||
|
"github.com/PowerDNS/lmdb-go/lmdb"
|
||||||
"golang.org/x/exp/slices"
|
"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
|
var count int64 = 0
|
||||||
|
|
||||||
queries, extraAuthors, extraKinds, extraTagKey, extraTagValues, since, err := b.prepareQueries(filter)
|
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{}
|
evt := &nostr.Event{}
|
||||||
if err := bin.Unmarshal(val, evt); err != nil {
|
if err := betterbinary.Unmarshal(val, evt); err != nil {
|
||||||
it.next()
|
it.next()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -94,8 +93,9 @@ func (b *LMDBBackend) CountEvents(ctx context.Context, filter nostr.Filter) (int
|
|||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountEventsHLL is like CountEvents, but it will build a hyperloglog value while iterating through results, following NIP-45
|
// CountEventsHLL is like CountEvents, but it will build a hyperloglog value while iterating through results,
|
||||||
func (b *LMDBBackend) CountEventsHLL(ctx context.Context, filter nostr.Filter, offset int) (int64, *hyperloglog.HyperLogLog, error) {
|
// following NIP-45
|
||||||
|
func (b *LMDBBackend) CountEventsHLL(filter nostr.Filter, offset int) (int64, *hyperloglog.HyperLogLog, error) {
|
||||||
if useCache, _ := b.EnableHLLCacheFor(filter.Kinds[0]); useCache {
|
if useCache, _ := b.EnableHLLCacheFor(filter.Kinds[0]); useCache {
|
||||||
return b.countEventsHLLCached(filter)
|
return b.countEventsHLLCached(filter)
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,7 @@ func (b *LMDBBackend) CountEventsHLL(ctx context.Context, filter nostr.Filter, o
|
|||||||
if extraKinds == nil && extraTagValues == nil {
|
if extraKinds == nil && extraTagValues == nil {
|
||||||
// nothing extra to check
|
// nothing extra to check
|
||||||
count++
|
count++
|
||||||
hll.AddBytes(val[32:64])
|
hll.AddBytes(nostr.PubKey(val[32:64]))
|
||||||
} else {
|
} else {
|
||||||
// check it against kinds without decoding the entire thing
|
// check it against kinds without decoding the entire thing
|
||||||
if !slices.Contains(extraKinds, [2]byte(val[132:134])) {
|
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{}
|
evt := &nostr.Event{}
|
||||||
if err := bin.Unmarshal(val, evt); err != nil {
|
if err := betterbinary.Unmarshal(val, evt); err != nil {
|
||||||
it.next()
|
it.next()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -211,7 +211,7 @@ func (b *LMDBBackend) countEventsHLLCached(filter nostr.Filter) (int64, *hyperlo
|
|||||||
return count, hll, err
|
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)
|
cacheKey := make([]byte, 2+8)
|
||||||
binary.BigEndian.PutUint16(cacheKey[0:2], uint16(evt.Kind))
|
binary.BigEndian.PutUint16(cacheKey[0:2], uint16(evt.Kind))
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,39 @@
|
|||||||
package lmdb
|
package lmdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/PowerDNS/lmdb-go/lmdb"
|
|
||||||
"fiatjaf.com/nostr"
|
"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.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 {
|
func (b *LMDBBackend) delete(txn *lmdb.Txn, id nostr.ID) error {
|
||||||
idPrefix8, _ := hex.DecodeString(evt.ID[0 : 8*2])
|
// check if we have this actually
|
||||||
idx, err := txn.Get(b.indexId, idPrefix8)
|
idx, err := txn.Get(b.indexId, id[0:8])
|
||||||
if lmdb.IsNotFound(err) {
|
if lmdb.IsNotFound(err) {
|
||||||
// we already do not have this
|
// we already do not have this
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err != 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
|
// calculate all index keys we have for this event and delete them
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/PowerDNS/lmdb-go/lmdb"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"github.com/PowerDNS/lmdb-go/lmdb"
|
||||||
"golang.org/x/exp/slices"
|
"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)
|
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) {
|
return func(yield func(key) bool) {
|
||||||
{
|
{
|
||||||
// ~ by id
|
// ~ by id
|
||||||
k := make([]byte, 8)
|
if !yield(key{dbi: b.indexId, key: evt.ID[0:8]}) {
|
||||||
hex.Decode(k[0:8], []byte(evt.ID[0:8*2]))
|
|
||||||
if !yield(key{dbi: b.indexId, key: k[0:8]}) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/PowerDNS/lmdb-go/lmdb"
|
|
||||||
"fiatjaf.com/nostr/eventstore"
|
"fiatjaf.com/nostr/eventstore"
|
||||||
|
"github.com/PowerDNS/lmdb-go/lmdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ eventstore.Store = (*LMDBBackend)(nil)
|
var _ eventstore.Store = (*LMDBBackend)(nil)
|
||||||
@@ -34,7 +34,7 @@ type LMDBBackend struct {
|
|||||||
indexPTagKind lmdb.DBI
|
indexPTagKind lmdb.DBI
|
||||||
|
|
||||||
hllCache lmdb.DBI
|
hllCache lmdb.DBI
|
||||||
EnableHLLCacheFor func(kind int) (useCache bool, skipSavingActualEvent bool)
|
EnableHLLCacheFor func(kind uint16) (useCache bool, skipSavingActualEvent bool)
|
||||||
|
|
||||||
lastId atomic.Uint32
|
lastId atomic.Uint32
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ package lmdb
|
|||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/PowerDNS/lmdb-go/lmdb"
|
"github.com/PowerDNS/lmdb-go/lmdb"
|
||||||
bin "fiatjaf.com/nostr/eventstore/internal/binary"
|
|
||||||
"fiatjaf.com/nostr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -28,113 +25,17 @@ func (b *LMDBBackend) runMigrations() error {
|
|||||||
version = binary.BigEndian.Uint16(v)
|
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)
|
// do the migrations in increasing steps (there is no rollback)
|
||||||
//
|
//
|
||||||
|
|
||||||
// this is when we reindex everything
|
// this is when we reindex everything
|
||||||
if version < 8 {
|
if version < 1 {
|
||||||
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
|
// bump version
|
||||||
if err := b.setVersion(txn, 8); err != nil {
|
// if err := b.setVersion(txn, 1); err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,59 +2,49 @@ package lmdb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"iter"
|
||||||
"log"
|
"log"
|
||||||
"slices"
|
"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"
|
||||||
|
"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) {
|
func (b *LMDBBackend) QueryEvents(filter nostr.Filter) iter.Seq[nostr.Event] {
|
||||||
ch := make(chan *nostr.Event)
|
return func(yield func(nostr.Event) bool) {
|
||||||
|
|
||||||
if filter.Search != "" {
|
if filter.Search != "" {
|
||||||
close(ch)
|
return
|
||||||
return ch, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// max number of events we'll return
|
// max number of events we'll return
|
||||||
maxLimit := b.MaxLimit
|
|
||||||
var limit int
|
var limit int
|
||||||
if eventstore.IsNegentropySession(ctx) {
|
limit = b.MaxLimit / 4
|
||||||
maxLimit = b.MaxLimitNegentropy
|
if filter.Limit > 0 && filter.Limit <= b.MaxLimit {
|
||||||
limit = maxLimit
|
|
||||||
} else {
|
|
||||||
limit = maxLimit / 4
|
|
||||||
}
|
|
||||||
if filter.Limit > 0 && filter.Limit <= maxLimit {
|
|
||||||
limit = filter.Limit
|
limit = filter.Limit
|
||||||
}
|
}
|
||||||
if tlimit := nostr.GetTheoreticalLimit(filter); tlimit == 0 {
|
if tlimit := nostr.GetTheoreticalLimit(filter); tlimit == 0 {
|
||||||
close(ch)
|
return
|
||||||
return ch, nil
|
|
||||||
} else if tlimit > 0 {
|
} else if tlimit > 0 {
|
||||||
limit = tlimit
|
limit = tlimit
|
||||||
}
|
}
|
||||||
|
|
||||||
go b.lmdbEnv.View(func(txn *lmdb.Txn) error {
|
b.lmdbEnv.View(func(txn *lmdb.Txn) error {
|
||||||
txn.RawRead = true
|
txn.RawRead = true
|
||||||
defer close(ch)
|
|
||||||
results, err := b.query(txn, filter, limit)
|
results, err := b.query(txn, filter, limit)
|
||||||
|
|
||||||
for _, ie := range results {
|
for _, ie := range results {
|
||||||
ch <- ie.Event
|
if !yield(ie.Event) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
}
|
||||||
return ch, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LMDBBackend) query(txn *lmdb.Txn, filter nostr.Filter, limit int) ([]internal.IterEvent, error) {
|
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
|
// we will continue to pull from it as soon as some other iterator takes the position
|
||||||
oldest := internal.IterEvent{Q: -1}
|
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)
|
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
|
// while merging results in the second phase we will alternate between these two lists
|
||||||
// to avoid having to create new lists all the time
|
// to avoid having to create new lists all the time
|
||||||
var secondPhaseResultsA []internal.IterEvent
|
var sndPhaseResultsA []internal.IterEvent
|
||||||
var secondPhaseResultsB []internal.IterEvent
|
var sndPhaseResultsB []internal.IterEvent
|
||||||
var secondPhaseResultsToggle bool // this is just a dummy thing we use to keep track of the alternating
|
var sndPhaseResultsToggle bool // this is just a dummy thing we use to keep track of the alternating
|
||||||
var secondPhaseHasResultsPending bool
|
var sndPhaseHasResultsPending bool
|
||||||
|
|
||||||
remainingUnexhausted := len(queries) // when all queries are exhausted we can finally end this thing
|
remainingUnexhausted := len(queries) // when all queries are exhausted we can finally end this thing
|
||||||
batchSizePerQuery := internal.BatchSizePerNumberOfQueries(limit, remainingUnexhausted)
|
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
|
// decode the entire thing
|
||||||
event := &nostr.Event{}
|
event := nostr.Event{}
|
||||||
if err := bin.Unmarshal(val, event); err != nil {
|
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],
|
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)
|
query.prefix, query.startingPoint, query.dbi, err)
|
||||||
return nil, fmt.Errorf("event read error: %w", 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}
|
evt := internal.IterEvent{Event: event, Q: q}
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
if secondPhase {
|
if sndPhase {
|
||||||
// do the process described below at HIWAWVRTP.
|
// do the process described below at HIWAWVRTP.
|
||||||
// if we've reached here this means we've already passed the `since` check.
|
// 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.
|
// now we have to eliminate the event currently at the `since` threshold.
|
||||||
nextThreshold := firstPhaseResults[len(firstPhaseResults)-2]
|
nextThreshold := firstPhaseResults[len(firstPhaseResults)-2]
|
||||||
if oldest.Event == nil {
|
if oldest.Event.ID == nostr.ZeroID {
|
||||||
// fmt.Println(" b1", evt.ID[0:8])
|
// fmt.Println(" b1", evt.ID[0:8])
|
||||||
// BRANCH WHEN WE DON'T HAVE THE OLDEST EVENT (BWWDHTOE)
|
// BRANCH WHEN WE DON'T HAVE THE OLDEST EVENT (BWWDHTOE)
|
||||||
// when we don't have the oldest set, we will keep the results
|
// 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.
|
// and not change the cutting point -- it's bad, but hopefully not that bad.
|
||||||
results[q] = append(results[q], evt)
|
results[q] = append(results[q], evt)
|
||||||
secondPhaseHasResultsPending = true
|
sndPhaseHasResultsPending = true
|
||||||
} else if nextThreshold.CreatedAt > oldest.CreatedAt {
|
} else if nextThreshold.CreatedAt > oldest.CreatedAt {
|
||||||
// fmt.Println(" b2", nextThreshold.CreatedAt, ">", oldest.CreatedAt, evt.ID[0:8])
|
// fmt.Println(" b2", nextThreshold.CreatedAt, ">", oldest.CreatedAt, evt.ID[0:8])
|
||||||
// one of the events we have stored is the actual next threshold
|
// 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
|
// finally
|
||||||
// add this to the results to be merged later
|
// add this to the results to be merged later
|
||||||
results[q] = append(results[q], evt)
|
results[q] = append(results[q], evt)
|
||||||
secondPhaseHasResultsPending = true
|
sndPhaseHasResultsPending = true
|
||||||
} else if nextThreshold.CreatedAt < evt.CreatedAt {
|
} else if nextThreshold.CreatedAt < evt.CreatedAt {
|
||||||
// the next last event in the firstPhaseResults is the next threshold
|
// the next last event in the firstPhaseResults is the next threshold
|
||||||
// fmt.Println(" b3", nextThreshold.CreatedAt, "<", oldest.CreatedAt, evt.ID[0:8])
|
// 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)
|
// fmt.Println(" new since", since)
|
||||||
// add this to the results to be merged later
|
// add this to the results to be merged later
|
||||||
results[q] = append(results[q], evt)
|
results[q] = append(results[q], evt)
|
||||||
secondPhaseHasResultsPending = true
|
sndPhaseHasResultsPending = true
|
||||||
// update the oldest event
|
// update the oldest event
|
||||||
if evt.CreatedAt < oldest.CreatedAt {
|
if evt.CreatedAt < oldest.CreatedAt {
|
||||||
oldest = evt
|
oldest = evt
|
||||||
@@ -247,7 +237,7 @@ func (b *LMDBBackend) query(txn *lmdb.Txn, filter nostr.Filter, limit int) ([]in
|
|||||||
firstPhaseTotalPulled++
|
firstPhaseTotalPulled++
|
||||||
|
|
||||||
// update the oldest event
|
// update the oldest event
|
||||||
if oldest.Event == nil || evt.CreatedAt < oldest.CreatedAt {
|
if oldest.Event.ID == nostr.ZeroID || evt.CreatedAt < oldest.CreatedAt {
|
||||||
oldest = evt
|
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
|
// 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)
|
// 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!")
|
// fmt.Println("second phase aggregation!")
|
||||||
// when we are in the second phase we will aggressively aggregate results on every iteration
|
// when we are in the second phase we will aggressively aggregate results on every iteration
|
||||||
//
|
//
|
||||||
secondBatch = secondBatch[:0]
|
secondBatch = secondBatch[:0]
|
||||||
for s := 0; s < len(secondPhaseParticipants); s++ {
|
for s := 0; s < len(sndPhaseParticipants); s++ {
|
||||||
q := secondPhaseParticipants[s]
|
q := sndPhaseParticipants[s]
|
||||||
|
|
||||||
if len(results[q]) > 0 {
|
if len(results[q]) > 0 {
|
||||||
secondBatch = append(secondBatch, results[q])
|
secondBatch = append(secondBatch, results[q])
|
||||||
}
|
}
|
||||||
|
|
||||||
if exhausted[q] {
|
if exhausted[q] {
|
||||||
secondPhaseParticipants = internal.SwapDelete(secondPhaseParticipants, s)
|
sndPhaseParticipants = internal.SwapDelete(sndPhaseParticipants, s)
|
||||||
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
|
// every time we get here we will alternate between these A and B lists
|
||||||
// combining everything we have into a new partial results list.
|
// combining everything we have into a new partial results list.
|
||||||
// after we've done that we can again set the oldest.
|
// after we've done that we can again set the oldest.
|
||||||
// fmt.Println(" xxx", secondPhaseResultsToggle)
|
// fmt.Println(" xxx", sndPhaseResultsToggle)
|
||||||
if secondPhaseResultsToggle {
|
if sndPhaseResultsToggle {
|
||||||
secondBatch = append(secondBatch, secondPhaseResultsB)
|
secondBatch = append(secondBatch, sndPhaseResultsB)
|
||||||
secondPhaseResultsA = internal.MergeSortMultiple(secondBatch, limit, secondPhaseResultsA)
|
sndPhaseResultsA = internal.MergeSortMultiple(secondBatch, limit, sndPhaseResultsA)
|
||||||
oldest = secondPhaseResultsA[len(secondPhaseResultsA)-1]
|
oldest = sndPhaseResultsA[len(sndPhaseResultsA)-1]
|
||||||
// fmt.Println(" new aggregated a", len(secondPhaseResultsB))
|
// fmt.Println(" new aggregated a", len(sndPhaseResultsB))
|
||||||
} else {
|
} else {
|
||||||
secondBatch = append(secondBatch, secondPhaseResultsA)
|
secondBatch = append(secondBatch, sndPhaseResultsA)
|
||||||
secondPhaseResultsB = internal.MergeSortMultiple(secondBatch, limit, secondPhaseResultsB)
|
sndPhaseResultsB = internal.MergeSortMultiple(secondBatch, limit, sndPhaseResultsB)
|
||||||
oldest = secondPhaseResultsB[len(secondPhaseResultsB)-1]
|
oldest = sndPhaseResultsB[len(sndPhaseResultsB)-1]
|
||||||
// fmt.Println(" new aggregated b", len(secondPhaseResultsB))
|
// fmt.Println(" new aggregated b", len(sndPhaseResultsB))
|
||||||
}
|
}
|
||||||
secondPhaseResultsToggle = !secondPhaseResultsToggle
|
sndPhaseResultsToggle = !sndPhaseResultsToggle
|
||||||
|
|
||||||
since = uint32(oldest.CreatedAt)
|
since = uint32(oldest.CreatedAt)
|
||||||
// fmt.Println(" new since", since)
|
// fmt.Println(" new since", since)
|
||||||
|
|
||||||
// reset the `results` list so we can keep using it
|
// reset the `results` list so we can keep using it
|
||||||
results = results[:len(queries)]
|
results = results[:len(queries)]
|
||||||
for _, q := range secondPhaseParticipants {
|
for _, q := range sndPhaseParticipants {
|
||||||
results[q] = results[q][:0]
|
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)
|
// fmt.Println("have enough!", firstPhaseTotalPulled, "/", limit, "remaining", remainingUnexhausted)
|
||||||
|
|
||||||
// we will exclude this oldest number as it is not relevant anymore
|
// 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]
|
results[q] = results[q][:0]
|
||||||
|
|
||||||
// build this index of indexes with everybody who remains
|
// 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
|
// we create these two lists and alternate between them so we don't have to create a
|
||||||
// a new one every time
|
// a new one every time
|
||||||
secondPhaseResultsA = make([]internal.IterEvent, 0, limit*2)
|
sndPhaseResultsA = make([]internal.IterEvent, 0, limit*2)
|
||||||
secondPhaseResultsB = make([]internal.IterEvent, 0, limit*2)
|
sndPhaseResultsB = make([]internal.IterEvent, 0, limit*2)
|
||||||
|
|
||||||
// from now on we won't run this block anymore
|
// from now on we won't run this block anymore
|
||||||
secondPhase = true
|
sndPhase = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// fmt.Println("remaining", remainingUnexhausted)
|
// 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
|
var combinedResults []internal.IterEvent
|
||||||
|
|
||||||
if secondPhase {
|
if sndPhase {
|
||||||
// fmt.Println("ending second phase")
|
// 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
|
// the other will be empty
|
||||||
var secondPhaseResults []internal.IterEvent
|
var sndPhaseResults []internal.IterEvent
|
||||||
// fmt.Println("xxx", secondPhaseResultsToggle, len(secondPhaseResultsA), len(secondPhaseResultsB))
|
// fmt.Println("xxx", sndPhaseResultsToggle, len(sndPhaseResultsA), len(sndPhaseResultsB))
|
||||||
if secondPhaseResultsToggle {
|
if sndPhaseResultsToggle {
|
||||||
secondPhaseResults = secondPhaseResultsB
|
sndPhaseResults = sndPhaseResultsB
|
||||||
combinedResults = secondPhaseResultsA[0:limit] // reuse this
|
combinedResults = sndPhaseResultsA[0:limit] // reuse this
|
||||||
// fmt.Println(" using b", len(secondPhaseResultsA))
|
// fmt.Println(" using b", len(sndPhaseResultsA))
|
||||||
} else {
|
} else {
|
||||||
secondPhaseResults = secondPhaseResultsA
|
sndPhaseResults = sndPhaseResultsA
|
||||||
combinedResults = secondPhaseResultsB[0:limit] // reuse this
|
combinedResults = sndPhaseResultsB[0:limit] // reuse this
|
||||||
// fmt.Println(" using a", len(secondPhaseResultsA))
|
// fmt.Println(" using a", len(sndPhaseResultsA))
|
||||||
}
|
}
|
||||||
|
|
||||||
all := [][]internal.IterEvent{firstPhaseResults, secondPhaseResults}
|
all := [][]internal.IterEvent{firstPhaseResults, sndPhaseResults}
|
||||||
combinedResults = internal.MergeSortMultiple(all, limit, combinedResults)
|
combinedResults = internal.MergeSortMultiple(all, limit, combinedResults)
|
||||||
// fmt.Println("final combinedResults", len(combinedResults), cap(combinedResults), limit)
|
// fmt.Println("final combinedResults", len(combinedResults), cap(combinedResults), limit)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
package lmdb
|
package lmdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/PowerDNS/lmdb-go/lmdb"
|
|
||||||
"fiatjaf.com/nostr/eventstore/internal"
|
|
||||||
"fiatjaf.com/nostr"
|
"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
|
// sanity checking
|
||||||
if evt.CreatedAt > math.MaxUint32 || evt.Kind > math.MaxUint16 {
|
if evt.CreatedAt > math.MaxUint32 || evt.Kind > math.MaxUint16 {
|
||||||
return fmt.Errorf("event with values out of expected boundaries")
|
return fmt.Errorf("event with values out of expected boundaries")
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.lmdbEnv.Update(func(txn *lmdb.Txn) error {
|
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) {
|
if nostr.IsAddressableKind(evt.Kind) {
|
||||||
// when addressable, add the "d" tag to the filter
|
// when addressable, add the "d" tag to the filter
|
||||||
filter.Tags = nostr.TagMap{"d": []string{evt.Tags.GetD()}}
|
filter.Tags = nostr.TagMap{"d": []string{evt.Tags.GetD()}}
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
package lmdb
|
package lmdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/PowerDNS/lmdb-go/lmdb"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/eventstore"
|
"fiatjaf.com/nostr/eventstore"
|
||||||
bin "fiatjaf.com/nostr/eventstore/internal/binary"
|
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
|
// sanity checking
|
||||||
if evt.CreatedAt > math.MaxUint32 || evt.Kind > math.MaxUint16 {
|
if evt.CreatedAt > math.MaxUint32 || evt.Kind > math.MaxUint16 {
|
||||||
return fmt.Errorf("event with values out of expected boundaries")
|
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
|
// check if we already have this id
|
||||||
id, _ := hex.DecodeString(evt.ID)
|
_, err := txn.Get(b.indexId, evt.ID[0:8])
|
||||||
_, err := txn.Get(b.indexId, id)
|
|
||||||
if operr, ok := err.(*lmdb.OpError); ok && operr.Errno != lmdb.NotFound {
|
if operr, ok := err.(*lmdb.OpError); ok && operr.Errno != lmdb.NotFound {
|
||||||
// we will only proceed if we get a NotFound
|
// we will only proceed if we get a NotFound
|
||||||
return eventstore.ErrDupEvent
|
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
|
// encode to binary form so we'll save it
|
||||||
bin, err := bin.Marshal(evt)
|
bin, err := bin.Marshal(evt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -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{}{})
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
package slicestore
|
package slicestore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"bytes"
|
||||||
|
"cmp"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"iter"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/eventstore"
|
"fiatjaf.com/nostr/eventstore"
|
||||||
"fiatjaf.com/nostr/eventstore/internal"
|
"fiatjaf.com/nostr/eventstore/internal"
|
||||||
"fiatjaf.com/nostr"
|
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,13 +17,13 @@ var _ eventstore.Store = (*SliceStore)(nil)
|
|||||||
|
|
||||||
type SliceStore struct {
|
type SliceStore struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
internal []*nostr.Event
|
internal []nostr.Event
|
||||||
|
|
||||||
MaxLimit int
|
MaxLimit int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *SliceStore) Init() error {
|
func (b *SliceStore) Init() error {
|
||||||
b.internal = make([]*nostr.Event, 0, 5000)
|
b.internal = make([]nostr.Event, 0, 5000)
|
||||||
if b.MaxLimit == 0 {
|
if b.MaxLimit == 0 {
|
||||||
b.MaxLimit = 500
|
b.MaxLimit = 500
|
||||||
}
|
}
|
||||||
@@ -31,8 +32,8 @@ func (b *SliceStore) Init() error {
|
|||||||
|
|
||||||
func (b *SliceStore) Close() {}
|
func (b *SliceStore) Close() {}
|
||||||
|
|
||||||
func (b *SliceStore) QueryEvents(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
func (b *SliceStore) QueryEvents(filter nostr.Filter) iter.Seq[nostr.Event] {
|
||||||
ch := make(chan *nostr.Event)
|
return func(yield func(nostr.Event) bool) {
|
||||||
if filter.Limit > b.MaxLimit || (filter.Limit == 0 && !filter.LimitZero) {
|
if filter.Limit > b.MaxLimit || (filter.Limit == 0 && !filter.LimitZero) {
|
||||||
filter.Limit = b.MaxLimit
|
filter.Limit = b.MaxLimit
|
||||||
}
|
}
|
||||||
@@ -49,32 +50,26 @@ func (b *SliceStore) QueryEvents(ctx context.Context, filter nostr.Filter) (chan
|
|||||||
|
|
||||||
// ham
|
// ham
|
||||||
if end < start {
|
if end < start {
|
||||||
close(ch)
|
return
|
||||||
return ch, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
count := 0
|
count := 0
|
||||||
go func() {
|
|
||||||
for _, event := range b.internal[start:end] {
|
for _, event := range b.internal[start:end] {
|
||||||
if count == filter.Limit {
|
if count == filter.Limit {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if filter.Matches(event) {
|
if filter.Matches(event) {
|
||||||
select {
|
if !yield(event) {
|
||||||
case ch <- event:
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
count++
|
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
|
var val int64
|
||||||
for _, event := range b.internal {
|
for _, event := range b.internal {
|
||||||
if filter.Matches(event) {
|
if filter.Matches(event) {
|
||||||
@@ -84,7 +79,7 @@ func (b *SliceStore) CountEvents(ctx context.Context, filter nostr.Filter) (int6
|
|||||||
return val, nil
|
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)
|
idx, found := slices.BinarySearchFunc(b.internal, evt, eventComparator)
|
||||||
if found {
|
if found {
|
||||||
return eventstore.ErrDupEvent
|
return eventstore.ErrDupEvent
|
||||||
@@ -97,8 +92,8 @@ func (b *SliceStore) SaveEvent(ctx context.Context, evt *nostr.Event) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *SliceStore) DeleteEvent(ctx context.Context, evt *nostr.Event) error {
|
func (b *SliceStore) DeleteEvent(id nostr.ID) error {
|
||||||
idx, found := slices.BinarySearchFunc(b.internal, evt, eventComparator)
|
idx, found := slices.BinarySearchFunc(b.internal, id, eventIDComparator)
|
||||||
if !found {
|
if !found {
|
||||||
// we don't have this event
|
// we don't have this event
|
||||||
return nil
|
return nil
|
||||||
@@ -110,24 +105,19 @@ func (b *SliceStore) DeleteEvent(ctx context.Context, evt *nostr.Event) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *SliceStore) ReplaceEvent(ctx context.Context, evt *nostr.Event) error {
|
func (b *SliceStore) ReplaceEvent(evt nostr.Event) error {
|
||||||
b.Lock()
|
b.Lock()
|
||||||
defer b.Unlock()
|
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) {
|
if nostr.IsAddressableKind(evt.Kind) {
|
||||||
filter.Tags = nostr.TagMap{"d": []string{evt.Tags.GetD()}}
|
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
|
shouldStore := true
|
||||||
for previous := range ch {
|
for previous := range b.QueryEvents(filter) {
|
||||||
if internal.IsOlder(previous, evt) {
|
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)
|
return fmt.Errorf("failed to delete event for replacing: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -136,7 +126,7 @@ func (b *SliceStore) ReplaceEvent(ctx context.Context, evt *nostr.Event) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if shouldStore {
|
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)
|
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
|
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)
|
return int(t) - int(e.CreatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func eventComparator(a *nostr.Event, b *nostr.Event) int {
|
func eventIDComparator(e nostr.Event, i nostr.ID) int {
|
||||||
c := int(b.CreatedAt) - int(a.CreatedAt)
|
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 {
|
if c != 0 {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
return strings.Compare(b.ID, a.ID)
|
return bytes.Compare(b.ID[:], a.ID[:])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
package slicestore
|
package slicestore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBasicStuff(t *testing.T) {
|
func TestBasicStuff(t *testing.T) {
|
||||||
ctx := context.Background()
|
|
||||||
ss := &SliceStore{}
|
ss := &SliceStore{}
|
||||||
ss.Init()
|
ss.Init()
|
||||||
defer ss.Close()
|
defer ss.Close()
|
||||||
@@ -22,12 +20,11 @@ func TestBasicStuff(t *testing.T) {
|
|||||||
if i%3 == 0 {
|
if i%3 == 0 {
|
||||||
kind = 12
|
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)
|
||||||
list := make([]*nostr.Event, 0, 20)
|
for event := range ss.QueryEvents(nostr.Filter{}) {
|
||||||
for event := range ch {
|
|
||||||
list = append(list, event)
|
list = append(list, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,9 +36,8 @@ func TestBasicStuff(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
until := nostr.Timestamp(9999)
|
until := nostr.Timestamp(9999)
|
||||||
ch, _ = ss.QueryEvents(ctx, nostr.Filter{Limit: 15, Until: &until, Kinds: []int{11}})
|
list = make([]nostr.Event, 0, 7)
|
||||||
list = make([]*nostr.Event, 0, 7)
|
for event := range ss.QueryEvents(nostr.Filter{Limit: 15, Until: &until, Kinds: []uint16{11}}) {
|
||||||
for event := range ch {
|
|
||||||
list = append(list, event)
|
list = append(list, event)
|
||||||
}
|
}
|
||||||
if len(list) != 7 {
|
if len(list) != 7 {
|
||||||
@@ -49,9 +45,8 @@ func TestBasicStuff(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
since := nostr.Timestamp(10009)
|
since := nostr.Timestamp(10009)
|
||||||
ch, _ = ss.QueryEvents(ctx, nostr.Filter{Since: &since})
|
list = make([]nostr.Event, 0, 5)
|
||||||
list = make([]*nostr.Event, 0, 5)
|
for event := range ss.QueryEvents(nostr.Filter{Since: &since}) {
|
||||||
for event := range ch {
|
|
||||||
list = append(list, event)
|
list = append(list, event)
|
||||||
}
|
}
|
||||||
if len(list) != 5 {
|
if len(list) != 5 {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package eventstore
|
package eventstore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"iter"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"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 must be called after you're done using the store, to free up resources and so on.
|
||||||
Close()
|
Close()
|
||||||
|
|
||||||
// QueryEvents should return a channel with the events as they're recovered from a database.
|
// QueryEvents returns events that match the filter
|
||||||
// the channel should be closed after the events are all delivered.
|
QueryEvents(nostr.Filter) iter.Seq[nostr.Event]
|
||||||
QueryEvents(context.Context, nostr.Filter) (chan *nostr.Event, error)
|
|
||||||
// DeleteEvent just deletes an event, no side-effects.
|
// DeleteEvent deletes an event atomically by ID
|
||||||
DeleteEvent(context.Context, *nostr.Event) error
|
DeleteEvent(nostr.ID) error
|
||||||
|
|
||||||
// SaveEvent just saves an event, no side-effects.
|
// 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.
|
// ReplaceEvent atomically replaces a replaceable or addressable event.
|
||||||
// Conceptually it is like a Query->Delete->Save, but streamlined.
|
// Conceptually it is like a Query->Delete->Save, but streamlined.
|
||||||
ReplaceEvent(context.Context, *nostr.Event) error
|
ReplaceEvent(nostr.Event) error
|
||||||
}
|
|
||||||
|
|
||||||
type Counter interface {
|
// CountEvents counts all events that match a given filter
|
||||||
CountEvents(context.Context, nostr.Filter) (int64, error)
|
CountEvents(nostr.Filter) (int64, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
|
|
||||||
"fiatjaf.com/nostr/eventstore"
|
"fiatjaf.com/nostr/eventstore"
|
||||||
"fiatjaf.com/nostr/eventstore/badger"
|
"fiatjaf.com/nostr/eventstore/badger"
|
||||||
"fiatjaf.com/nostr/eventstore/lmdb"
|
"fiatjaf.com/nostr/eventstore/lmdb"
|
||||||
"fiatjaf.com/nostr/eventstore/postgresql"
|
|
||||||
"fiatjaf.com/nostr/eventstore/slicestore"
|
"fiatjaf.com/nostr/eventstore/slicestore"
|
||||||
"fiatjaf.com/nostr/eventstore/sqlite3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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"}) })
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (ef Filter) String() string {
|
|||||||
return string(j)
|
return string(j)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ef Filter) Matches(event *Event) bool {
|
func (ef Filter) Matches(event Event) bool {
|
||||||
if !ef.MatchesIgnoringTimestampConstraints(event) {
|
if !ef.MatchesIgnoringTimestampConstraints(event) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -43,11 +43,7 @@ func (ef Filter) Matches(event *Event) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ef Filter) MatchesIgnoringTimestampConstraints(event *Event) bool {
|
func (ef Filter) MatchesIgnoringTimestampConstraints(event Event) bool {
|
||||||
if event == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if ef.IDs != nil && !slices.Contains(ef.IDs, event.ID) {
|
if ef.IDs != nil && !slices.Contains(ef.IDs, event.ID) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
72
interface.go
72
interface.go
@@ -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...)
|
|
||||||
}
|
|
||||||
@@ -25,12 +25,12 @@ func NewBunkerSignerFromBunkerClient(bc *nip46.BunkerClient) BunkerSigner {
|
|||||||
|
|
||||||
// GetPublicKey retrieves the public key from the remote bunker.
|
// GetPublicKey retrieves the public key from the remote bunker.
|
||||||
// It uses a timeout to prevent hanging indefinitely.
|
// 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"))
|
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*30, errors.New("get_public_key took too long"))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
pk, err := bs.bunker.GetPublicKey(ctx)
|
pk, err := bs.bunker.GetPublicKey(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nostr.ZeroPK, err
|
||||||
}
|
}
|
||||||
return pk, nil
|
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.
|
// 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)
|
return bs.bunker.NIP44Encrypt(ctx, recipient, plaintext)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt decrypts a base64-encoded ciphertext from a sender using the remote bunker.
|
// 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)
|
return bs.bunker.NIP44Encrypt(ctx, sender, base64ciphertext)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,26 +16,23 @@ var _ nostr.Keyer = (*EncryptedKeySigner)(nil)
|
|||||||
// when needed for operations.
|
// when needed for operations.
|
||||||
type EncryptedKeySigner struct {
|
type EncryptedKeySigner struct {
|
||||||
ncryptsec string
|
ncryptsec string
|
||||||
pk string
|
pk nostr.PubKey
|
||||||
callback func(context.Context) string
|
callback func(context.Context) string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPublicKey returns the public key associated with this signer.
|
// 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
|
// If the public key is not cached, it will decrypt the private key using the password
|
||||||
// callback to derive the public key.
|
// callback to derive the public key.
|
||||||
func (es *EncryptedKeySigner) GetPublicKey(ctx context.Context) (string, error) {
|
func (es *EncryptedKeySigner) GetPublicKey(ctx context.Context) (nostr.PubKey, error) {
|
||||||
if es.pk != "" {
|
if es.pk != nostr.ZeroPK {
|
||||||
return es.pk, nil
|
return es.pk, nil
|
||||||
}
|
}
|
||||||
password := es.callback(ctx)
|
password := es.callback(ctx)
|
||||||
key, err := nip49.Decrypt(es.ncryptsec, password)
|
key, err := nip49.Decrypt(es.ncryptsec, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nostr.ZeroPK, err
|
||||||
}
|
|
||||||
pk, err := nostr.GetPublicKey(key)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
|
pk := nostr.GetPublicKey(key)
|
||||||
es.pk = pk
|
es.pk = pk
|
||||||
return pk, nil
|
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.
|
// Encrypt encrypts a plaintext message for a recipient using NIP-44.
|
||||||
// It first decrypts the private key using the password callback.
|
// 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)
|
password := es.callback(ctx)
|
||||||
sk, err := nip49.Decrypt(es.ncryptsec, password)
|
sk, err := nip49.Decrypt(es.ncryptsec, password)
|
||||||
if err != nil {
|
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.
|
// Decrypt decrypts a base64-encoded ciphertext from a sender using NIP-44.
|
||||||
// It first decrypts the private key using the password callback.
|
// 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)
|
password := es.callback(ctx)
|
||||||
sk, err := nip49.Decrypt(es.ncryptsec, password)
|
sk, err := nip49.Decrypt(es.ncryptsec, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ type SignerOptions struct {
|
|||||||
// The context is used for operations that may require network access.
|
// The context is used for operations that may require network access.
|
||||||
// The pool is used for relay connections when needed.
|
// The pool is used for relay connections when needed.
|
||||||
// Options are used for additional pieces required for EncryptedKeySigner and BunkerSigner.
|
// 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 {
|
if opts == nil {
|
||||||
opts = &SignerOptions{}
|
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)
|
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
|
return KeySigner{sec, pk, xsync.NewMapOf[string, [32]byte]()}, nil
|
||||||
} else if nip46.IsValidBunkerURL(input) || nip05.IsValidIdentifier(input) {
|
} else if nip46.IsValidBunkerURL(input) || nip05.IsValidIdentifier(input) {
|
||||||
bcsk := nostr.GeneratePrivateKey()
|
bcsk := nostr.GeneratePrivateKey()
|
||||||
|
|||||||
@@ -14,16 +14,16 @@ var _ nostr.Keyer = (*ManualSigner)(nil)
|
|||||||
// app wants to implement custom signing logic.
|
// app wants to implement custom signing logic.
|
||||||
type ManualSigner struct {
|
type ManualSigner struct {
|
||||||
// ManualGetPublicKey is called when the public key is needed
|
// 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 is called when an event needs to be signed
|
||||||
ManualSignEvent func(context.Context, *nostr.Event) error
|
ManualSignEvent func(context.Context, *nostr.Event) error
|
||||||
|
|
||||||
// ManualEncrypt is called when a message needs to be encrypted
|
// 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 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.
|
// 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.
|
// 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)
|
return ms.ManualGetPublicKey(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt delegates encryption to the ManualEncrypt function.
|
// 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)
|
return ms.ManualEncrypt(ctx, plaintext, recipient)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt delegates decryption to the ManualDecrypt function.
|
// 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)
|
return ms.ManualDecrypt(ctx, base64ciphertext, sender)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,20 +12,16 @@ var _ nostr.Keyer = (*KeySigner)(nil)
|
|||||||
|
|
||||||
// KeySigner is a signer that holds the private key in memory
|
// KeySigner is a signer that holds the private key in memory
|
||||||
type KeySigner struct {
|
type KeySigner struct {
|
||||||
sk string
|
sk [32]byte
|
||||||
pk string
|
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.
|
// NewPlainKeySigner creates a new KeySigner from a private key.
|
||||||
// Returns an error if the private key is invalid.
|
// Returns an error if the private key is invalid.
|
||||||
func NewPlainKeySigner(sec string) (KeySigner, error) {
|
func NewPlainKeySigner(sec [32]byte) (KeySigner, error) {
|
||||||
pk, err := nostr.GetPublicKey(sec)
|
return KeySigner{sec, nostr.GetPublicKey(sec), xsync.NewMapOf[nostr.PubKey, [32]byte]()}, nil
|
||||||
if err != nil {
|
|
||||||
return KeySigner{}, err
|
|
||||||
}
|
|
||||||
return KeySigner{sec, pk, xsync.NewMapOf[string, [32]byte]()}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignEvent signs the provided event with the signer's private key.
|
// 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) }
|
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.
|
// 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.
|
// Encrypt encrypts a plaintext message for a recipient using NIP-44.
|
||||||
// It caches conversation keys for efficiency in repeated operations.
|
// 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)
|
ck, ok := ks.conversationKeys.Load(recipient)
|
||||||
if !ok {
|
if !ok {
|
||||||
var err error
|
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.
|
// Decrypt decrypts a base64-encoded ciphertext from a sender using NIP-44.
|
||||||
// It caches conversation keys for efficiency in repeated operations.
|
// 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)
|
ck, ok := ks.conversationKeys.Load(sender)
|
||||||
if !ok {
|
if !ok {
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/liamg/magic"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"github.com/liamg/magic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bs BlossomServer) handleUploadCheck(w http.ResponseWriter, r *http.Request) {
|
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
|
// get the file size from the incoming header
|
||||||
size, _ := strconv.Atoi(r.Header.Get("X-Content-Length"))
|
size, _ := strconv.Atoi(r.Header.Get("X-Content-Length"))
|
||||||
|
|
||||||
for _, rb := range bs.RejectUpload {
|
if bs.RejectUpload != nil {
|
||||||
reject, reason, code := rb(r.Context(), auth, size, ext)
|
reject, reason, code := bs.RejectUpload(r.Context(), auth, size, ext)
|
||||||
if reject {
|
if reject {
|
||||||
blossomError(w, reason, code)
|
blossomError(w, reason, code)
|
||||||
return
|
return
|
||||||
@@ -336,13 +336,13 @@ func (bs BlossomServer) handleReport(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var evt *nostr.Event
|
var evt nostr.Event
|
||||||
if err := json.Unmarshal(body, evt); err != nil {
|
if err := json.Unmarshal(body, &evt); err != nil {
|
||||||
blossomError(w, "can't parse event", 400)
|
blossomError(w, "can't parse event", 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if isValid, _ := evt.CheckSignature(); !isValid {
|
if !evt.VerifySignature() {
|
||||||
blossomError(w, "invalid report event is provided", 400)
|
blossomError(w, "invalid report event is provided", 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -352,8 +352,8 @@ func (bs BlossomServer) handleReport(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rr := range bs.ReceiveReport {
|
if bs.ReceiveReport != nil {
|
||||||
if err := rr(r.Context(), evt); err != nil {
|
if err := bs.ReceiveReport(r.Context(), evt); err != nil {
|
||||||
blossomError(w, "failed to receive report: "+err.Error(), 500)
|
blossomError(w, "failed to receive report: "+err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,23 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"fiatjaf.com/nostr/khatru"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/khatru"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BlossomServer struct {
|
type BlossomServer struct {
|
||||||
ServiceURL string
|
ServiceURL string
|
||||||
Store BlobIndex
|
Store BlobIndex
|
||||||
|
|
||||||
StoreBlob []func(ctx context.Context, sha256 string, body []byte) error
|
StoreBlob func(ctx context.Context, sha256 string, body []byte) error
|
||||||
LoadBlob []func(ctx context.Context, sha256 string) (io.ReadSeeker, error)
|
LoadBlob func(ctx context.Context, sha256 string) (io.ReadSeeker, error)
|
||||||
DeleteBlob []func(ctx context.Context, sha256 string) error
|
DeleteBlob func(ctx context.Context, sha256 string) error
|
||||||
ReceiveReport []func(ctx context.Context, reportEvt *nostr.Event) 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)
|
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)
|
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)
|
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)
|
RejectDelete func(ctx context.Context, auth *nostr.Event, sha256 string) (bool, string, int)
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(rl *khatru.Relay, serviceURL string) *BlossomServer {
|
func New(rl *khatru.Relay, serviceURL string) *BlossomServer {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -6,9 +6,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/khatru"
|
"fiatjaf.com/nostr/khatru"
|
||||||
"fiatjaf.com/nostr/khatru/policies"
|
"fiatjaf.com/nostr/khatru/policies"
|
||||||
"fiatjaf.com/nostr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
254
khatru/go.sum
254
khatru/go.sum
@@ -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=
|
|
||||||
@@ -12,8 +12,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/bep/debounce"
|
|
||||||
"github.com/fasthttp/websocket"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/nip42"
|
"fiatjaf.com/nostr/nip42"
|
||||||
"fiatjaf.com/nostr/nip45"
|
"fiatjaf.com/nostr/nip45"
|
||||||
@@ -21,6 +19,8 @@ import (
|
|||||||
"fiatjaf.com/nostr/nip70"
|
"fiatjaf.com/nostr/nip70"
|
||||||
"fiatjaf.com/nostr/nip77"
|
"fiatjaf.com/nostr/nip77"
|
||||||
"fiatjaf.com/nostr/nip77/negentropy"
|
"fiatjaf.com/nostr/nip77/negentropy"
|
||||||
|
"github.com/bep/debounce"
|
||||||
|
"github.com/fasthttp/websocket"
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
"github.com/rs/cors"
|
"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) {
|
func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
for _, reject := range rl.RejectConnection {
|
if rl.RejectConnection == nil {
|
||||||
if reject(r) {
|
if rl.RejectConnection(r) {
|
||||||
w.WriteHeader(429) // Too many requests
|
w.WriteHeader(429) // Too many requests
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -92,8 +92,8 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
kill := func() {
|
kill := func() {
|
||||||
for _, ondisconnect := range rl.OnDisconnect {
|
if rl.OnDisconnect == nil {
|
||||||
ondisconnect(ctx)
|
rl.OnDisconnect(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
@@ -114,8 +114,8 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, onconnect := range rl.OnConnect {
|
if rl.OnConnect == nil {
|
||||||
onconnect(ctx)
|
rl.OnConnect(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
smp := nostr.NewMessageParser()
|
smp := nostr.NewMessageParser()
|
||||||
@@ -169,10 +169,7 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check signature
|
// check signature
|
||||||
if ok, err := env.Event.CheckSignature(); err != nil {
|
if !env.Event.VerifySignature() {
|
||||||
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "error: failed to verify signature"})
|
|
||||||
return
|
|
||||||
} else if !ok {
|
|
||||||
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "invalid: signature is invalid"})
|
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "invalid: signature is invalid"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -228,9 +225,6 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
var reason string
|
var reason string
|
||||||
if writeErr == nil {
|
if writeErr == nil {
|
||||||
ok = true
|
ok = true
|
||||||
for _, ovw := range srl.OverwriteResponseEvent {
|
|
||||||
ovw(ctx, &env.Event)
|
|
||||||
}
|
|
||||||
if !skipBroadcast {
|
if !skipBroadcast {
|
||||||
n := srl.notifyListeners(&env.Event)
|
n := srl.notifyListeners(&env.Event)
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ import (
|
|||||||
type RelayManagementAPI struct {
|
type RelayManagementAPI struct {
|
||||||
RejectAPICall []func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string)
|
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)
|
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)
|
ListAllowedPubKeys func(ctx context.Context) ([]nip86.PubKeyReason, error)
|
||||||
ListEventsNeedingModeration func(ctx context.Context) ([]nip86.IDReason, error)
|
ListEventsNeedingModeration func(ctx context.Context) ([]nip86.IDReason, error)
|
||||||
AllowEvent 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 string, reason string) error
|
BanEvent func(ctx context.Context, id nostr.ID, reason string) error
|
||||||
ListBannedEvents func(ctx context.Context) ([]nip86.IDReason, error)
|
ListBannedEvents func(ctx context.Context) ([]nip86.IDReason, error)
|
||||||
ListAllowedEvents func(ctx context.Context) ([]nip86.IDReason, error)
|
ListAllowedEvents func(ctx context.Context) ([]nip86.IDReason, error)
|
||||||
ChangeRelayName func(ctx context.Context, name string) 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
|
UnblockIP func(ctx context.Context, ip net.IP, reason string) error
|
||||||
ListBlockedIPs func(ctx context.Context) ([]nip86.IPReason, error)
|
ListBlockedIPs func(ctx context.Context) ([]nip86.IPReason, error)
|
||||||
Stats func(ctx context.Context) (nip86.Response, error)
|
Stats func(ctx context.Context) (nip86.Response, error)
|
||||||
GrantAdmin 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 string, 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)
|
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"
|
resp.Error = "invalid auth event json"
|
||||||
goto respond
|
goto respond
|
||||||
}
|
}
|
||||||
if ok, _ := evt.CheckSignature(); !ok {
|
if !evt.VerifySignature() {
|
||||||
resp.Error = "invalid auth event"
|
resp.Error = "invalid auth event"
|
||||||
goto respond
|
goto respond
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fasthttp/websocket"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/nip11"
|
"fiatjaf.com/nostr/nip11"
|
||||||
"fiatjaf.com/nostr/nip45/hyperloglog"
|
"fiatjaf.com/nostr/nip45/hyperloglog"
|
||||||
|
"github.com/fasthttp/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRelay() *Relay {
|
func NewRelay() *Relay {
|
||||||
@@ -56,25 +56,23 @@ type Relay struct {
|
|||||||
ServiceURL string
|
ServiceURL string
|
||||||
|
|
||||||
// hooks that will be called at various times
|
// hooks that will be called at various times
|
||||||
RejectEvent []func(ctx context.Context, event *nostr.Event) (reject bool, msg string)
|
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)
|
OverwriteDeletionOutcome func(ctx context.Context, target *nostr.Event, deletion *nostr.Event) (acceptDeletion bool, msg string)
|
||||||
StoreEvent []func(ctx context.Context, event *nostr.Event) error
|
StoreEvent func(ctx context.Context, event *nostr.Event) error
|
||||||
ReplaceEvent []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
|
DeleteEvent func(ctx context.Context, event *nostr.Event) error
|
||||||
OnEventSaved []func(ctx context.Context, event *nostr.Event)
|
OnEventSaved func(ctx context.Context, event *nostr.Event)
|
||||||
OnEphemeralEvent []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)
|
RejectFilter func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
|
||||||
RejectCountFilter []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)
|
||||||
QueryEvents []func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)
|
CountEvents func(ctx context.Context, filter nostr.Filter) (int64, 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)
|
||||||
CountEventsHLL []func(ctx context.Context, filter nostr.Filter, offset int) (int64, *hyperloglog.HyperLogLog, error)
|
RejectConnection func(r *http.Request) bool
|
||||||
RejectConnection []func(r *http.Request) bool
|
OnConnect func(ctx context.Context)
|
||||||
OnConnect []func(ctx context.Context)
|
OnDisconnect func(ctx context.Context)
|
||||||
OnDisconnect []func(ctx context.Context)
|
OverwriteRelayInformation func(ctx context.Context, r *http.Request, info nip11.RelayInformationDocument) nip11.RelayInformationDocument
|
||||||
OverwriteRelayInformation []func(ctx context.Context, r *http.Request, info nip11.RelayInformationDocument) nip11.RelayInformationDocument
|
PreventBroadcast func(ws *WebSocket, event *nostr.Event) bool
|
||||||
OverwriteResponseEvent []func(ctx context.Context, event *nostr.Event)
|
|
||||||
PreventBroadcast []func(ws *WebSocket, event *nostr.Event) bool
|
|
||||||
|
|
||||||
// these are used when this relays acts as a router
|
// these are used when this relays acts as a router
|
||||||
routes []Route
|
routes []Route
|
||||||
|
|||||||
@@ -12,12 +12,6 @@ import (
|
|||||||
func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGroup, ws *WebSocket, filter nostr.Filter) error {
|
func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGroup, ws *WebSocket, filter nostr.Filter) error {
|
||||||
defer eose.Done()
|
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 {
|
if filter.LimitZero {
|
||||||
// don't do any queries, just subscribe to future events
|
// don't do any queries, just subscribe to future events
|
||||||
return nil
|
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
|
// 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
|
// that we know we don't support, and then if the end result is an empty
|
||||||
// filter we can just reject it)
|
// filter we can just reject it)
|
||||||
for _, reject := range rl.RejectFilter {
|
if rl.RejectFilter != nil {
|
||||||
if reject, msg := reject(ctx, filter); reject {
|
if reject, msg := rl.RejectFilter(ctx, filter); reject {
|
||||||
return errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
|
return errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// run the functions to query events (generally just one,
|
// run the function to query events
|
||||||
// but we might be fetching stuff from multiple places)
|
if rl.QueryEvents != nil {
|
||||||
eose.Add(len(rl.QueryEvents))
|
ch, err := rl.QueryEvents(ctx, filter)
|
||||||
for _, query := range rl.QueryEvents {
|
|
||||||
ch, err := query(ctx, filter)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
|
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
|
||||||
eose.Done()
|
eose.Done()
|
||||||
continue
|
|
||||||
} else if ch == nil {
|
} else if ch == nil {
|
||||||
eose.Done()
|
eose.Done()
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go func(ch chan *nostr.Event) {
|
go func(ch chan *nostr.Event) {
|
||||||
for event := range ch {
|
for event := range ch {
|
||||||
for _, ovw := range rl.OverwriteResponseEvent {
|
|
||||||
ovw(ctx, event)
|
|
||||||
}
|
|
||||||
ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &id, Event: *event})
|
ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &id, Event: *event})
|
||||||
}
|
}
|
||||||
eose.Done()
|
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 {
|
func (rl *Relay) handleCountRequest(ctx context.Context, ws *WebSocket, filter nostr.Filter) int64 {
|
||||||
// check if we'll reject this filter
|
// check if we'll reject this filter
|
||||||
for _, reject := range rl.RejectCountFilter {
|
if rl.RejectCountFilter != nil {
|
||||||
if rejecting, msg := reject(ctx, filter); rejecting {
|
if rejecting, msg := rl.RejectCountFilter(ctx, filter); rejecting {
|
||||||
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
||||||
return 0
|
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)
|
// run the functions to count (generally it will be just one)
|
||||||
var subtotal int64 = 0
|
var subtotal int64 = 0
|
||||||
for _, count := range rl.CountEvents {
|
if rl.CountEvents != nil {
|
||||||
res, err := count(ctx, filter)
|
res, err := rl.CountEvents(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
|
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
|
||||||
}
|
}
|
||||||
@@ -90,8 +77,8 @@ func (rl *Relay) handleCountRequestWithHLL(
|
|||||||
offset int,
|
offset int,
|
||||||
) (int64, *hyperloglog.HyperLogLog) {
|
) (int64, *hyperloglog.HyperLogLog) {
|
||||||
// check if we'll reject this filter
|
// check if we'll reject this filter
|
||||||
for _, reject := range rl.RejectCountFilter {
|
if rl.RejectCountFilter != nil {
|
||||||
if rejecting, msg := reject(ctx, filter); rejecting {
|
if rejecting, msg := rl.RejectCountFilter(ctx, filter); rejecting {
|
||||||
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
@@ -100,8 +87,8 @@ func (rl *Relay) handleCountRequestWithHLL(
|
|||||||
// run the functions to count (generally it will be just one)
|
// run the functions to count (generally it will be just one)
|
||||||
var subtotal int64 = 0
|
var subtotal int64 = 0
|
||||||
var hll *hyperloglog.HyperLogLog
|
var hll *hyperloglog.HyperLogLog
|
||||||
for _, countHLL := range rl.CountEventsHLL {
|
if rl.CountEventsHLL != nil {
|
||||||
res, fhll, err := countHLL(ctx, filter, offset)
|
res, fhll, err := rl.CountEventsHLL(ctx, filter, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
|
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,14 @@ func GetConnection(ctx context.Context) *WebSocket {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAuthed(ctx context.Context) string {
|
func GetAuthed(ctx context.Context) (nostr.PubKey, bool) {
|
||||||
if conn := GetConnection(ctx); conn != nil {
|
if conn := GetConnection(ctx); conn != nil {
|
||||||
return conn.AuthedPublicKey
|
return conn.AuthedPublicKey, true
|
||||||
}
|
}
|
||||||
if nip86Auth := ctx.Value(nip86HeaderAuthKey); nip86Auth != nil {
|
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
|
// IsInternalCall returns true when a call to QueryEvents, for example, is being made because of a deletion
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
"github.com/fasthttp/websocket"
|
"github.com/fasthttp/websocket"
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
)
|
)
|
||||||
@@ -22,7 +23,7 @@ type WebSocket struct {
|
|||||||
|
|
||||||
// nip42
|
// nip42
|
||||||
Challenge string
|
Challenge string
|
||||||
AuthedPublicKey string
|
AuthedPublicKey nostr.PubKey
|
||||||
Authed chan struct{}
|
Authed chan struct{}
|
||||||
|
|
||||||
// nip77
|
// nip77
|
||||||
|
|||||||
4
log.go
4
log.go
@@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// call SetOutput on InfoLogger to enable info logging
|
// 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
|
// 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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ func CommittedDifficulty(event *nostr.Event) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Difficulty counts the number of leading zero bits in an event ID.
|
// 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 zeros int
|
||||||
var b [1]byte
|
var b [1]byte
|
||||||
for i := 0; i < 64; i += 2 {
|
for i := 0; i < 32; i += 2 {
|
||||||
if id[i:i+2] == "00" {
|
if id[i] == 0 {
|
||||||
zeros += 8
|
zeros += 8
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -70,8 +70,8 @@ func difficultyBytes(id [32]byte) int {
|
|||||||
// Check reports whether the event ID demonstrates a sufficient proof of work difficulty.
|
// 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
|
// 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,
|
// in an event ID. It is up to the callers to verify the event with other methods,
|
||||||
// such as [nostr.Event.CheckSignature].
|
// such as [nostr.Event.VerifySignature].
|
||||||
func Check(id string, minDifficulty int) error {
|
func Check(id nostr.ID, minDifficulty int) error {
|
||||||
if Difficulty(id) < minDifficulty {
|
if Difficulty(id) < minDifficulty {
|
||||||
return ErrDifficultyTooLow
|
return ErrDifficultyTooLow
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ func Check(id string, minDifficulty int) error {
|
|||||||
// nonce (as a nostr.Tag) that yields the required work.
|
// nonce (as a nostr.Tag) that yields the required work.
|
||||||
// Returns an error if the context expires before that.
|
// Returns an error if the context expires before that.
|
||||||
func DoWork(ctx context.Context, event nostr.Event, targetDifficulty int) (nostr.Tag, error) {
|
func DoWork(ctx context.Context, event nostr.Event, targetDifficulty int) (nostr.Tag, error) {
|
||||||
if event.PubKey == "" {
|
if event.PubKey == nostr.ZeroPK {
|
||||||
return nil, ErrMissingPubKey
|
return nil, ErrMissingPubKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"fiatjaf.com/nostr/nip59"
|
"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{
|
ie := pool.QuerySingle(ctx, relaysToQuery, nostr.Filter{
|
||||||
Authors: []string{pubkey},
|
Authors: []string{pubkey},
|
||||||
Kinds: []int{nostr.KindDMRelayList},
|
Kinds: []int{nostr.KindDMRelayList},
|
||||||
@@ -35,7 +35,7 @@ func PublishMessage(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
content string,
|
content string,
|
||||||
tags nostr.Tags,
|
tags nostr.Tags,
|
||||||
pool *nostr.SimplePool,
|
pool *nostr.Pool,
|
||||||
ourRelays []string,
|
ourRelays []string,
|
||||||
theirRelays []string,
|
theirRelays []string,
|
||||||
kr nostr.Keyer,
|
kr nostr.Keyer,
|
||||||
@@ -137,7 +137,7 @@ func PrepareMessage(
|
|||||||
// ListenForMessages returns a channel with the rumors already decrypted and checked
|
// ListenForMessages returns a channel with the rumors already decrypted and checked
|
||||||
func ListenForMessages(
|
func ListenForMessages(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
pool *nostr.SimplePool,
|
pool *nostr.Pool,
|
||||||
kr nostr.Keyer,
|
kr nostr.Keyer,
|
||||||
ourRelays []string,
|
ourRelays []string,
|
||||||
since nostr.Timestamp,
|
since nostr.Timestamp,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
// CreateUnsignedAuthEvent creates an event which should be sent via an "AUTH" command.
|
// CreateUnsignedAuthEvent creates an event which should be sent via an "AUTH" command.
|
||||||
// If the authentication succeeds, the user will be authenticated as pubkey.
|
// 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{
|
return nostr.Event{
|
||||||
PubKey: pubkey,
|
PubKey: pubkey,
|
||||||
CreatedAt: nostr.Now(),
|
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.
|
// 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.
|
// 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 {
|
if event.Kind != nostr.KindClientAuthentication {
|
||||||
return "", false
|
return nostr.ZeroPK, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.Tags.FindWithValue("challenge", challenge) == nil {
|
if event.Tags.FindWithValue("challenge", challenge) == nil {
|
||||||
return "", false
|
return nostr.ZeroPK, false
|
||||||
}
|
}
|
||||||
|
|
||||||
expected, err := parseURL(relayURL)
|
expected, err := parseURL(relayURL)
|
||||||
if err != nil {
|
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 {
|
if err != nil {
|
||||||
return "", false
|
return nostr.ZeroPK, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if expected.Scheme != found.Scheme ||
|
if expected.Scheme != found.Scheme ||
|
||||||
expected.Host != found.Host ||
|
expected.Host != found.Host ||
|
||||||
expected.Path != found.Path {
|
expected.Path != found.Path {
|
||||||
return "", false
|
return nostr.ZeroPK, false
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if event.CreatedAt.Time().After(now.Add(10*time.Minute)) || event.CreatedAt.Time().Before(now.Add(-10*time.Minute)) {
|
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
|
// save for last, as it is most expensive operation
|
||||||
// no need to check returned error, since ok == true implies err == nil.
|
// no need to check returned error, since ok == true implies err == nil.
|
||||||
if ok, _ := event.CheckSignature(); !ok {
|
if !event.VerifySignature() {
|
||||||
return "", false
|
return nostr.ZeroPK, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return event.PubKey, true
|
return event.PubKey, true
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"fiatjaf.com/nostr"
|
"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) {
|
return func(yield func(string, int) bool) {
|
||||||
switch evt.Kind {
|
switch evt.Kind {
|
||||||
case 3:
|
case 3:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package hyperloglog
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"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)
|
// 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) {
|
func (hll *HyperLogLog) Add(pubkey [32]byte) {
|
||||||
x, _ := hex.DecodeString(pubkey[hll.offset*2 : hll.offset*2+8*2])
|
x := pubkey[hll.offset : hll.offset+8]
|
||||||
j := x[0] // register address (first 8 bits, i.e. first byte)
|
j := x[0] // register address (first 8 bits, i.e. first byte)
|
||||||
|
|
||||||
w := binary.BigEndian.Uint64(x) // number that we will use
|
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
|
// 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]
|
x := pubkey[hll.offset : hll.offset+8]
|
||||||
j := x[0] // register address (first 8 bits, i.e. first byte)
|
j := x[0] // register address (first 8 bits, i.e. first byte)
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ package nip46
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/nip04"
|
|
||||||
"fiatjaf.com/nostr/nip44"
|
"fiatjaf.com/nostr/nip44"
|
||||||
"github.com/mailru/easyjson"
|
"github.com/mailru/easyjson"
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
@@ -17,9 +18,9 @@ import (
|
|||||||
|
|
||||||
type BunkerClient struct {
|
type BunkerClient struct {
|
||||||
serial atomic.Uint64
|
serial atomic.Uint64
|
||||||
clientSecretKey string
|
clientSecretKey [32]byte
|
||||||
pool *nostr.SimplePool
|
pool *nostr.Pool
|
||||||
target string
|
target nostr.PubKey
|
||||||
relays []string
|
relays []string
|
||||||
conversationKey [32]byte // nip44
|
conversationKey [32]byte // nip44
|
||||||
listeners *xsync.MapOf[string, chan Response]
|
listeners *xsync.MapOf[string, chan Response]
|
||||||
@@ -28,7 +29,7 @@ type BunkerClient struct {
|
|||||||
onAuth func(string)
|
onAuth func(string)
|
||||||
|
|
||||||
// memoized
|
// memoized
|
||||||
getPublicKeyResponse string
|
getPublicKeyResponse nostr.PubKey
|
||||||
|
|
||||||
// SkipSignatureCheck can be set if you don't want to double-check incoming signatures
|
// SkipSignatureCheck can be set if you don't want to double-check incoming signatures
|
||||||
SkipSignatureCheck bool
|
SkipSignatureCheck bool
|
||||||
@@ -40,7 +41,7 @@ func ConnectBunker(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
clientSecretKey nostr.PubKey,
|
clientSecretKey nostr.PubKey,
|
||||||
bunkerURLOrNIP05 string,
|
bunkerURLOrNIP05 string,
|
||||||
pool *nostr.SimplePool,
|
pool *nostr.Pool,
|
||||||
onAuth func(string),
|
onAuth func(string),
|
||||||
) (*BunkerClient, error) {
|
) (*BunkerClient, error) {
|
||||||
parsed, err := url.Parse(bunkerURLOrNIP05)
|
parsed, err := url.Parse(bunkerURLOrNIP05)
|
||||||
@@ -79,7 +80,7 @@ func ConnectBunker(
|
|||||||
pool,
|
pool,
|
||||||
onAuth,
|
onAuth,
|
||||||
)
|
)
|
||||||
_, err = bunker.RPC(ctx, "connect", []string{targetPublicKey, secret})
|
_, err = bunker.RPC(ctx, "connect", []string{hex.EncodeToString(targetPublicKey[:]), secret})
|
||||||
return bunker, err
|
return bunker, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,11 +89,11 @@ func NewBunker(
|
|||||||
clientSecretKey [32]byte,
|
clientSecretKey [32]byte,
|
||||||
targetPublicKey nostr.PubKey,
|
targetPublicKey nostr.PubKey,
|
||||||
relays []string,
|
relays []string,
|
||||||
pool *nostr.SimplePool,
|
pool *nostr.Pool,
|
||||||
onAuth func(string),
|
onAuth func(string),
|
||||||
) *BunkerClient {
|
) *BunkerClient {
|
||||||
if pool == nil {
|
if pool == nil {
|
||||||
pool = nostr.NewSimplePool(ctx)
|
pool = nostr.NewPool(nostr.PoolOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
clientPublicKey := nostr.GetPublicKey(clientSecretKey)
|
clientPublicKey := nostr.GetPublicKey(clientSecretKey)
|
||||||
@@ -113,11 +114,13 @@ func NewBunker(
|
|||||||
go func() {
|
go func() {
|
||||||
now := nostr.Now()
|
now := nostr.Now()
|
||||||
events := pool.SubscribeMany(ctx, relays, nostr.Filter{
|
events := pool.SubscribeMany(ctx, relays, nostr.Filter{
|
||||||
Tags: nostr.TagMap{"p": []string{clientPublicKey}},
|
Tags: nostr.TagMap{"p": []string{clientPublicKey.Hex()}},
|
||||||
Kinds: []int{nostr.KindNostrConnect},
|
Kinds: []uint16{nostr.KindNostrConnect},
|
||||||
Since: &now,
|
Since: &now,
|
||||||
LimitZero: true,
|
LimitZero: true,
|
||||||
}, nostr.WithLabel("bunker46client"))
|
}, nostr.SubscriptionOptions{
|
||||||
|
Label: "bunker46client",
|
||||||
|
})
|
||||||
for ie := range events {
|
for ie := range events {
|
||||||
if ie.Kind != nostr.KindNostrConnect {
|
if ie.Kind != nostr.KindNostrConnect {
|
||||||
continue
|
continue
|
||||||
@@ -125,12 +128,9 @@ func NewBunker(
|
|||||||
|
|
||||||
var resp Response
|
var resp Response
|
||||||
plain, err := nip44.Decrypt(ie.Content, conversationKey)
|
plain, err := nip44.Decrypt(ie.Content, conversationKey)
|
||||||
if err != nil {
|
|
||||||
plain, err = nip04.Decrypt(ie.Content, sharedSecret)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(plain), &resp)
|
err = json.Unmarshal([]byte(plain), &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -164,13 +164,22 @@ func (bunker *BunkerClient) Ping(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bunker *BunkerClient) GetPublicKey(ctx context.Context) (string, error) {
|
func (bunker *BunkerClient) GetPublicKey(ctx context.Context) (nostr.PubKey, error) {
|
||||||
if bunker.getPublicKeyResponse != "" {
|
if bunker.getPublicKeyResponse != nostr.ZeroPK {
|
||||||
return bunker.getPublicKeyResponse, nil
|
return bunker.getPublicKeyResponse, nil
|
||||||
}
|
}
|
||||||
resp, err := bunker.RPC(ctx, "get_public_key", []string{})
|
resp, err := bunker.RPC(ctx, "get_public_key", []string{})
|
||||||
bunker.getPublicKeyResponse = resp
|
if err != nil {
|
||||||
return resp, err
|
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 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = easyjson.Unmarshal([]byte(resp), evt)
|
err = easyjson.Unmarshal(unsafe.Slice(unsafe.StringData(resp), len(resp)), evt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -188,7 +197,7 @@ func (bunker *BunkerClient) SignEvent(ctx context.Context, evt *nostr.Event) err
|
|||||||
if ok := evt.CheckID(); !ok {
|
if ok := evt.CheckID(); !ok {
|
||||||
return fmt.Errorf("sign_event response from bunker has invalid id")
|
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")
|
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(
|
func (bunker *BunkerClient) NIP44Encrypt(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
targetPublicKey string,
|
targetPublicKey nostr.PubKey,
|
||||||
plaintext string,
|
plaintext string,
|
||||||
) (string, error) {
|
) (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(
|
func (bunker *BunkerClient) NIP44Decrypt(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
targetPublicKey string,
|
targetPublicKey nostr.PubKey,
|
||||||
ciphertext string,
|
ciphertext string,
|
||||||
) (string, error) {
|
) (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(
|
func (bunker *BunkerClient) NIP04Encrypt(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
targetPublicKey string,
|
targetPublicKey nostr.PubKey,
|
||||||
plaintext string,
|
plaintext string,
|
||||||
) (string, error) {
|
) (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(
|
func (bunker *BunkerClient) NIP04Decrypt(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
targetPublicKey string,
|
targetPublicKey nostr.PubKey,
|
||||||
ciphertext string,
|
ciphertext string,
|
||||||
) (string, error) {
|
) (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) {
|
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,
|
Content: content,
|
||||||
CreatedAt: nostr.Now(),
|
CreatedAt: nostr.Now(),
|
||||||
Kind: nostr.KindNostrConnect,
|
Kind: nostr.KindNostrConnect,
|
||||||
Tags: nostr.Tags{{"p", bunker.target}},
|
Tags: nostr.Tags{{"p", bunker.target.Hex()}},
|
||||||
}
|
}
|
||||||
if err := evt.Sign(bunker.clientSecretKey); err != nil {
|
if err := evt.Sign(bunker.clientSecretKey); err != nil {
|
||||||
return "", fmt.Errorf("failed to sign request event: %w", err)
|
return "", fmt.Errorf("failed to sign request event: %w", err)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package nip49
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
@@ -21,15 +20,7 @@ const (
|
|||||||
ClientDoesNotTrackThisData KeySecurityByte = 0x02
|
ClientDoesNotTrackThisData KeySecurityByte = 0x02
|
||||||
)
|
)
|
||||||
|
|
||||||
func Encrypt(secretKey string, 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) {
|
||||||
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) {
|
|
||||||
salt := make([]byte, 16)
|
salt := make([]byte, 16)
|
||||||
if _, err := rand.Read(salt); err != nil {
|
if _, err := rand.Read(salt); err != nil {
|
||||||
return "", fmt.Errorf("failed to read salt: %w", err)
|
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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to start xchacha20poly1305: %w", err)
|
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)
|
copy(concat[2+16+24+1:], ciphertext)
|
||||||
|
|
||||||
bits5, err := bech32.ConvertBits(concat, 8, 5, true)
|
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)
|
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)
|
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) {
|
func DecryptToBytes(bech32string string, password string) (secretKey []byte, err error) {
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
||||||
"github.com/mailru/easyjson"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/nip44"
|
"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
|
// 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'.
|
// signs that (after potentially applying a modify function, which can be nil otherwise), yielding a 'gift-wrap'.
|
||||||
func GiftWrap(
|
func GiftWrap(
|
||||||
rumor nostr.Event,
|
rumor nostr.Event,
|
||||||
recipient string,
|
recipient nostr.PubKey,
|
||||||
encrypt func(plaintext string) (string, error),
|
encrypt func(plaintext string) (string, error),
|
||||||
sign func(*nostr.Event) error,
|
sign func(*nostr.Event) error,
|
||||||
modify func(*nostr.Event),
|
modify func(*nostr.Event),
|
||||||
) (nostr.Event, error) {
|
) (nostr.Event, error) {
|
||||||
rumor.Sig = ""
|
rumor.Sig = [64]byte{}
|
||||||
|
|
||||||
rumorCiphertext, err := encrypt(rumor.String())
|
rumorCiphertext, err := encrypt(rumor.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,7 +51,7 @@ func GiftWrap(
|
|||||||
Content: sealCiphertext,
|
Content: sealCiphertext,
|
||||||
CreatedAt: nostr.Now() - nostr.Timestamp(60*rand.Int63n(600) /* up to 6 hours in the past */),
|
CreatedAt: nostr.Now() - nostr.Timestamp(60*rand.Int63n(600) /* up to 6 hours in the past */),
|
||||||
Tags: nostr.Tags{
|
Tags: nostr.Tags{
|
||||||
nostr.Tag{"p", recipient},
|
nostr.Tag{"p", recipient.Hex()},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if modify != nil {
|
if modify != nil {
|
||||||
@@ -66,7 +66,7 @@ func GiftWrap(
|
|||||||
|
|
||||||
func GiftUnwrap(
|
func GiftUnwrap(
|
||||||
gw nostr.Event,
|
gw nostr.Event,
|
||||||
decrypt func(otherpubkey, ciphertext string) (string, error),
|
decrypt func(otherpubkey nostr.PubKey, ciphertext string) (string, error),
|
||||||
) (rumor nostr.Event, err error) {
|
) (rumor nostr.Event, err error) {
|
||||||
jseal, err := decrypt(gw.PubKey, gw.Content)
|
jseal, err := decrypt(gw.PubKey, gw.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -79,7 +79,7 @@ func GiftUnwrap(
|
|||||||
return rumor, fmt.Errorf("seal is invalid json: %w", err)
|
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")
|
return rumor, fmt.Errorf("seal signature is invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ type Wallet struct {
|
|||||||
func LoadWallet(
|
func LoadWallet(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
kr nostr.Keyer,
|
kr nostr.Keyer,
|
||||||
pool *nostr.SimplePool,
|
pool *nostr.Pool,
|
||||||
relays []string,
|
relays []string,
|
||||||
) *Wallet {
|
) *Wallet {
|
||||||
return loadWalletFromPool(ctx, kr, pool, relays, false)
|
return loadWalletFromPool(ctx, kr, pool, relays, false)
|
||||||
@@ -59,7 +59,7 @@ func LoadWallet(
|
|||||||
func LoadWalletWithHistory(
|
func LoadWalletWithHistory(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
kr nostr.Keyer,
|
kr nostr.Keyer,
|
||||||
pool *nostr.SimplePool,
|
pool *nostr.Pool,
|
||||||
relays []string,
|
relays []string,
|
||||||
) *Wallet {
|
) *Wallet {
|
||||||
return loadWalletFromPool(ctx, kr, pool, relays, true)
|
return loadWalletFromPool(ctx, kr, pool, relays, true)
|
||||||
@@ -68,7 +68,7 @@ func LoadWalletWithHistory(
|
|||||||
func loadWalletFromPool(
|
func loadWalletFromPool(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
kr nostr.Keyer,
|
kr nostr.Keyer,
|
||||||
pool *nostr.SimplePool,
|
pool *nostr.Pool,
|
||||||
relays []string,
|
relays []string,
|
||||||
withHistory bool,
|
withHistory bool,
|
||||||
) *Wallet {
|
) *Wallet {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func SendNutzap(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
kr nostr.Keyer,
|
kr nostr.Keyer,
|
||||||
w *nip60.Wallet,
|
w *nip60.Wallet,
|
||||||
pool *nostr.SimplePool,
|
pool *nostr.Pool,
|
||||||
targetUserPublickey string,
|
targetUserPublickey string,
|
||||||
getUserReadRelays func(context.Context, string, int) []string,
|
getUserReadRelays func(context.Context, string, int) []string,
|
||||||
relays []string,
|
relays []string,
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ func FetchIDsOnly(
|
|||||||
url string,
|
url string,
|
||||||
filter nostr.Filter,
|
filter nostr.Filter,
|
||||||
) (<-chan nostr.ID, error) {
|
) (<-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)
|
neg := negentropy.New(empty.Empty{}, 1024*1024)
|
||||||
result := make(chan error)
|
result := make(chan error)
|
||||||
|
|
||||||
var r *nostr.Relay
|
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)
|
envelope := ParseNegMessage(data)
|
||||||
if envelope == nil {
|
if envelope == nil {
|
||||||
return
|
return
|
||||||
@@ -44,7 +44,7 @@ func FetchIDsOnly(
|
|||||||
r.Write(msgb)
|
r.Write(msgb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,15 @@ func DecodeRequest(req Request) (MethodParams, error) {
|
|||||||
if len(req.Params) == 0 {
|
if len(req.Params) == 0 {
|
||||||
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
|
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
|
||||||
}
|
}
|
||||||
pk, ok := req.Params[0].(string)
|
pkh, ok := req.Params[0].(string)
|
||||||
if !ok || !nostr.IsValidPublicKey(pk) {
|
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)
|
return nil, fmt.Errorf("invalid pubkey param for '%s'", req.Method)
|
||||||
}
|
}
|
||||||
|
|
||||||
var reason string
|
var reason string
|
||||||
if len(req.Params) >= 2 {
|
if len(req.Params) >= 2 {
|
||||||
reason, _ = req.Params[1].(string)
|
reason, _ = req.Params[1].(string)
|
||||||
@@ -31,10 +36,15 @@ func DecodeRequest(req Request) (MethodParams, error) {
|
|||||||
if len(req.Params) == 0 {
|
if len(req.Params) == 0 {
|
||||||
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
|
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
|
||||||
}
|
}
|
||||||
pk, ok := req.Params[0].(string)
|
pkh, ok := req.Params[0].(string)
|
||||||
if !ok || !nostr.IsValidPublicKey(pk) {
|
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)
|
return nil, fmt.Errorf("invalid pubkey param for '%s'", req.Method)
|
||||||
}
|
}
|
||||||
|
|
||||||
var reason string
|
var reason string
|
||||||
if len(req.Params) >= 2 {
|
if len(req.Params) >= 2 {
|
||||||
reason, _ = req.Params[1].(string)
|
reason, _ = req.Params[1].(string)
|
||||||
@@ -48,10 +58,15 @@ func DecodeRequest(req Request) (MethodParams, error) {
|
|||||||
if len(req.Params) == 0 {
|
if len(req.Params) == 0 {
|
||||||
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
|
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
|
||||||
}
|
}
|
||||||
id, ok := req.Params[0].(string)
|
idh, ok := req.Params[0].(string)
|
||||||
if !ok || !nostr.IsValid32ByteHex(id) {
|
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)
|
return nil, fmt.Errorf("invalid id param for '%s'", req.Method)
|
||||||
}
|
}
|
||||||
|
|
||||||
var reason string
|
var reason string
|
||||||
if len(req.Params) >= 2 {
|
if len(req.Params) >= 2 {
|
||||||
reason, _ = req.Params[1].(string)
|
reason, _ = req.Params[1].(string)
|
||||||
@@ -61,10 +76,15 @@ func DecodeRequest(req Request) (MethodParams, error) {
|
|||||||
if len(req.Params) == 0 {
|
if len(req.Params) == 0 {
|
||||||
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
|
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
|
||||||
}
|
}
|
||||||
id, ok := req.Params[0].(string)
|
idh, ok := req.Params[0].(string)
|
||||||
if !ok || !nostr.IsValid32ByteHex(id) {
|
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)
|
return nil, fmt.Errorf("invalid id param for '%s'", req.Method)
|
||||||
}
|
}
|
||||||
|
|
||||||
var reason string
|
var reason string
|
||||||
if len(req.Params) >= 2 {
|
if len(req.Params) >= 2 {
|
||||||
reason, _ = req.Params[1].(string)
|
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)
|
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)
|
allowedMethods := req.Params[1].([]string)
|
||||||
|
|
||||||
return GrantAdmin{
|
return GrantAdmin{
|
||||||
Pubkey: pubkey,
|
Pubkey: pk,
|
||||||
AllowMethods: allowedMethods,
|
AllowMethods: allowedMethods,
|
||||||
}, nil
|
}, nil
|
||||||
case "revokeadmin":
|
case "revokeadmin":
|
||||||
@@ -161,11 +189,19 @@ func DecodeRequest(req Request) (MethodParams, error) {
|
|||||||
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
|
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)
|
disallowedMethods := req.Params[1].([]string)
|
||||||
|
|
||||||
return RevokeAdmin{
|
return RevokeAdmin{
|
||||||
Pubkey: pubkey,
|
Pubkey: pk,
|
||||||
DisallowMethods: disallowedMethods,
|
DisallowMethods: disallowedMethods,
|
||||||
}, nil
|
}, nil
|
||||||
case "stats":
|
case "stats":
|
||||||
@@ -210,7 +246,7 @@ type SupportedMethods struct{}
|
|||||||
func (SupportedMethods) MethodName() string { return "supportedmethods" }
|
func (SupportedMethods) MethodName() string { return "supportedmethods" }
|
||||||
|
|
||||||
type BanPubKey struct {
|
type BanPubKey struct {
|
||||||
PubKey string
|
PubKey nostr.PubKey
|
||||||
Reason string
|
Reason string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +257,7 @@ type ListBannedPubKeys struct{}
|
|||||||
func (ListBannedPubKeys) MethodName() string { return "listbannedpubkeys" }
|
func (ListBannedPubKeys) MethodName() string { return "listbannedpubkeys" }
|
||||||
|
|
||||||
type AllowPubKey struct {
|
type AllowPubKey struct {
|
||||||
PubKey string
|
PubKey nostr.PubKey
|
||||||
Reason string
|
Reason string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,14 +272,14 @@ type ListEventsNeedingModeration struct{}
|
|||||||
func (ListEventsNeedingModeration) MethodName() string { return "listeventsneedingmoderation" }
|
func (ListEventsNeedingModeration) MethodName() string { return "listeventsneedingmoderation" }
|
||||||
|
|
||||||
type AllowEvent struct {
|
type AllowEvent struct {
|
||||||
ID string
|
ID nostr.ID
|
||||||
Reason string
|
Reason string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AllowEvent) MethodName() string { return "allowevent" }
|
func (AllowEvent) MethodName() string { return "allowevent" }
|
||||||
|
|
||||||
type BanEvent struct {
|
type BanEvent struct {
|
||||||
ID string
|
ID nostr.ID
|
||||||
Reason string
|
Reason string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,14 +350,14 @@ type ListDisallowedKinds struct{}
|
|||||||
func (ListDisallowedKinds) MethodName() string { return "listdisallowedkinds" }
|
func (ListDisallowedKinds) MethodName() string { return "listdisallowedkinds" }
|
||||||
|
|
||||||
type GrantAdmin struct {
|
type GrantAdmin struct {
|
||||||
Pubkey string
|
Pubkey nostr.PubKey
|
||||||
AllowMethods []string
|
AllowMethods []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (GrantAdmin) MethodName() string { return "grantadmin" }
|
func (GrantAdmin) MethodName() string { return "grantadmin" }
|
||||||
|
|
||||||
type RevokeAdmin struct {
|
type RevokeAdmin struct {
|
||||||
Pubkey string
|
Pubkey nostr.PubKey
|
||||||
DisallowMethods []string
|
DisallowMethods []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (pool *SimplePool) PaginatorWithInterval(
|
func (pool *Pool) PaginatorWithInterval(
|
||||||
interval time.Duration,
|
interval time.Duration,
|
||||||
) func(ctx context.Context, urls []string, filter Filter, opts ...SubscriptionOption) chan RelayEvent {
|
) 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 {
|
return func(ctx context.Context, urls []string, filter Filter, opts ...SubscriptionOption) chan RelayEvent {
|
||||||
|
|||||||
265
pool.go
265
pool.go
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -20,22 +19,22 @@ const (
|
|||||||
seenAlreadyDropTick = time.Minute
|
seenAlreadyDropTick = time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
// SimplePool manages connections to multiple relays, ensures they are reopened when necessary and not duplicated.
|
// Pool manages connections to multiple relays, ensures they are reopened when necessary and not duplicated.
|
||||||
type SimplePool struct {
|
type Pool struct {
|
||||||
Relays *xsync.MapOf[string, *Relay]
|
Relays *xsync.MapOf[string, *Relay]
|
||||||
Context context.Context
|
Context context.Context
|
||||||
|
|
||||||
authHandler func(context.Context, RelayEvent) error
|
authHandler func(context.Context, *Event) error
|
||||||
cancel context.CancelCauseFunc
|
cancel context.CancelCauseFunc
|
||||||
|
|
||||||
eventMiddleware func(RelayEvent)
|
eventMiddleware func(RelayEvent)
|
||||||
duplicateMiddleware func(relay string, id ID)
|
duplicateMiddleware func(relay string, id ID)
|
||||||
queryMiddleware func(relay string, pubkey PubKey, kind uint16)
|
queryMiddleware func(relay string, pubkey PubKey, kind uint16)
|
||||||
|
relayOptions RelayOptions
|
||||||
|
|
||||||
// custom things not often used
|
// custom things not often used
|
||||||
penaltyBoxMu sync.Mutex
|
penaltyBoxMu sync.Mutex
|
||||||
penaltyBox map[string][2]float64
|
penaltyBox map[string][2]float64
|
||||||
relayOptions []RelayOption
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DirectedFilter combines a Filter with a specific relay URL.
|
// DirectedFilter combines a Filter with a specific relay URL.
|
||||||
@@ -44,64 +43,58 @@ type DirectedFilter struct {
|
|||||||
Relay string
|
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) }
|
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.
|
// NewPool creates a new Pool with the given context and options.
|
||||||
type PoolOption interface {
|
func NewPool(opts PoolOptions) *Pool {
|
||||||
ApplyPoolOption(*SimplePool)
|
ctx, cancel := context.WithCancelCause(context.Background())
|
||||||
}
|
|
||||||
|
|
||||||
// NewSimplePool creates a new SimplePool with the given context and options.
|
pool := &Pool{
|
||||||
func NewSimplePool(ctx context.Context, opts ...PoolOption) *SimplePool {
|
|
||||||
ctx, cancel := context.WithCancelCause(ctx)
|
|
||||||
|
|
||||||
pool := &SimplePool{
|
|
||||||
Relays: xsync.NewMapOf[string, *Relay](),
|
Relays: xsync.NewMapOf[string, *Relay](),
|
||||||
|
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
|
|
||||||
|
authHandler: opts.AuthHandler,
|
||||||
|
eventMiddleware: opts.EventMiddleware,
|
||||||
|
duplicateMiddleware: opts.DuplicateMiddleware,
|
||||||
|
queryMiddleware: opts.AuthorKindQueryMiddleware,
|
||||||
|
relayOptions: opts.RelayOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
if opts.PenaltyBox {
|
||||||
opt.ApplyPoolOption(pool)
|
go pool.startPenaltyBox()
|
||||||
}
|
}
|
||||||
|
|
||||||
return pool
|
return pool
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithRelayOptions sets options that will be used on every relay instance created by this pool.
|
type PoolOptions struct {
|
||||||
func WithRelayOptions(ropts ...RelayOption) withRelayOptionsOpt {
|
// AuthHandler, if given, must be a function that signs the auth event when called.
|
||||||
return ropts
|
// 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 (pool *Pool) startPenaltyBox() {
|
||||||
|
|
||||||
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) {
|
|
||||||
pool.penaltyBox = make(map[string][2]float64)
|
pool.penaltyBox = make(map[string][2]float64)
|
||||||
go func() {
|
go func() {
|
||||||
sleep := 30.0
|
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.
|
// EnsureRelay ensures that a relay connection exists and is active.
|
||||||
// If the relay is not connected, it attempts to connect.
|
// 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)
|
nm := NormalizeURL(url)
|
||||||
defer namedLock(nm)()
|
defer namedLock(nm)()
|
||||||
|
|
||||||
@@ -190,7 +154,7 @@ func (pool *SimplePool) EnsureRelay(url string) (*Relay, error) {
|
|||||||
)
|
)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
relay = NewRelay(context.Background(), url, pool.relayOptions...)
|
relay = NewRelay(pool.Context, url, pool.relayOptions)
|
||||||
if err := relay.Connect(ctx); err != nil {
|
if err := relay.Connect(ctx); err != nil {
|
||||||
if pool.penaltyBox != nil {
|
if pool.penaltyBox != nil {
|
||||||
// putting relay in penalty box
|
// 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.
|
// 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))
|
ch := make(chan PublishResult, len(urls))
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
@@ -235,9 +199,7 @@ func (pool *SimplePool) PublishMany(ctx context.Context, urls []string, evt Even
|
|||||||
ch <- PublishResult{nil, url, relay}
|
ch <- PublishResult{nil, url, relay}
|
||||||
} else if strings.HasPrefix(err.Error(), "msg: auth-required:") && pool.authHandler != nil {
|
} else if strings.HasPrefix(err.Error(), "msg: auth-required:") && pool.authHandler != nil {
|
||||||
// try to authenticate if we can
|
// try to authenticate if we can
|
||||||
if authErr := relay.Auth(ctx, func(event *Event) error {
|
if authErr := relay.Auth(ctx, pool.authHandler); authErr == nil {
|
||||||
return pool.authHandler(ctx, RelayEvent{Event: event, Relay: relay})
|
|
||||||
}); authErr == nil {
|
|
||||||
if err := relay.Publish(ctx, evt); err == nil {
|
if err := relay.Publish(ctx, evt); err == nil {
|
||||||
// success after auth
|
// success after auth
|
||||||
ch <- PublishResult{nil, url, relay}
|
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
|
// 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.
|
// 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,
|
ctx context.Context,
|
||||||
urls []string,
|
urls []string,
|
||||||
filter Filter,
|
filter Filter,
|
||||||
opts ...SubscriptionOption,
|
opts SubscriptionOptions,
|
||||||
) chan RelayEvent {
|
) 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
|
// FetchMany opens a subscription, much like SubscribeMany, but it ends as soon as all Relays
|
||||||
// return an EOSE message.
|
// return an EOSE message.
|
||||||
func (pool *SimplePool) FetchMany(
|
func (pool *Pool) FetchMany(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
urls []string,
|
urls []string,
|
||||||
filter Filter,
|
filter Filter,
|
||||||
opts ...SubscriptionOption,
|
opts SubscriptionOptions,
|
||||||
) chan RelayEvent {
|
) 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
|
// SubscribeManyNotifyEOSE is like SubscribeMany, but takes a channel that is closed when
|
||||||
// all subscriptions have received an EOSE
|
// all subscriptions have received an EOSE
|
||||||
func (pool *SimplePool) SubscribeManyNotifyEOSE(
|
func (pool *Pool) SubscribeManyNotifyEOSE(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
urls []string,
|
urls []string,
|
||||||
filter Filter,
|
filter Filter,
|
||||||
eoseChan chan struct{},
|
eoseChan chan struct{},
|
||||||
opts ...SubscriptionOption,
|
opts SubscriptionOptions,
|
||||||
) chan RelayEvent {
|
) chan RelayEvent {
|
||||||
return pool.subMany(ctx, urls, filter, eoseChan, opts...)
|
return pool.subMany(ctx, urls, filter, eoseChan, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReplaceableKey struct {
|
type ReplaceableKey struct {
|
||||||
@@ -304,21 +276,21 @@ type ReplaceableKey struct {
|
|||||||
|
|
||||||
// FetchManyReplaceable is like FetchMany, but deduplicates replaceable and addressable events and returns
|
// FetchManyReplaceable is like FetchMany, but deduplicates replaceable and addressable events and returns
|
||||||
// only the latest for each "d" tag.
|
// only the latest for each "d" tag.
|
||||||
func (pool *SimplePool) FetchManyReplaceable(
|
func (pool *Pool) FetchManyReplaceable(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
urls []string,
|
urls []string,
|
||||||
filter Filter,
|
filter Filter,
|
||||||
opts ...SubscriptionOption,
|
opts SubscriptionOptions,
|
||||||
) *xsync.MapOf[ReplaceableKey, *Event] {
|
) *xsync.MapOf[ReplaceableKey, Event] {
|
||||||
ctx, cancel := context.WithCancelCause(ctx)
|
ctx, cancel := context.WithCancelCause(ctx)
|
||||||
|
|
||||||
results := xsync.NewMapOf[ReplaceableKey, *Event]()
|
results := xsync.NewMapOf[ReplaceableKey, Event]()
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
wg.Add(len(urls))
|
wg.Add(len(urls))
|
||||||
|
|
||||||
seenAlreadyLatest := xsync.NewMapOf[ReplaceableKey, Timestamp]()
|
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
|
updated := false
|
||||||
seenAlreadyLatest.Compute(rk, func(latest Timestamp, _ bool) (newValue Timestamp, delete bool) {
|
seenAlreadyLatest.Compute(rk, func(latest Timestamp, _ bool) (newValue Timestamp, delete bool) {
|
||||||
if ts > latest {
|
if ts > latest {
|
||||||
@@ -328,7 +300,7 @@ func (pool *SimplePool) FetchManyReplaceable(
|
|||||||
return latest, false // the one we had was already more recent
|
return latest, false // the one we had was already more recent
|
||||||
})
|
})
|
||||||
return updated
|
return updated
|
||||||
}))
|
}
|
||||||
|
|
||||||
for _, url := range urls {
|
for _, url := range urls {
|
||||||
go func(nm string) {
|
go func(nm string) {
|
||||||
@@ -353,7 +325,7 @@ func (pool *SimplePool) FetchManyReplaceable(
|
|||||||
hasAuthed := false
|
hasAuthed := false
|
||||||
|
|
||||||
subscribe:
|
subscribe:
|
||||||
sub, err := relay.Subscribe(ctx, filter, opts...)
|
sub, err := relay.Subscribe(ctx, filter, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugLogf("error subscribing to %s with %v: %s", relay, filter, err)
|
debugLogf("error subscribing to %s with %v: %s", relay, filter, err)
|
||||||
return
|
return
|
||||||
@@ -368,9 +340,7 @@ func (pool *SimplePool) FetchManyReplaceable(
|
|||||||
case reason := <-sub.ClosedReason:
|
case reason := <-sub.ClosedReason:
|
||||||
if strings.HasPrefix(reason, "auth-required:") && pool.authHandler != nil && !hasAuthed {
|
if strings.HasPrefix(reason, "auth-required:") && pool.authHandler != nil && !hasAuthed {
|
||||||
// relay is requesting auth. if we can we will perform auth and try again
|
// relay is requesting auth. if we can we will perform auth and try again
|
||||||
err := relay.Auth(ctx, func(event *Event) error {
|
err := relay.Auth(ctx, pool.authHandler)
|
||||||
return pool.authHandler(ctx, RelayEvent{Event: event, Relay: relay})
|
|
||||||
})
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
hasAuthed = true // so we don't keep doing AUTH again and again
|
hasAuthed = true // so we don't keep doing AUTH again and again
|
||||||
goto subscribe
|
goto subscribe
|
||||||
@@ -401,12 +371,12 @@ func (pool *SimplePool) FetchManyReplaceable(
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pool *SimplePool) subMany(
|
func (pool *Pool) subMany(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
urls []string,
|
urls []string,
|
||||||
filter Filter,
|
filter Filter,
|
||||||
eoseChan chan struct{},
|
eoseChan chan struct{},
|
||||||
opts ...SubscriptionOption,
|
opts SubscriptionOptions,
|
||||||
) chan RelayEvent {
|
) chan RelayEvent {
|
||||||
ctx, cancel := context.WithCancelCause(ctx)
|
ctx, cancel := context.WithCancelCause(ctx)
|
||||||
_ = cancel // do this so `go vet` will stop complaining
|
_ = 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 := xsync.NewCounter()
|
||||||
pending.Add(int64(len(urls)))
|
pending.Add(int64(len(urls)))
|
||||||
for i, url := range urls {
|
for i, url := range urls {
|
||||||
@@ -485,15 +463,7 @@ func (pool *SimplePool) subMany(
|
|||||||
hasAuthed = false
|
hasAuthed = false
|
||||||
|
|
||||||
subscribe:
|
subscribe:
|
||||||
sub, err = relay.Subscribe(ctx, filter, append(opts,
|
sub, err = relay.Subscribe(ctx, filter, opts)
|
||||||
WithCheckDuplicate(func(id ID, relay string) bool {
|
|
||||||
_, exists := seenAlready.Load(id)
|
|
||||||
if exists && pool.duplicateMiddleware != nil {
|
|
||||||
pool.duplicateMiddleware(relay, id)
|
|
||||||
}
|
|
||||||
return exists
|
|
||||||
}),
|
|
||||||
)...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugLogf("%s reconnecting because subscription died\n", nm)
|
debugLogf("%s reconnecting because subscription died\n", nm)
|
||||||
goto reconnect
|
goto reconnect
|
||||||
@@ -546,9 +516,7 @@ func (pool *SimplePool) subMany(
|
|||||||
case reason := <-sub.ClosedReason:
|
case reason := <-sub.ClosedReason:
|
||||||
if strings.HasPrefix(reason, "auth-required:") && pool.authHandler != nil && !hasAuthed {
|
if strings.HasPrefix(reason, "auth-required:") && pool.authHandler != nil && !hasAuthed {
|
||||||
// relay is requesting auth. if we can we will perform auth and try again
|
// relay is requesting auth. if we can we will perform auth and try again
|
||||||
err := relay.Auth(ctx, func(event *Event) error {
|
err := relay.Auth(ctx, pool.authHandler)
|
||||||
return pool.authHandler(ctx, RelayEvent{Event: event, Relay: relay})
|
|
||||||
})
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
hasAuthed = true // so we don't keep doing AUTH again and again
|
hasAuthed = true // so we don't keep doing AUTH again and again
|
||||||
goto subscribe
|
goto subscribe
|
||||||
@@ -575,32 +543,11 @@ func (pool *SimplePool) subMany(
|
|||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: SubManyEose is deprecated: use FetchMany instead.
|
func (pool *Pool) subManyEoseNonOverwriteCheckDuplicate(
|
||||||
func (pool *SimplePool) SubManyEose(
|
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
urls []string,
|
urls []string,
|
||||||
filter Filter,
|
filter Filter,
|
||||||
opts ...SubscriptionOption,
|
opts SubscriptionOptions,
|
||||||
) 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,
|
|
||||||
) chan RelayEvent {
|
) chan RelayEvent {
|
||||||
ctx, cancel := context.WithCancelCause(ctx)
|
ctx, cancel := context.WithCancelCause(ctx)
|
||||||
|
|
||||||
@@ -608,8 +555,6 @@ func (pool *SimplePool) subManyEoseNonOverwriteCheckDuplicate(
|
|||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
wg.Add(len(urls))
|
wg.Add(len(urls))
|
||||||
|
|
||||||
opts = append(opts, wcd)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
// this will happen when all subscriptions get an eose (or when they die)
|
// this will happen when all subscriptions get an eose (or when they die)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -640,7 +585,7 @@ func (pool *SimplePool) subManyEoseNonOverwriteCheckDuplicate(
|
|||||||
hasAuthed := false
|
hasAuthed := false
|
||||||
|
|
||||||
subscribe:
|
subscribe:
|
||||||
sub, err := relay.Subscribe(ctx, filter, opts...)
|
sub, err := relay.Subscribe(ctx, filter, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugLogf("error subscribing to %s with %v: %s", relay, filter, err)
|
debugLogf("error subscribing to %s with %v: %s", relay, filter, err)
|
||||||
return
|
return
|
||||||
@@ -655,9 +600,7 @@ func (pool *SimplePool) subManyEoseNonOverwriteCheckDuplicate(
|
|||||||
case reason := <-sub.ClosedReason:
|
case reason := <-sub.ClosedReason:
|
||||||
if strings.HasPrefix(reason, "auth-required:") && pool.authHandler != nil && !hasAuthed {
|
if strings.HasPrefix(reason, "auth-required:") && pool.authHandler != nil && !hasAuthed {
|
||||||
// relay is requesting auth. if we can we will perform auth and try again
|
// relay is requesting auth. if we can we will perform auth and try again
|
||||||
err := relay.Auth(ctx, func(event *Event) error {
|
err := relay.Auth(ctx, pool.authHandler)
|
||||||
return pool.authHandler(ctx, RelayEvent{Event: event, Relay: relay})
|
|
||||||
})
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
hasAuthed = true // so we don't keep doing AUTH again and again
|
hasAuthed = true // so we don't keep doing AUTH again and again
|
||||||
goto subscribe
|
goto subscribe
|
||||||
@@ -689,11 +632,11 @@ func (pool *SimplePool) subManyEoseNonOverwriteCheckDuplicate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CountMany aggregates count results from multiple relays using NIP-45 HyperLogLog
|
// CountMany aggregates count results from multiple relays using NIP-45 HyperLogLog
|
||||||
func (pool *SimplePool) CountMany(
|
func (pool *Pool) CountMany(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
urls []string,
|
urls []string,
|
||||||
filter Filter,
|
filter Filter,
|
||||||
opts []SubscriptionOption,
|
opts SubscriptionOptions,
|
||||||
) int {
|
) int {
|
||||||
hll := hyperloglog.New(0) // offset is irrelevant here
|
hll := hyperloglog.New(0) // offset is irrelevant here
|
||||||
|
|
||||||
@@ -706,7 +649,7 @@ func (pool *SimplePool) CountMany(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ce, err := relay.countInternal(ctx, filter, opts...)
|
ce, err := relay.countInternal(ctx, filter, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -722,14 +665,14 @@ func (pool *SimplePool) CountMany(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// QuerySingle returns the first event returned by the first relay, cancels everything else.
|
// QuerySingle returns the first event returned by the first relay, cancels everything else.
|
||||||
func (pool *SimplePool) QuerySingle(
|
func (pool *Pool) QuerySingle(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
urls []string,
|
urls []string,
|
||||||
filter Filter,
|
filter Filter,
|
||||||
opts ...SubscriptionOption,
|
opts SubscriptionOptions,
|
||||||
) *RelayEvent {
|
) *RelayEvent {
|
||||||
ctx, cancel := context.WithCancelCause(ctx)
|
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"))
|
cancel(errors.New("got the first event and ended successfully"))
|
||||||
return &ievt
|
return &ievt
|
||||||
}
|
}
|
||||||
@@ -738,28 +681,30 @@ func (pool *SimplePool) QuerySingle(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BatchedSubManyEose performs batched subscriptions to multiple relays with different filters.
|
// BatchedSubManyEose performs batched subscriptions to multiple relays with different filters.
|
||||||
func (pool *SimplePool) BatchedSubManyEose(
|
func (pool *Pool) BatchedSubManyEose(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
dfs []DirectedFilter,
|
dfs []DirectedFilter,
|
||||||
opts ...SubscriptionOption,
|
opts SubscriptionOptions,
|
||||||
) chan RelayEvent {
|
) chan RelayEvent {
|
||||||
res := make(chan RelayEvent)
|
res := make(chan RelayEvent)
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
wg.Add(len(dfs))
|
wg.Add(len(dfs))
|
||||||
seenAlready := xsync.NewMapOf[ID, struct{}]()
|
seenAlready := xsync.NewMapOf[ID, struct{}]()
|
||||||
|
|
||||||
for _, df := range dfs {
|
opts.CheckDuplicate = func(id ID, relay string) bool {
|
||||||
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{}{})
|
_, exists := seenAlready.LoadOrStore(id, struct{}{})
|
||||||
if exists && pool.duplicateMiddleware != nil {
|
if exists && pool.duplicateMiddleware != nil {
|
||||||
pool.duplicateMiddleware(relay, id)
|
pool.duplicateMiddleware(relay, id)
|
||||||
}
|
}
|
||||||
return exists
|
return exists
|
||||||
}), opts...,
|
}
|
||||||
|
|
||||||
|
for _, df := range dfs {
|
||||||
|
go func(df DirectedFilter) {
|
||||||
|
for ie := range pool.subManyEoseNonOverwriteCheckDuplicate(ctx,
|
||||||
|
[]string{df.Relay},
|
||||||
|
df.Filter,
|
||||||
|
opts,
|
||||||
) {
|
) {
|
||||||
select {
|
select {
|
||||||
case res <- ie:
|
case res <- ie:
|
||||||
@@ -781,6 +726,6 @@ func (pool *SimplePool) BatchedSubManyEose(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the pool with the given reason.
|
// 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))
|
pool.cancel(fmt.Errorf("pool closed with reason: '%s'", reason))
|
||||||
}
|
}
|
||||||
|
|||||||
150
relay.go
150
relay.go
@@ -51,7 +51,7 @@ type writeRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewRelay returns a new relay. It takes a context that, when canceled, will close the relay connection.
|
// 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)
|
ctx, cancel := context.WithCancelCause(ctx)
|
||||||
r := &Relay{
|
r := &Relay{
|
||||||
URL: NormalizeURL(url),
|
URL: NormalizeURL(url),
|
||||||
@@ -64,10 +64,6 @@ func NewRelay(ctx context.Context, url string, opts ...RelayOption) *Relay {
|
|||||||
requestHeader: nil,
|
requestHeader: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt.ApplyRelayOption(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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().
|
// 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.
|
// If you need fine grained long-term connection contexts, use NewRelay() instead.
|
||||||
func RelayConnect(ctx context.Context, url string, opts ...RelayOption) (*Relay, error) {
|
func RelayConnect(ctx context.Context, url string, opts RelayOptions) (*Relay, error) {
|
||||||
r := NewRelay(context.Background(), url, opts...)
|
r := NewRelay(context.Background(), url, opts)
|
||||||
err := r.Connect(ctx)
|
err := r.Connect(ctx)
|
||||||
return r, err
|
return r, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RelayOption is the type of the argument passed when instantiating relay connections.
|
type RelayOptions struct {
|
||||||
type RelayOption interface {
|
// NoticeHandler just takes notices and is expected to do something with them.
|
||||||
ApplyRelayOption(*Relay)
|
// When not given defaults to logging the notices.
|
||||||
}
|
NoticeHandler func(notice string)
|
||||||
|
|
||||||
var (
|
// CustomHandler, if given, must be a function that handles any relay message
|
||||||
_ RelayOption = (WithNoticeHandler)(nil)
|
// that couldn't be parsed as a standard envelope.
|
||||||
_ RelayOption = (WithCustomHandler)(nil)
|
CustomHandler func(data string)
|
||||||
_ RelayOption = (WithRequestHeader)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
// WithNoticeHandler just takes notices and is expected to do something with them.
|
// RequestHeader sets the HTTP request header of the websocket preflight request
|
||||||
// when not given, defaults to logging the notices.
|
RequestHeader http.Header
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// String just returns the relay URL.
|
// String just returns the relay URL.
|
||||||
@@ -273,21 +248,21 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
|
|||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
// check if the event matches the desired filter, ignore otherwise
|
// check if the event matches the desired filter, ignore otherwise
|
||||||
if !sub.match(&env.Event) {
|
if !sub.match(env.Event) {
|
||||||
InfoLogger.Printf("{%s} filter does not match: %v ~ %v\n", r.URL, sub.Filters, env.Event)
|
InfoLogger.Printf("{%s} filter does not match: %v ~ %v\n", r.URL, sub.Filter, env.Event)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// check signature, ignore invalid, except from trusted (AssumeValid) relays
|
// check signature, ignore invalid, except from trusted (AssumeValid) relays
|
||||||
if !r.AssumeValid {
|
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)
|
InfoLogger.Printf("{%s} bad signature on %s\n", r.URL, env.Event.ID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// dispatch this to the internal .events channel of the subscription
|
// dispatch this to the internal .events channel of the subscription
|
||||||
sub.dispatchEvent(&env.Event)
|
sub.dispatchEvent(env.Event)
|
||||||
}
|
}
|
||||||
case *EOSEEnvelope:
|
case *EOSEEnvelope:
|
||||||
if subscription, ok := r.Subscriptions.Load(subIdToSerial(string(*env))); ok {
|
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
|
// 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.
|
// 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{
|
authEvent := Event{
|
||||||
CreatedAt: Now(),
|
CreatedAt: Now(),
|
||||||
Kind: KindClientAuthentication,
|
Kind: KindClientAuthentication,
|
||||||
@@ -344,7 +319,7 @@ func (r *Relay) Auth(ctx context.Context, sign func(event *Event) error) error {
|
|||||||
},
|
},
|
||||||
Content: "",
|
Content: "",
|
||||||
}
|
}
|
||||||
if err := sign(&authEvent); err != nil {
|
if err := sign(ctx, &authEvent); err != nil {
|
||||||
return fmt.Errorf("error signing auth event: %w", err)
|
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.
|
// 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.
|
// 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) {
|
func (r *Relay) Subscribe(ctx context.Context, filter Filter, opts SubscriptionOptions) (*Subscription, error) {
|
||||||
sub := r.PrepareSubscription(ctx, filters, opts...)
|
sub := r.PrepareSubscription(ctx, filter, opts)
|
||||||
|
|
||||||
if r.Connection == nil {
|
if r.Connection == nil {
|
||||||
return nil, fmt.Errorf("not connected to %s", r.URL)
|
return nil, fmt.Errorf("not connected to %s", r.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := sub.Fire(); err != nil {
|
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
|
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.
|
// 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.
|
// 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)
|
current := subscriptionIDCounter.Add(1)
|
||||||
ctx, cancel := context.WithCancelCause(ctx)
|
ctx, cancel := context.WithCancelCause(ctx)
|
||||||
|
|
||||||
@@ -431,30 +406,21 @@ func (r *Relay) PrepareSubscription(ctx context.Context, filters Filters, opts .
|
|||||||
Context: ctx,
|
Context: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
counter: current,
|
counter: current,
|
||||||
Events: make(chan *Event),
|
Events: make(chan Event),
|
||||||
EndOfStoredEvents: make(chan struct{}, 1),
|
EndOfStoredEvents: make(chan struct{}, 1),
|
||||||
ClosedReason: make(chan string, 1),
|
ClosedReason: make(chan string, 1),
|
||||||
Filters: filters,
|
Filter: filter,
|
||||||
match: filters.Match,
|
match: filter.Matches,
|
||||||
}
|
}
|
||||||
|
|
||||||
label := ""
|
sub.checkDuplicate = opts.CheckDuplicate
|
||||||
for _, opt := range opts {
|
sub.checkDuplicateReplaceable = opts.CheckDuplicateReplaceable
|
||||||
switch o := opt.(type) {
|
|
||||||
case WithLabel:
|
|
||||||
label = string(o)
|
|
||||||
case WithCheckDuplicate:
|
|
||||||
sub.checkDuplicate = o
|
|
||||||
case WithCheckDuplicateReplaceable:
|
|
||||||
sub.checkDuplicateReplaceable = o
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// subscription id computation
|
// subscription id computation
|
||||||
buf := subIdPool.Get().([]byte)[:0]
|
buf := subIdPool.Get().([]byte)[:0]
|
||||||
buf = strconv.AppendInt(buf, sub.counter, 10)
|
buf = strconv.AppendInt(buf, sub.counter, 10)
|
||||||
buf = append(buf, ':')
|
buf = append(buf, ':')
|
||||||
buf = append(buf, label...)
|
buf = append(buf, opts.Label...)
|
||||||
defer subIdPool.Put(buf)
|
defer subIdPool.Put(buf)
|
||||||
sub.id = string(buf)
|
sub.id = string(buf)
|
||||||
|
|
||||||
@@ -467,63 +433,13 @@ func (r *Relay) PrepareSubscription(ctx context.Context, filters Filters, opts .
|
|||||||
return sub
|
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.
|
// Count sends a "COUNT" command to the relay and returns the count of events matching the filters.
|
||||||
func (r *Relay) Count(
|
func (r *Relay) Count(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
filters Filters,
|
filter Filter,
|
||||||
opts ...SubscriptionOption,
|
opts SubscriptionOptions,
|
||||||
) (int64, []byte, error) {
|
) (int64, []byte, error) {
|
||||||
v, err := r.countInternal(ctx, filters, opts...)
|
v, err := r.countInternal(ctx, filter, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
@@ -531,8 +447,8 @@ func (r *Relay) Count(
|
|||||||
return *v.Count, v.HyperLogLog, nil
|
return *v.Count, v.HyperLogLog, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Relay) countInternal(ctx context.Context, filters Filters, opts ...SubscriptionOption) (CountEnvelope, error) {
|
func (r *Relay) countInternal(ctx context.Context, filter Filter, opts SubscriptionOptions) (CountEnvelope, error) {
|
||||||
sub := r.PrepareSubscription(ctx, filters, opts...)
|
sub := r.PrepareSubscription(ctx, filter, opts)
|
||||||
sub.countResult = make(chan CountEnvelope)
|
sub.countResult = make(chan CountEnvelope)
|
||||||
|
|
||||||
if err := sub.Fire(); err != nil {
|
if err := sub.Fire(); err != nil {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ type System struct {
|
|||||||
FollowSetsCache cache.Cache32[GenericSets[ProfileRef]]
|
FollowSetsCache cache.Cache32[GenericSets[ProfileRef]]
|
||||||
TopicSetsCache cache.Cache32[GenericSets[Topic]]
|
TopicSetsCache cache.Cache32[GenericSets[Topic]]
|
||||||
Hints hints.HintsDB
|
Hints hints.HintsDB
|
||||||
Pool *nostr.SimplePool
|
Pool *nostr.Pool
|
||||||
RelayListRelays *RelayStream
|
RelayListRelays *RelayStream
|
||||||
FollowListRelays *RelayStream
|
FollowListRelays *RelayStream
|
||||||
MetadataRelays *RelayStream
|
MetadataRelays *RelayStream
|
||||||
@@ -118,7 +118,7 @@ func NewSystem(mods ...SystemModifier) *System {
|
|||||||
Hints: memoryh.NewHintDB(),
|
Hints: memoryh.NewHintDB(),
|
||||||
}
|
}
|
||||||
|
|
||||||
sys.Pool = nostr.NewSimplePool(context.Background(),
|
sys.Pool = nostr.NewPool(context.Background(),
|
||||||
nostr.WithAuthorKindQueryMiddleware(sys.TrackQueryAttempts),
|
nostr.WithAuthorKindQueryMiddleware(sys.TrackQueryAttempts),
|
||||||
nostr.WithEventMiddleware(sys.TrackEventHintsAndRelays),
|
nostr.WithEventMiddleware(sys.TrackEventHintsAndRelays),
|
||||||
nostr.WithDuplicateMiddleware(sys.TrackEventRelaysD),
|
nostr.WithDuplicateMiddleware(sys.TrackEventRelaysD),
|
||||||
|
|||||||
13
signature.go
13
signature.go
@@ -4,31 +4,30 @@ package nostr
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"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.
|
// 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.
|
// Returns true if the signature is valid, false otherwise.
|
||||||
func (evt Event) CheckSignature() (bool, error) {
|
func (evt Event) VerifySignature() bool {
|
||||||
// read and check pubkey
|
// read and check pubkey
|
||||||
pubkey, err := schnorr.ParsePubKey(evt.PubKey[:])
|
pubkey, err := schnorr.ParsePubKey(evt.PubKey[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("event has invalid pubkey '%s': %w", evt.PubKey, err)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// read signature
|
// read signature
|
||||||
sig, err := schnorr.ParseSignature(evt.Sig[:])
|
sig, err := schnorr.ParseSignature(evt.Sig[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to parse signature: %w", err)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// check signature
|
// check signature
|
||||||
hash := sha256.Sum256(evt.Serialize())
|
hash := sha256.Sum256(evt.Serialize())
|
||||||
return sig.Verify(hash[:], pubkey), nil
|
return sig.Verify(hash[:], pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign signs an event with a given privateKey.
|
// Sign signs an event with a given privateKey.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type Subscription struct {
|
|||||||
|
|
||||||
// the Events channel emits all EVENTs that come in a Subscription
|
// the Events channel emits all EVENTs that come in a Subscription
|
||||||
// will be closed when the subscription ends
|
// will be closed when the subscription ends
|
||||||
Events chan *Event
|
Events chan Event
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
// the EndOfStoredEvents channel gets closed when an EOSE comes for that subscription
|
// 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.
|
// if it returns true that event will not be processed further.
|
||||||
checkDuplicateReplaceable func(rk ReplaceableKey, ts Timestamp) bool
|
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
|
live atomic.Bool
|
||||||
eosed atomic.Bool
|
eosed atomic.Bool
|
||||||
cancel context.CancelCauseFunc
|
cancel context.CancelCauseFunc
|
||||||
@@ -51,33 +51,19 @@ type Subscription struct {
|
|||||||
storedwg sync.WaitGroup
|
storedwg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubscriptionOption is the type of the argument passed when instantiating relay connections.
|
// All SubscriptionOptions fields are optional
|
||||||
// Some examples are WithLabel.
|
type SubscriptionOptions struct {
|
||||||
type SubscriptionOption interface {
|
// Label puts a label on the subscription (it is prepended to the automatic id) that is sent to relays.
|
||||||
IsSubscriptionOption()
|
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() {
|
func (sub *Subscription) start() {
|
||||||
<-sub.Context.Done()
|
<-sub.Context.Done()
|
||||||
|
|
||||||
@@ -93,7 +79,7 @@ func (sub *Subscription) start() {
|
|||||||
// GetID returns the subscription ID.
|
// GetID returns the subscription ID.
|
||||||
func (sub *Subscription) GetID() string { return sub.id }
|
func (sub *Subscription) GetID() string { return sub.id }
|
||||||
|
|
||||||
func (sub *Subscription) dispatchEvent(evt *Event) {
|
func (sub *Subscription) dispatchEvent(evt Event) {
|
||||||
added := false
|
added := false
|
||||||
if !sub.eosed.Load() {
|
if !sub.eosed.Load() {
|
||||||
sub.storedwg.Add(1)
|
sub.storedwg.Add(1)
|
||||||
|
|||||||
17
types.go
17
types.go
@@ -6,6 +6,12 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RelayEvent represents an event received from a specific relay.
|
||||||
|
type RelayEvent struct {
|
||||||
|
Event
|
||||||
|
Relay *Relay
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ZeroID = [32]byte{}
|
ZeroID = [32]byte{}
|
||||||
ZeroPK = [32]byte{}
|
ZeroPK = [32]byte{}
|
||||||
@@ -14,6 +20,7 @@ var (
|
|||||||
type PubKey [32]byte
|
type PubKey [32]byte
|
||||||
|
|
||||||
func (pk PubKey) String() string { return hex.EncodeToString(pk[:]) }
|
func (pk PubKey) String() string { return hex.EncodeToString(pk[:]) }
|
||||||
|
func (pk PubKey) Hex() string { return hex.EncodeToString(pk[:]) }
|
||||||
|
|
||||||
func PubKeyFromHex(pkh string) (PubKey, error) {
|
func PubKeyFromHex(pkh string) (PubKey, error) {
|
||||||
pk := PubKey{}
|
pk := PubKey{}
|
||||||
@@ -49,9 +56,19 @@ func MustPubKeyFromHex(pkh string) PubKey {
|
|||||||
return pk
|
return pk
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ContainsPubKey(haystack []PubKey, needle PubKey) bool {
|
||||||
|
for _, cand := range haystack {
|
||||||
|
if cand == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type ID [32]byte
|
type ID [32]byte
|
||||||
|
|
||||||
func (id ID) String() string { return hex.EncodeToString(id[:]) }
|
func (id ID) String() string { return hex.EncodeToString(id[:]) }
|
||||||
|
func (id ID) Hex() string { return hex.EncodeToString(id[:]) }
|
||||||
|
|
||||||
func IDFromHex(idh string) (ID, error) {
|
func IDFromHex(idh string) (ID, error) {
|
||||||
id := ID{}
|
id := ID{}
|
||||||
|
|||||||
36
utils.go
36
utils.go
@@ -46,39 +46,3 @@ func CompareEventReverse(b, a Event) int {
|
|||||||
}
|
}
|
||||||
return cmp.Compare(a.CreatedAt, b.CreatedAt)
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -53,23 +53,23 @@ func TestEventsCompare(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestEventsComparePtr(t *testing.T) {
|
func TestEventsComparePtr(t *testing.T) {
|
||||||
list := []*Event{
|
list := []Event{
|
||||||
{CreatedAt: 12},
|
{CreatedAt: 12},
|
||||||
{CreatedAt: 8},
|
{CreatedAt: 8},
|
||||||
{CreatedAt: 26},
|
{CreatedAt: 26},
|
||||||
{CreatedAt: 1},
|
{CreatedAt: 1},
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.SortFunc(list, CompareEventPtr)
|
slices.SortFunc(list, CompareEvent)
|
||||||
require.Equal(t, []*Event{
|
require.Equal(t, []Event{
|
||||||
{CreatedAt: 1},
|
{CreatedAt: 1},
|
||||||
{CreatedAt: 8},
|
{CreatedAt: 8},
|
||||||
{CreatedAt: 12},
|
{CreatedAt: 12},
|
||||||
{CreatedAt: 26},
|
{CreatedAt: 26},
|
||||||
}, list)
|
}, list)
|
||||||
|
|
||||||
slices.SortFunc(list, CompareEventPtrReverse)
|
slices.SortFunc(list, CompareEventReverse)
|
||||||
require.Equal(t, []*Event{
|
require.Equal(t, []Event{
|
||||||
{CreatedAt: 26},
|
{CreatedAt: 26},
|
||||||
{CreatedAt: 12},
|
{CreatedAt: 12},
|
||||||
{CreatedAt: 8},
|
{CreatedAt: 8},
|
||||||
|
|||||||
Reference in New Issue
Block a user