a bunch of conversions and api tweaks on khatru and eventstore.
This commit is contained in:
@@ -24,7 +24,7 @@ type Store interface {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
[](https://pkg.go.dev/github.com/fiatjaf/eventstore) [](https://github.com/fiatjaf/eventstore/actions/workflows/test.yml)
|
[](https://pkg.go.dev/fiatjaf.com/nostr/eventstore) [](https://fiatjaf.com/nostr/eventstore/actions/workflows/test.yml)
|
||||||
|
|
||||||
## command-line tool
|
## command-line tool
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# eventstore command-line tool
|
# eventstore command-line tool
|
||||||
|
|
||||||
```
|
```
|
||||||
go install github.com/fiatjaf/eventstore/cmd/eventstore@latest
|
go install fiatjaf.com/nostr/eventstore/cmd/eventstore@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|||||||
@@ -3,13 +3,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"fiatjaf.com/nostr/eventstore"
|
"fiatjaf.com/nostr/eventstore"
|
||||||
"fiatjaf.com/nostr/eventstore/mmm"
|
"fiatjaf.com/nostr/eventstore/mmm"
|
||||||
"fiatjaf.com/nostr"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,9 +22,7 @@ func doMmmInit(path string) (eventstore.Store, error) {
|
|||||||
if err := mmmm.Init(); err != nil {
|
if err := mmmm.Init(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
il := &mmm.IndexingLayer{
|
il := &mmm.IndexingLayer{}
|
||||||
ShouldIndex: func(ctx context.Context, e *nostr.Event) bool { return false },
|
|
||||||
}
|
|
||||||
if err := mmmm.EnsureLayer(filepath.Base(path), il); err != nil {
|
if err := mmmm.EnsureLayer(filepath.Base(path), il); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
"github.com/mailru/easyjson"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/nip77/negentropy"
|
"fiatjaf.com/nostr/nip77/negentropy"
|
||||||
"fiatjaf.com/nostr/nip77/negentropy/storage/vector"
|
"fiatjaf.com/nostr/nip77/negentropy/storage/vector"
|
||||||
|
"github.com/mailru/easyjson"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var neg = &cli.Command{
|
var neg = &cli.Command{
|
||||||
@@ -44,11 +44,7 @@ var neg = &cli.Command{
|
|||||||
// create negentropy object and initialize it with events
|
// create negentropy object and initialize it with events
|
||||||
vec := vector.New()
|
vec := vector.New()
|
||||||
neg := negentropy.New(vec, frameSizeLimit)
|
neg := negentropy.New(vec, frameSizeLimit)
|
||||||
ch, err := db.QueryEvents(ctx, filter)
|
for evt := range db.QueryEvents(filter) {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error querying: %s\n", err)
|
|
||||||
}
|
|
||||||
for evt := range ch {
|
|
||||||
vec.Insert(evt.CreatedAt, evt.ID)
|
vec.Insert(evt.CreatedAt, evt.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/mailru/easyjson"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"github.com/mailru/easyjson"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,14 +25,7 @@ var query = &cli.Command{
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ch, err := db.QueryEvents(ctx, filter)
|
for evt := range db.QueryEvents(filter) {
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "error querying: %s\n", err)
|
|
||||||
hasError = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for evt := range ch {
|
|
||||||
fmt.Println(evt)
|
fmt.Println(evt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
"github.com/mailru/easyjson"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"github.com/mailru/easyjson"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var save = &cli.Command{
|
var save = &cli.Command{
|
||||||
@@ -25,7 +25,7 @@ var save = &cli.Command{
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.SaveEvent(ctx, &event); err != nil {
|
if err := db.SaveEvent(event); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to save event '%s': %s\n", line, err)
|
fmt.Fprintf(os.Stderr, "failed to save event '%s': %s\n", line, err)
|
||||||
hasError = true
|
hasError = true
|
||||||
continue
|
continue
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -19,7 +19,6 @@ require (
|
|||||||
github.com/dgraph-io/ristretto v1.0.0
|
github.com/dgraph-io/ristretto v1.0.0
|
||||||
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3
|
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3
|
||||||
github.com/fasthttp/websocket v1.5.12
|
github.com/fasthttp/websocket v1.5.12
|
||||||
github.com/fiatjaf/eventstore v0.16.2
|
|
||||||
github.com/fiatjaf/khatru v0.17.4
|
github.com/fiatjaf/khatru v0.17.4
|
||||||
github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62
|
github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
@@ -70,6 +69,7 @@ require (
|
|||||||
github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect
|
github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect
|
||||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
|
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/fiatjaf/eventstore v0.16.2 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ func main() {
|
|||||||
|
|
||||||
### But I don't want to write my own database!
|
### But I don't want to write my own database!
|
||||||
|
|
||||||
Fear no more. Using the https://github.com/fiatjaf/eventstore module you get a bunch of compatible databases out of the box and you can just plug them into your relay. For example, [sqlite](https://pkg.go.dev/github.com/fiatjaf/eventstore/sqlite3):
|
Fear no more. Using the https://fiatjaf.com/nostr/eventstore module you get a bunch of compatible databases out of the box and you can just plug them into your relay. For example, [sqlite](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/sqlite3):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
db := sqlite3.SQLite3Backend{DatabaseURL: "/tmp/khatru-sqlite-tmp"}
|
db := sqlite3.SQLite3Backend{DatabaseURL: "/tmp/khatru-sqlite-tmp"}
|
||||||
|
|||||||
@@ -5,16 +5,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"fiatjaf.com/nostr/eventstore"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/eventstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddEvent sends an event through then normal add pipeline, as if it was received from a websocket.
|
// AddEvent sends an event through then normal add pipeline, as if it was received from a websocket.
|
||||||
func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) (skipBroadcast bool, writeError error) {
|
func (rl *Relay) AddEvent(ctx context.Context, evt nostr.Event) (skipBroadcast bool, writeError error) {
|
||||||
if evt == nil {
|
|
||||||
return false, errors.New("error: event is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if nostr.IsEphemeralKind(evt.Kind) {
|
if nostr.IsEphemeralKind(evt.Kind) {
|
||||||
return false, rl.handleEphemeral(ctx, evt)
|
return false, rl.handleEphemeral(ctx, evt)
|
||||||
} else {
|
} else {
|
||||||
@@ -22,9 +18,9 @@ func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) (skipBroadcast
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *Relay) handleNormal(ctx context.Context, evt *nostr.Event) (skipBroadcast bool, writeError error) {
|
func (rl *Relay) handleNormal(ctx context.Context, evt nostr.Event) (skipBroadcast bool, writeError error) {
|
||||||
for _, reject := range rl.RejectEvent {
|
if nil != rl.OnEvent {
|
||||||
if reject, msg := reject(ctx, evt); reject {
|
if reject, msg := rl.OnEvent(ctx, evt); reject {
|
||||||
if msg == "" {
|
if msg == "" {
|
||||||
return true, errors.New("blocked: no reason")
|
return true, errors.New("blocked: no reason")
|
||||||
} else {
|
} else {
|
||||||
@@ -36,8 +32,8 @@ func (rl *Relay) handleNormal(ctx context.Context, evt *nostr.Event) (skipBroadc
|
|||||||
// will store
|
// will store
|
||||||
// regular kinds are just saved directly
|
// regular kinds are just saved directly
|
||||||
if nostr.IsRegularKind(evt.Kind) {
|
if nostr.IsRegularKind(evt.Kind) {
|
||||||
for _, store := range rl.StoreEvent {
|
if nil != rl.StoreEvent {
|
||||||
if err := store(ctx, evt); err != nil {
|
if err := rl.StoreEvent(ctx, evt); err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
case eventstore.ErrDupEvent:
|
case eventstore.ErrDupEvent:
|
||||||
return true, nil
|
return true, nil
|
||||||
@@ -47,63 +43,21 @@ func (rl *Relay) handleNormal(ctx context.Context, evt *nostr.Event) (skipBroadc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// otherwise it's a replaceable -- so we'll use the replacer functions if we have any
|
// otherwise it's a replaceable
|
||||||
if len(rl.ReplaceEvent) > 0 {
|
if nil != rl.ReplaceEvent {
|
||||||
for _, repl := range rl.ReplaceEvent {
|
if err := rl.ReplaceEvent(ctx, evt); err != nil {
|
||||||
if err := repl(ctx, evt); err != nil {
|
switch err {
|
||||||
switch err {
|
case eventstore.ErrDupEvent:
|
||||||
case eventstore.ErrDupEvent:
|
return true, nil
|
||||||
return true, nil
|
default:
|
||||||
default:
|
return false, fmt.Errorf("%s", nostr.NormalizeOKMessage(err.Error(), "error"))
|
||||||
return false, fmt.Errorf("%s", nostr.NormalizeOKMessage(err.Error(), "error"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// otherwise do it the manual way
|
|
||||||
filter := nostr.Filter{Limit: 1, Kinds: []int{evt.Kind}, Authors: []string{evt.PubKey}}
|
|
||||||
if nostr.IsAddressableKind(evt.Kind) {
|
|
||||||
// when addressable, add the "d" tag to the filter
|
|
||||||
filter.Tags = nostr.TagMap{"d": []string{evt.Tags.GetD()}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// now we fetch old events and delete them
|
|
||||||
shouldStore := true
|
|
||||||
for _, query := range rl.QueryEvents {
|
|
||||||
ch, err := query(ctx, filter)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for previous := range ch {
|
|
||||||
if isOlder(previous, evt) {
|
|
||||||
for _, del := range rl.DeleteEvent {
|
|
||||||
del(ctx, previous)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// we found a more recent event, so we won't delete it and also will not store this new one
|
|
||||||
shouldStore = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// store
|
|
||||||
if shouldStore {
|
|
||||||
for _, store := range rl.StoreEvent {
|
|
||||||
if saveErr := store(ctx, evt); saveErr != nil {
|
|
||||||
switch saveErr {
|
|
||||||
case eventstore.ErrDupEvent:
|
|
||||||
return true, nil
|
|
||||||
default:
|
|
||||||
return false, fmt.Errorf("%s", nostr.NormalizeOKMessage(saveErr.Error(), "error"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ons := range rl.OnEventSaved {
|
if nil != rl.OnEventSaved {
|
||||||
ons(ctx, evt)
|
rl.OnEventSaved(ctx, evt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// track event expiration if applicable
|
// track event expiration if applicable
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mailru/easyjson"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"github.com/mailru/easyjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
func readAuthorization(r *http.Request) (*nostr.Event, error) {
|
func readAuthorization(r *http.Request) (*nostr.Event, error) {
|
||||||
@@ -28,7 +28,7 @@ func readAuthorization(r *http.Request) (*nostr.Event, error) {
|
|||||||
if evt.Kind != 24242 || !evt.CheckID() {
|
if evt.Kind != 24242 || !evt.CheckID() {
|
||||||
return nil, fmt.Errorf("invalid event")
|
return nil, fmt.Errorf("invalid event")
|
||||||
}
|
}
|
||||||
if ok, _ := evt.CheckSignature(); !ok {
|
if !evt.VerifySignature() {
|
||||||
return nil, fmt.Errorf("invalid signature")
|
return nil, fmt.Errorf("invalid signature")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package blossom
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"iter"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
)
|
)
|
||||||
@@ -13,14 +14,14 @@ type BlobDescriptor struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Uploaded nostr.Timestamp `json:"uploaded"`
|
Uploaded nostr.Timestamp `json:"uploaded"`
|
||||||
|
|
||||||
Owner string `json:"-"`
|
Owner nostr.PubKey `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlobIndex interface {
|
type BlobIndex interface {
|
||||||
Keep(ctx context.Context, blob BlobDescriptor, pubkey string) error
|
Keep(ctx context.Context, blob BlobDescriptor, pubkey nostr.PubKey) error
|
||||||
List(ctx context.Context, pubkey string) (chan BlobDescriptor, error)
|
List(ctx context.Context, pubkey nostr.PubKey) iter.Seq[BlobDescriptor]
|
||||||
Get(ctx context.Context, sha256 string) (*BlobDescriptor, error)
|
Get(ctx context.Context, sha256 string) (*BlobDescriptor, error)
|
||||||
Delete(ctx context.Context, sha256 string, pubkey string) error
|
Delete(ctx context.Context, sha256 string, pubkey nostr.PubKey) error
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ BlobIndex = (*EventStoreBlobIndexWrapper)(nil)
|
var _ BlobIndex = (*EventStoreBlobIndexWrapper)(nil)
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ package blossom
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"iter"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"fiatjaf.com/nostr/eventstore"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/eventstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EventStoreBlobIndexWrapper uses fake events to keep track of what blobs we have stored and who owns them
|
// EventStoreBlobIndexWrapper uses fake events to keep track of what blobs we have stored and who owns them
|
||||||
@@ -15,15 +16,15 @@ type EventStoreBlobIndexWrapper struct {
|
|||||||
ServiceURL string
|
ServiceURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (es EventStoreBlobIndexWrapper) Keep(ctx context.Context, blob BlobDescriptor, pubkey string) error {
|
func (es EventStoreBlobIndexWrapper) Keep(ctx context.Context, blob BlobDescriptor, pubkey nostr.PubKey) error {
|
||||||
ch, err := es.Store.QueryEvents(ctx, nostr.Filter{Authors: []string{pubkey}, Kinds: []int{24242}, Tags: nostr.TagMap{"x": []string{blob.SHA256}}})
|
next, stop := iter.Pull(
|
||||||
if err != nil {
|
es.Store.QueryEvents(nostr.Filter{Authors: []nostr.PubKey{pubkey}, Kinds: []uint16{24242}, Tags: nostr.TagMap{"x": []string{blob.SHA256}}}),
|
||||||
return err
|
)
|
||||||
}
|
defer stop()
|
||||||
|
|
||||||
if <-ch == nil {
|
if _, exists := next(); !exists {
|
||||||
// doesn't exist, save
|
// doesn't exist, save
|
||||||
evt := &nostr.Event{
|
evt := nostr.Event{
|
||||||
PubKey: pubkey,
|
PubKey: pubkey,
|
||||||
Kind: 24242,
|
Kind: 24242,
|
||||||
Tags: nostr.Tags{
|
Tags: nostr.Tags{
|
||||||
@@ -34,38 +35,31 @@ func (es EventStoreBlobIndexWrapper) Keep(ctx context.Context, blob BlobDescript
|
|||||||
CreatedAt: blob.Uploaded,
|
CreatedAt: blob.Uploaded,
|
||||||
}
|
}
|
||||||
evt.ID = evt.GetID()
|
evt.ID = evt.GetID()
|
||||||
es.Store.SaveEvent(ctx, evt)
|
es.Store.SaveEvent(evt)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (es EventStoreBlobIndexWrapper) List(ctx context.Context, pubkey string) (chan BlobDescriptor, error) {
|
func (es EventStoreBlobIndexWrapper) List(ctx context.Context, pubkey nostr.PubKey) iter.Seq[BlobDescriptor] {
|
||||||
ech, err := es.Store.QueryEvents(ctx, nostr.Filter{Authors: []string{pubkey}, Kinds: []int{24242}})
|
return func(yield func(BlobDescriptor) bool) {
|
||||||
if err != nil {
|
for evt := range es.Store.QueryEvents(nostr.Filter{
|
||||||
return nil, err
|
Authors: []nostr.PubKey{pubkey},
|
||||||
}
|
Kinds: []uint16{24242},
|
||||||
|
}) {
|
||||||
ch := make(chan BlobDescriptor)
|
yield(es.parseEvent(evt))
|
||||||
|
|
||||||
go func() {
|
|
||||||
for evt := range ech {
|
|
||||||
ch <- es.parseEvent(evt)
|
|
||||||
}
|
}
|
||||||
close(ch)
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
return ch, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (es EventStoreBlobIndexWrapper) Get(ctx context.Context, sha256 string) (*BlobDescriptor, error) {
|
func (es EventStoreBlobIndexWrapper) Get(ctx context.Context, sha256 string) (*BlobDescriptor, error) {
|
||||||
ech, err := es.Store.QueryEvents(ctx, nostr.Filter{Tags: nostr.TagMap{"x": []string{sha256}}, Kinds: []int{24242}, Limit: 1})
|
next, stop := iter.Pull(
|
||||||
if err != nil {
|
es.Store.QueryEvents(nostr.Filter{Tags: nostr.TagMap{"x": []string{sha256}}, Kinds: []uint16{24242}, Limit: 1}),
|
||||||
return nil, err
|
)
|
||||||
}
|
|
||||||
|
|
||||||
evt := <-ech
|
defer stop()
|
||||||
if evt != nil {
|
|
||||||
|
if evt, found := next(); found {
|
||||||
bd := es.parseEvent(evt)
|
bd := es.parseEvent(evt)
|
||||||
return &bd, nil
|
return &bd, nil
|
||||||
}
|
}
|
||||||
@@ -73,21 +67,27 @@ func (es EventStoreBlobIndexWrapper) Get(ctx context.Context, sha256 string) (*B
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (es EventStoreBlobIndexWrapper) Delete(ctx context.Context, sha256 string, pubkey string) error {
|
func (es EventStoreBlobIndexWrapper) Delete(ctx context.Context, sha256 string, pubkey nostr.PubKey) error {
|
||||||
ech, err := es.Store.QueryEvents(ctx, nostr.Filter{Authors: []string{pubkey}, Tags: nostr.TagMap{"x": []string{sha256}}, Kinds: []int{24242}, Limit: 1})
|
next, stop := iter.Pull(
|
||||||
if err != nil {
|
es.Store.QueryEvents(nostr.Filter{
|
||||||
return err
|
Authors: []nostr.PubKey{pubkey},
|
||||||
}
|
Tags: nostr.TagMap{"x": []string{sha256}},
|
||||||
|
Kinds: []uint16{24242},
|
||||||
|
Limit: 1,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
evt := <-ech
|
defer stop()
|
||||||
if evt != nil {
|
|
||||||
return es.Store.DeleteEvent(ctx, evt)
|
if evt, found := next(); found {
|
||||||
|
return es.Store.DeleteEvent(evt.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (es EventStoreBlobIndexWrapper) parseEvent(evt *nostr.Event) BlobDescriptor {
|
func (es EventStoreBlobIndexWrapper) parseEvent(evt nostr.Event) BlobDescriptor {
|
||||||
hhash := evt.Tags[0][1]
|
hhash := evt.Tags[0][1]
|
||||||
mimetype := evt.Tags[1][1]
|
mimetype := evt.Tags[1][1]
|
||||||
ext := getExtension(mimetype)
|
ext := getExtension(mimetype)
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// run the reject hooks
|
// run the reject hooks
|
||||||
for _, ru := range bs.RejectUpload {
|
if nil != bs.RejectUpload {
|
||||||
reject, reason, code := ru(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
|
||||||
@@ -134,8 +134,8 @@ func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// save actual blob
|
// save actual blob
|
||||||
for _, sb := range bs.StoreBlob {
|
if nil != bs.StoreBlob {
|
||||||
if err := sb(r.Context(), hhash, b); err != nil {
|
if err := bs.StoreBlob(r.Context(), hhash, b); err != nil {
|
||||||
blossomError(w, "failed to save: "+err.Error(), 500)
|
blossomError(w, "failed to save: "+err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -175,8 +175,8 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rg := range bs.RejectGet {
|
if nil != bs.RejectGet {
|
||||||
reject, reason, code := rg(r.Context(), auth, hhash)
|
reject, reason, code := bs.RejectGet(r.Context(), auth, hhash)
|
||||||
if reject {
|
if reject {
|
||||||
blossomError(w, reason, code)
|
blossomError(w, reason, code)
|
||||||
return
|
return
|
||||||
@@ -188,8 +188,8 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) {
|
|||||||
ext = "." + spl[1]
|
ext = "." + spl[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, lb := range bs.LoadBlob {
|
if nil != bs.LoadBlob {
|
||||||
reader, _ := lb(r.Context(), hhash)
|
reader, _ := bs.LoadBlob(r.Context(), hhash)
|
||||||
if reader != nil {
|
if reader != nil {
|
||||||
// use unix epoch as the time if we can't find the descriptor
|
// use unix epoch as the time if we can't find the descriptor
|
||||||
// as described in the http.ServeContent documentation
|
// as described in the http.ServeContent documentation
|
||||||
@@ -245,26 +245,20 @@ func (bs BlossomServer) handleList(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pubkey := r.URL.Path[6:]
|
pubkey, err := nostr.PubKeyFromHex(r.URL.Path[6:])
|
||||||
|
|
||||||
for _, rl := range bs.RejectList {
|
if nil != bs.RejectList {
|
||||||
reject, reason, code := rl(r.Context(), auth, pubkey)
|
reject, reason, code := bs.RejectList(r.Context(), auth, pubkey)
|
||||||
if reject {
|
if reject {
|
||||||
blossomError(w, reason, code)
|
blossomError(w, reason, code)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ch, err := bs.Store.List(r.Context(), pubkey)
|
|
||||||
if err != nil {
|
|
||||||
blossomError(w, "failed to query: "+err.Error(), 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write([]byte{'['})
|
w.Write([]byte{'['})
|
||||||
enc := json.NewEncoder(w)
|
enc := json.NewEncoder(w)
|
||||||
first := true
|
first := true
|
||||||
for bd := range ch {
|
for bd := range bs.Store.List(r.Context(), pubkey) {
|
||||||
if !first {
|
if !first {
|
||||||
w.Write([]byte{','})
|
w.Write([]byte{','})
|
||||||
} else {
|
} else {
|
||||||
@@ -303,8 +297,8 @@ func (bs BlossomServer) handleDelete(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// should we accept this delete?
|
// should we accept this delete?
|
||||||
for _, rd := range bs.RejectDelete {
|
if nil != bs.RejectDelete {
|
||||||
reject, reason, code := rd(r.Context(), auth, hhash)
|
reject, reason, code := bs.RejectDelete(r.Context(), auth, hhash)
|
||||||
if reject {
|
if reject {
|
||||||
blossomError(w, reason, code)
|
blossomError(w, reason, code)
|
||||||
return
|
return
|
||||||
@@ -319,8 +313,8 @@ func (bs BlossomServer) handleDelete(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// we will actually only delete the file if no one else owns it
|
// we will actually only delete the file if no one else owns it
|
||||||
if bd, err := bs.Store.Get(r.Context(), hhash); err == nil && bd == nil {
|
if bd, err := bs.Store.Get(r.Context(), hhash); err == nil && bd == nil {
|
||||||
for _, del := range bs.DeleteBlob {
|
if nil != bs.DeleteBlob {
|
||||||
if err := del(r.Context(), hhash); err != nil {
|
if err := bs.DeleteBlob(r.Context(), hhash); err != nil {
|
||||||
blossomError(w, "failed to delete blob: "+err.Error(), 500)
|
blossomError(w, "failed to delete blob: "+err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type BlossomServer struct {
|
|||||||
|
|
||||||
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 nostr.PubKey) (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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ import (
|
|||||||
|
|
||||||
// BroadcastEvent emits an event to all listeners whose filters' match, skipping all filters and actions
|
// BroadcastEvent emits an event to all listeners whose filters' match, skipping all filters and actions
|
||||||
// it also doesn't attempt to store the event or trigger any reactions or callbacks
|
// it also doesn't attempt to store the event or trigger any reactions or callbacks
|
||||||
func (rl *Relay) BroadcastEvent(evt *nostr.Event) int {
|
func (rl *Relay) BroadcastEvent(evt nostr.Event) int {
|
||||||
return rl.notifyListeners(evt)
|
return rl.notifyListeners(evt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rl *Relay) handleDeleteRequest(ctx context.Context, evt *nostr.Event) error {
|
func (rl *Relay) handleDeleteRequest(ctx context.Context, evt nostr.Event) error {
|
||||||
// event deletion -- nip09
|
// event deletion -- nip09
|
||||||
for _, tag := range evt.Tags {
|
for _, tag := range evt.Tags {
|
||||||
if len(tag) >= 2 {
|
if len(tag) >= 2 {
|
||||||
@@ -17,7 +17,11 @@ func (rl *Relay) handleDeleteRequest(ctx context.Context, evt *nostr.Event) erro
|
|||||||
|
|
||||||
switch tag[0] {
|
switch tag[0] {
|
||||||
case "e":
|
case "e":
|
||||||
f = nostr.Filter{IDs: []string{tag[1]}}
|
id, err := nostr.IDFromHex(tag[1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid 'e' tag '%s': %w", tag[1], err)
|
||||||
|
}
|
||||||
|
f = nostr.Filter{IDs: []nostr.ID{id}}
|
||||||
case "a":
|
case "a":
|
||||||
spl := strings.Split(tag[1], ":")
|
spl := strings.Split(tag[1], ":")
|
||||||
if len(spl) != 3 {
|
if len(spl) != 3 {
|
||||||
@@ -27,11 +31,15 @@ func (rl *Relay) handleDeleteRequest(ctx context.Context, evt *nostr.Event) erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
author := spl[1]
|
author, err := nostr.PubKeyFromHex(spl[1])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
identifier := spl[2]
|
identifier := spl[2]
|
||||||
f = nostr.Filter{
|
f = nostr.Filter{
|
||||||
Kinds: []int{kind},
|
Kinds: []uint16{uint16(kind)},
|
||||||
Authors: []string{author},
|
Authors: []nostr.PubKey{author},
|
||||||
Tags: nostr.TagMap{"d": []string{identifier}},
|
Tags: nostr.TagMap{"d": []string{identifier}},
|
||||||
Until: &evt.CreatedAt,
|
Until: &evt.CreatedAt,
|
||||||
}
|
}
|
||||||
@@ -40,39 +48,30 @@ func (rl *Relay) handleDeleteRequest(ctx context.Context, evt *nostr.Event) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.WithValue(ctx, internalCallKey, struct{}{})
|
ctx := context.WithValue(ctx, internalCallKey, struct{}{})
|
||||||
for _, query := range rl.QueryEvents {
|
|
||||||
ch, err := query(ctx, f)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
target := <-ch
|
|
||||||
if target == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// got the event, now check if the user can delete it
|
|
||||||
acceptDeletion := target.PubKey == evt.PubKey
|
|
||||||
var msg string
|
|
||||||
if !acceptDeletion {
|
|
||||||
msg = "you are not the author of this event"
|
|
||||||
}
|
|
||||||
// but if we have a function to overwrite this outcome, use that instead
|
|
||||||
for _, odo := range rl.OverwriteDeletionOutcome {
|
|
||||||
acceptDeletion, msg = odo(ctx, target, evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
if acceptDeletion {
|
if nil != rl.QueryStored {
|
||||||
// delete it
|
for target := range rl.QueryStored(ctx, f) {
|
||||||
for _, del := range rl.DeleteEvent {
|
// got the event, now check if the user can delete it
|
||||||
if err := del(ctx, target); err != nil {
|
acceptDeletion := target.PubKey == evt.PubKey
|
||||||
return err
|
var msg string
|
||||||
}
|
if !acceptDeletion {
|
||||||
|
msg = "you are not the author of this event"
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it was tracked to be expired that is not needed anymore
|
if acceptDeletion {
|
||||||
rl.expirationManager.removeEvent(target.ID)
|
// delete it
|
||||||
} else {
|
if nil != rl.DeleteEvent {
|
||||||
// fail and stop here
|
if err := rl.DeleteEvent(ctx, target.ID); err != nil {
|
||||||
return fmt.Errorf("blocked: %s", msg)
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it was tracked to be expired that is not needed anymore
|
||||||
|
rl.expirationManager.removeEvent(target.ID)
|
||||||
|
} else {
|
||||||
|
// fail and stop here
|
||||||
|
return fmt.Errorf("blocked: %s", msg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't try to query this same event again
|
// don't try to query this same event again
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ outline: deep
|
|||||||
|
|
||||||
The [`nostr.Filter` type](https://pkg.go.dev/github.com/nbd-wtf/go-nostr#Filter) has a `Search` field, so you basically just has to handle that if it's present.
|
The [`nostr.Filter` type](https://pkg.go.dev/github.com/nbd-wtf/go-nostr#Filter) has a `Search` field, so you basically just has to handle that if it's present.
|
||||||
|
|
||||||
It can be tricky to implement fulltext search properly though, so some [eventstores](../core/eventstore) implement it natively, such as [Bluge](https://pkg.go.dev/github.com/fiatjaf/eventstore/bluge), [OpenSearch](https://pkg.go.dev/github.com/fiatjaf/eventstore/opensearch) and [ElasticSearch](https://pkg.go.dev/github.com/fiatjaf/eventstore/elasticsearch) (although for the last two you'll need an instance of these database servers running, while with Bluge it's embedded).
|
It can be tricky to implement fulltext search properly though, so some [eventstores](../core/eventstore) implement it natively, such as [Bluge](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/bluge), [OpenSearch](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/opensearch) and [ElasticSearch](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/elasticsearch) (although for the last two you'll need an instance of these database servers running, while with Bluge it's embedded).
|
||||||
|
|
||||||
If you have any of these you can just use them just like any other eventstore:
|
If you have any of these you can just use them just like any other eventstore:
|
||||||
|
|
||||||
@@ -33,9 +33,9 @@ func main () {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that in this case we're using the [LMDB](https://pkg.go.dev/github.com/fiatjaf/eventstore/lmdb) adapter for normal queries and it explicitly rejects any filter that contains a `Search` field, while [Bluge](https://pkg.go.dev/github.com/fiatjaf/eventstore/bluge) rejects any filter _without_ a `Search` value, which make them pair well together.
|
Note that in this case we're using the [LMDB](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/lmdb) adapter for normal queries and it explicitly rejects any filter that contains a `Search` field, while [Bluge](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/bluge) rejects any filter _without_ a `Search` value, which make them pair well together.
|
||||||
|
|
||||||
Other adapters, like [SQLite](https://pkg.go.dev/github.com/fiatjaf/eventstore/sqlite3), implement search functionality on their own, so if you don't want to use that you would have to have a middleware between, like:
|
Other adapters, like [SQLite](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/sqlite3), implement search functionality on their own, so if you don't want to use that you would have to have a middleware between, like:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent, search.SaveEvent)
|
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent, search.SaveEvent)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ outline: deep
|
|||||||
|
|
||||||
Khatru doesn't make any assumptions about how you'll want to store events. Any function can be plugged in to the `StoreEvent`, `DeleteEvent`, `ReplaceEvent` and `QueryEvents` hooks.
|
Khatru doesn't make any assumptions about how you'll want to store events. Any function can be plugged in to the `StoreEvent`, `DeleteEvent`, `ReplaceEvent` and `QueryEvents` hooks.
|
||||||
|
|
||||||
However the [`eventstore`](https://github.com/fiatjaf/eventstore) library has adapters that you can easily plug into `khatru`'s hooks.
|
However the [`eventstore`](https://fiatjaf.com/nostr/eventstore) library has adapters that you can easily plug into `khatru`'s hooks.
|
||||||
|
|
||||||
# Using the `eventstore` library
|
# Using the `eventstore` library
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ The library includes many different adapters -- often called "backends" --, writ
|
|||||||
|
|
||||||
For all of them you start by instantiating a struct containing some basic options and a pointer (a file path for local databases, a connection string for remote databases) to the data. Then you call `.Init()` and if all is well you're ready to start storing, querying and deleting events, so you can pass the respective functions to their `khatru` counterparts. These eventstores also expose a `.Close()` function that must be called if you're going to stop using that store and keep your application open.
|
For all of them you start by instantiating a struct containing some basic options and a pointer (a file path for local databases, a connection string for remote databases) to the data. Then you call `.Init()` and if all is well you're ready to start storing, querying and deleting events, so you can pass the respective functions to their `khatru` counterparts. These eventstores also expose a `.Close()` function that must be called if you're going to stop using that store and keep your application open.
|
||||||
|
|
||||||
Here's an example with the [Badger](https://pkg.go.dev/github.com/fiatjaf/eventstore/badger) adapter, made for the [Badger](https://github.com/dgraph-io/badger) embedded key-value database:
|
Here's an example with the [Badger](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/badger) adapter, made for the [Badger](https://github.com/dgraph-io/badger) embedded key-value database:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/fiatjaf/eventstore/badger"
|
"fiatjaf.com/nostr/eventstore/badger"
|
||||||
"github.com/fiatjaf/khatru"
|
"github.com/fiatjaf/khatru"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,11 +46,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
[LMDB](https://pkg.go.dev/github.com/fiatjaf/eventstore/lmdb) works the same way.
|
[LMDB](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/lmdb) works the same way.
|
||||||
|
|
||||||
[SQLite](https://pkg.go.dev/github.com/fiatjaf/eventstore/sqlite3) also stores things locally so it only needs a `Path`.
|
[SQLite](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/sqlite3) also stores things locally so it only needs a `Path`.
|
||||||
|
|
||||||
[PostgreSQL](https://pkg.go.dev/github.com/fiatjaf/eventstore/postgresql) and [MySQL](https://pkg.go.dev/github.com/fiatjaf/eventstore/mysql) use remote connections to database servers, so they take a `DatabaseURL` parameter, but after that it's the same.
|
[PostgreSQL](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/postgresql) and [MySQL](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/mysql) use remote connections to database servers, so they take a `DatabaseURL` parameter, but after that it's the same.
|
||||||
|
|
||||||
## Using two at a time
|
## Using two at a time
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ relay.Info.Description = "this is my custom relay"
|
|||||||
relay.Info.Icon = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fliquipedia.net%2Fcommons%2Fimages%2F3%2F35%2FSCProbe.jpg&f=1&nofb=1&ipt=0cbbfef25bce41da63d910e86c3c343e6c3b9d63194ca9755351bb7c2efa3359&ipo=images"
|
relay.Info.Icon = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fliquipedia.net%2Fcommons%2Fimages%2F3%2F35%2FSCProbe.jpg&f=1&nofb=1&ipt=0cbbfef25bce41da63d910e86c3c343e6c3b9d63194ca9755351bb7c2efa3359&ipo=images"
|
||||||
```
|
```
|
||||||
|
|
||||||
Now we must set up the basic functions for accepting events and answering queries. We could make our own querying engine from scratch, but we can also use [eventstore](https://github.com/fiatjaf/eventstore). In this example we'll use the SQLite adapter:
|
Now we must set up the basic functions for accepting events and answering queries. We could make our own querying engine from scratch, but we can also use [eventstore](https://fiatjaf.com/nostr/eventstore). In this example we'll use the SQLite adapter:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
db := sqlite3.SQLite3Backend{DatabaseURL: "/tmp/khatru-sqlite-tmp"}
|
db := sqlite3.SQLite3Backend{DatabaseURL: "/tmp/khatru-sqlite-tmp"}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rl *Relay) handleEphemeral(ctx context.Context, evt *nostr.Event) error {
|
func (rl *Relay) handleEphemeral(ctx context.Context, evt nostr.Event) error {
|
||||||
for _, reject := range rl.RejectEvent {
|
if nil != rl.OnEvent {
|
||||||
if reject, msg := reject(ctx, evt); reject {
|
if reject, msg := rl.OnEvent(ctx, evt); reject {
|
||||||
if msg == "" {
|
if msg == "" {
|
||||||
return errors.New("blocked: no reason")
|
return errors.New("blocked: no reason")
|
||||||
} else {
|
} else {
|
||||||
@@ -18,8 +18,8 @@ func (rl *Relay) handleEphemeral(ctx context.Context, evt *nostr.Event) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, oee := range rl.OnEphemeralEvent {
|
if nil != rl.OnEphemeralEvent {
|
||||||
oee(ctx, evt)
|
rl.OnEphemeralEvent(ctx, evt)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -11,16 +11,13 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
relay := khatru.NewRelay()
|
relay := khatru.NewRelay()
|
||||||
|
|
||||||
db := badger.BadgerBackend{Path: "/tmp/khatru-badgern-tmp"}
|
db := &badger.BadgerBackend{Path: "/tmp/khatru-badgern-tmp"}
|
||||||
if err := db.Init(); err != nil {
|
if err := db.Init(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
relay.UseEventstore(db)
|
||||||
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)
|
|
||||||
relay.Negentropy = true
|
relay.Negentropy = true
|
||||||
|
|
||||||
fmt.Println("running on :3334")
|
fmt.Println("running on :3334")
|
||||||
|
|||||||
@@ -12,17 +12,13 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
relay := khatru.NewRelay()
|
relay := khatru.NewRelay()
|
||||||
|
|
||||||
db := lmdb.LMDBBackend{Path: "/tmp/khatru-lmdb-tmp"}
|
db := &lmdb.LMDBBackend{Path: "/tmp/khatru-lmdb-tmp"}
|
||||||
os.MkdirAll(db.Path, 0o755)
|
os.MkdirAll(db.Path, 0o755)
|
||||||
if err := db.Init(); err != nil {
|
if err := db.Init(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
relay.UseEventstore(db)
|
||||||
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")
|
fmt.Println("running on :3334")
|
||||||
http.ListenAndServe(":3334", relay)
|
http.ListenAndServe(":3334", relay)
|
||||||
|
|||||||
@@ -19,11 +19,8 @@ func main() {
|
|||||||
if err := db.Init(); err != nil {
|
if err := db.Init(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
|
||||||
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
relay.UseEventstore(db)
|
||||||
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
|
||||||
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
|
||||||
relay.ReplaceEvent = append(relay.ReplaceEvent, db.ReplaceEvent)
|
|
||||||
|
|
||||||
bdb := &badger.BadgerBackend{Path: "/tmp/khatru-badger-blossom-tmp"}
|
bdb := &badger.BadgerBackend{Path: "/tmp/khatru-badger-blossom-tmp"}
|
||||||
if err := bdb.Init(); err != nil {
|
if err := bdb.Init(); err != nil {
|
||||||
@@ -31,15 +28,15 @@ func main() {
|
|||||||
}
|
}
|
||||||
bl := blossom.New(relay, "http://localhost:3334")
|
bl := blossom.New(relay, "http://localhost:3334")
|
||||||
bl.Store = blossom.EventStoreBlobIndexWrapper{Store: bdb, ServiceURL: bl.ServiceURL}
|
bl.Store = blossom.EventStoreBlobIndexWrapper{Store: bdb, ServiceURL: bl.ServiceURL}
|
||||||
bl.StoreBlob = append(bl.StoreBlob, func(ctx context.Context, sha256 string, body []byte) error {
|
bl.StoreBlob = func(ctx context.Context, sha256 string, body []byte) error {
|
||||||
fmt.Println("storing", sha256, len(body))
|
fmt.Println("storing", sha256, len(body))
|
||||||
return nil
|
return nil
|
||||||
})
|
}
|
||||||
bl.LoadBlob = append(bl.LoadBlob, func(ctx context.Context, sha256 string) (io.ReadSeeker, error) {
|
bl.LoadBlob = func(ctx context.Context, sha256 string) (io.ReadSeeker, error) {
|
||||||
fmt.Println("loading", sha256)
|
fmt.Println("loading", sha256)
|
||||||
blob := strings.NewReader("aaaaa")
|
blob := strings.NewReader("aaaaa")
|
||||||
return blob, nil
|
return blob, nil
|
||||||
})
|
}
|
||||||
|
|
||||||
fmt.Println("running on :3334")
|
fmt.Println("running on :3334")
|
||||||
http.ListenAndServe(":3334", relay)
|
http.ListenAndServe(":3334", relay)
|
||||||
|
|||||||
@@ -6,31 +6,28 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/eventstore/lmdb"
|
"fiatjaf.com/nostr/eventstore/lmdb"
|
||||||
"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() {
|
||||||
relay := khatru.NewRelay()
|
relay := khatru.NewRelay()
|
||||||
|
|
||||||
db := lmdb.LMDBBackend{Path: "/tmp/exclusive"}
|
db := &lmdb.LMDBBackend{Path: "/tmp/exclusive"}
|
||||||
os.MkdirAll(db.Path, 0o755)
|
os.MkdirAll(db.Path, 0o755)
|
||||||
if err := db.Init(); err != nil {
|
if err := db.Init(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
relay.UseEventstore(db)
|
||||||
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
|
||||||
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
|
||||||
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
|
||||||
|
|
||||||
relay.RejectEvent = append(relay.RejectEvent, policies.PreventTooManyIndexableTags(10, nil, nil))
|
relay.OnEvent = policies.PreventTooManyIndexableTags(10, nil, nil)
|
||||||
relay.RejectFilter = append(relay.RejectFilter, policies.NoComplexFilters)
|
relay.OnRequest = policies.NoComplexFilters
|
||||||
|
|
||||||
relay.OnEventSaved = append(relay.OnEventSaved, func(ctx context.Context, event *nostr.Event) {
|
relay.OnEventSaved = func(ctx context.Context, event nostr.Event) {
|
||||||
})
|
}
|
||||||
|
|
||||||
fmt.Println("running on :3334")
|
fmt.Println("running on :3334")
|
||||||
http.ListenAndServe(":3334", relay)
|
http.ListenAndServe(":3334", relay)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"iter"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -22,45 +23,36 @@ func main() {
|
|||||||
relay.Info.Icon = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fliquipedia.net%2Fcommons%2Fimages%2F3%2F35%2FSCProbe.jpg&f=1&nofb=1&ipt=0cbbfef25bce41da63d910e86c3c343e6c3b9d63194ca9755351bb7c2efa3359&ipo=images"
|
relay.Info.Icon = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fliquipedia.net%2Fcommons%2Fimages%2F3%2F35%2FSCProbe.jpg&f=1&nofb=1&ipt=0cbbfef25bce41da63d910e86c3c343e6c3b9d63194ca9755351bb7c2efa3359&ipo=images"
|
||||||
|
|
||||||
// you must bring your own storage scheme -- if you want to have any
|
// you must bring your own storage scheme -- if you want to have any
|
||||||
store := make(map[string]*nostr.Event, 120)
|
store := make(map[nostr.ID]nostr.Event, 120)
|
||||||
|
|
||||||
// set up the basic relay functions
|
// set up the basic relay functions
|
||||||
relay.StoreEvent = append(relay.StoreEvent,
|
relay.StoreEvent = func(ctx context.Context, event nostr.Event) error {
|
||||||
func(ctx context.Context, event *nostr.Event) error {
|
store[event.ID] = event
|
||||||
store[event.ID] = event
|
return nil
|
||||||
return nil
|
}
|
||||||
},
|
relay.QueryStored = func(ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event] {
|
||||||
)
|
return func(yield func(nostr.Event) bool) {
|
||||||
relay.QueryEvents = append(relay.QueryEvents,
|
for _, evt := range store {
|
||||||
func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
|
if filter.Matches(evt) {
|
||||||
ch := make(chan *nostr.Event)
|
yield(evt)
|
||||||
go func() {
|
|
||||||
for _, evt := range store {
|
|
||||||
if filter.Matches(evt) {
|
|
||||||
ch <- evt
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
close(ch)
|
}
|
||||||
}()
|
}
|
||||||
return ch, nil
|
}
|
||||||
},
|
relay.DeleteEvent = func(ctx context.Context, id nostr.ID) error {
|
||||||
)
|
delete(store, id)
|
||||||
relay.DeleteEvent = append(relay.DeleteEvent,
|
return nil
|
||||||
func(ctx context.Context, event *nostr.Event) error {
|
}
|
||||||
delete(store, event.ID)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// there are many other configurable things you can set
|
// there are many other configurable things you can set
|
||||||
relay.RejectEvent = append(relay.RejectEvent,
|
relay.OnEvent = policies.SeqEvent(
|
||||||
// built-in policies
|
// built-in policies
|
||||||
policies.ValidateKind,
|
policies.ValidateKind,
|
||||||
|
policies.PreventLargeTags(100),
|
||||||
|
|
||||||
// define your own policies
|
// define your own policies
|
||||||
policies.PreventLargeTags(100),
|
func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
|
||||||
func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
if event.PubKey == nostr.MustPubKeyFromHex("fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52") {
|
||||||
if event.PubKey == "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" {
|
|
||||||
return true, "we don't allow this person to write here"
|
return true, "we don't allow this person to write here"
|
||||||
}
|
}
|
||||||
return false, "" // anyone else can
|
return false, "" // anyone else can
|
||||||
@@ -68,13 +60,13 @@ func main() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// you can request auth by rejecting an event or a request with the prefix "auth-required: "
|
// you can request auth by rejecting an event or a request with the prefix "auth-required: "
|
||||||
relay.RejectFilter = append(relay.RejectFilter,
|
relay.OnRequest = policies.SeqRequest(
|
||||||
// built-in policies
|
// built-in policies
|
||||||
policies.NoComplexFilters,
|
policies.NoComplexFilters,
|
||||||
|
|
||||||
// define your own policies
|
// define your own policies
|
||||||
func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||||
if pubkey := khatru.GetAuthed(ctx); pubkey != "" {
|
if pubkey, isAuthed := khatru.GetAuthed(ctx); !isAuthed {
|
||||||
log.Printf("request from %s\n", pubkey)
|
log.Printf("request from %s\n", pubkey)
|
||||||
return false, ""
|
return false, ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,29 +12,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
db1 := slicestore.SliceStore{}
|
db1 := &slicestore.SliceStore{}
|
||||||
db1.Init()
|
db1.Init()
|
||||||
r1 := khatru.NewRelay()
|
r1 := khatru.NewRelay()
|
||||||
r1.StoreEvent = append(r1.StoreEvent, db1.SaveEvent)
|
r1.UseEventstore(db1)
|
||||||
r1.QueryEvents = append(r1.QueryEvents, db1.QueryEvents)
|
|
||||||
r1.CountEvents = append(r1.CountEvents, db1.CountEvents)
|
|
||||||
r1.DeleteEvent = append(r1.DeleteEvent, db1.DeleteEvent)
|
|
||||||
|
|
||||||
db2 := badger.BadgerBackend{DatabaseURL: "/tmp/t"}
|
db2 := &badger.BadgerBackend{Path: "/tmp/t"}
|
||||||
db2.Init()
|
db2.Init()
|
||||||
r2 := khatru.NewRelay()
|
r2 := khatru.NewRelay()
|
||||||
r2.StoreEvent = append(r2.StoreEvent, db2.SaveEvent)
|
r2.UseEventstore(db2)
|
||||||
r2.QueryEvents = append(r2.QueryEvents, db2.QueryEvents)
|
|
||||||
r2.CountEvents = append(r2.CountEvents, db2.CountEvents)
|
|
||||||
r2.DeleteEvent = append(r2.DeleteEvent, db2.DeleteEvent)
|
|
||||||
|
|
||||||
db3 := slicestore.SliceStore{}
|
db3 := &slicestore.SliceStore{}
|
||||||
db3.Init()
|
db3.Init()
|
||||||
r3 := khatru.NewRelay()
|
r3 := khatru.NewRelay()
|
||||||
r3.StoreEvent = append(r3.StoreEvent, db3.SaveEvent)
|
r3.UseEventstore(db3)
|
||||||
r3.QueryEvents = append(r3.QueryEvents, db3.QueryEvents)
|
|
||||||
r3.CountEvents = append(r3.CountEvents, db3.CountEvents)
|
|
||||||
r3.DeleteEvent = append(r3.DeleteEvent, db3.DeleteEvent)
|
|
||||||
|
|
||||||
router := khatru.NewRouter()
|
router := khatru.NewRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type expiringEvent struct {
|
type expiringEvent struct {
|
||||||
id string
|
id nostr.ID
|
||||||
expiresAt nostr.Timestamp
|
expiresAt nostr.Timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,13 +74,8 @@ func (em *expirationManager) initialScan(ctx context.Context) {
|
|||||||
|
|
||||||
// query all events
|
// query all events
|
||||||
ctx = context.WithValue(ctx, internalCallKey, struct{}{})
|
ctx = context.WithValue(ctx, internalCallKey, struct{}{})
|
||||||
for _, query := range em.relay.QueryEvents {
|
if nil != em.relay.QueryStored {
|
||||||
ch, err := query(ctx, nostr.Filter{})
|
for evt := range em.relay.QueryStored(ctx, nostr.Filter{}) {
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for evt := range ch {
|
|
||||||
if expiresAt := nip40.GetExpiration(evt.Tags); expiresAt != -1 {
|
if expiresAt := nip40.GetExpiration(evt.Tags); expiresAt != -1 {
|
||||||
heap.Push(&em.events, expiringEvent{
|
heap.Push(&em.events, expiringEvent{
|
||||||
id: evt.ID,
|
id: evt.ID,
|
||||||
@@ -109,23 +104,13 @@ func (em *expirationManager) checkExpiredEvents(ctx context.Context) {
|
|||||||
heap.Pop(&em.events)
|
heap.Pop(&em.events)
|
||||||
|
|
||||||
ctx := context.WithValue(ctx, internalCallKey, struct{}{})
|
ctx := context.WithValue(ctx, internalCallKey, struct{}{})
|
||||||
for _, query := range em.relay.QueryEvents {
|
if nil != em.relay.DeleteEvent {
|
||||||
ch, err := query(ctx, nostr.Filter{IDs: []string{next.id}})
|
em.relay.DeleteEvent(ctx, next.id)
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if evt := <-ch; evt != nil {
|
|
||||||
for _, del := range em.relay.DeleteEvent {
|
|
||||||
del(ctx, evt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (em *expirationManager) trackEvent(evt *nostr.Event) {
|
func (em *expirationManager) trackEvent(evt nostr.Event) {
|
||||||
if expiresAt := nip40.GetExpiration(evt.Tags); expiresAt != -1 {
|
if expiresAt := nip40.GetExpiration(evt.Tags); expiresAt != -1 {
|
||||||
em.mu.Lock()
|
em.mu.Lock()
|
||||||
heap.Push(&em.events, expiringEvent{
|
heap.Push(&em.events, expiringEvent{
|
||||||
@@ -136,7 +121,7 @@ func (em *expirationManager) trackEvent(evt *nostr.Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (em *expirationManager) removeEvent(id string) {
|
func (em *expirationManager) removeEvent(id nostr.ID) {
|
||||||
em.mu.Lock()
|
em.mu.Lock()
|
||||||
defer em.mu.Unlock()
|
defer em.mu.Unlock()
|
||||||
|
|
||||||
|
|||||||
@@ -176,8 +176,8 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// check NIP-70 protected
|
// check NIP-70 protected
|
||||||
if nip70.IsProtected(env.Event) {
|
if nip70.IsProtected(env.Event) {
|
||||||
authed := GetAuthed(ctx)
|
authed, isAuthed := GetAuthed(ctx)
|
||||||
if authed == "" {
|
if isAuthed {
|
||||||
RequestAuth(ctx)
|
RequestAuth(ctx)
|
||||||
ws.WriteJSON(nostr.OKEnvelope{
|
ws.WriteJSON(nostr.OKEnvelope{
|
||||||
EventID: env.Event.ID,
|
EventID: env.Event.ID,
|
||||||
@@ -213,20 +213,20 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if env.Event.Kind == 5 {
|
if env.Event.Kind == 5 {
|
||||||
// this always returns "blocked: " whenever it returns an error
|
// this always returns "blocked: " whenever it returns an error
|
||||||
writeErr = srl.handleDeleteRequest(ctx, &env.Event)
|
writeErr = srl.handleDeleteRequest(ctx, env.Event)
|
||||||
} else if nostr.IsEphemeralKind(env.Event.Kind) {
|
} else if nostr.IsEphemeralKind(env.Event.Kind) {
|
||||||
// this will also always return a prefixed reason
|
// this will also always return a prefixed reason
|
||||||
writeErr = srl.handleEphemeral(ctx, &env.Event)
|
writeErr = srl.handleEphemeral(ctx, env.Event)
|
||||||
} else {
|
} else {
|
||||||
// this will also always return a prefixed reason
|
// this will also always return a prefixed reason
|
||||||
skipBroadcast, writeErr = srl.handleNormal(ctx, &env.Event)
|
skipBroadcast, writeErr = srl.handleNormal(ctx, env.Event)
|
||||||
}
|
}
|
||||||
|
|
||||||
var reason string
|
var reason string
|
||||||
if writeErr == nil {
|
if writeErr == nil {
|
||||||
ok = true
|
ok = true
|
||||||
if !skipBroadcast {
|
if !skipBroadcast {
|
||||||
n := srl.notifyListeners(&env.Event)
|
n := srl.notifyListeners(env.Event)
|
||||||
|
|
||||||
// the number of notified listeners matters in ephemeral events
|
// the number of notified listeners matters in ephemeral events
|
||||||
if nostr.IsEphemeralKind(env.Event.Kind) {
|
if nostr.IsEphemeralKind(env.Event.Kind) {
|
||||||
@@ -247,12 +247,12 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: ok, Reason: reason})
|
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: ok, Reason: reason})
|
||||||
case *nostr.CountEnvelope:
|
case *nostr.CountEnvelope:
|
||||||
if rl.CountEvents == nil && rl.CountEventsHLL == nil {
|
if rl.Count == nil && rl.CountHLL == nil {
|
||||||
ws.WriteJSON(nostr.ClosedEnvelope{SubscriptionID: env.SubscriptionID, Reason: "unsupported: this relay does not support NIP-45"})
|
ws.WriteJSON(nostr.ClosedEnvelope{SubscriptionID: env.SubscriptionID, Reason: "unsupported: this relay does not support NIP-45"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var total int64
|
var total uint32
|
||||||
var hll *hyperloglog.HyperLogLog
|
var hll *hyperloglog.HyperLogLog
|
||||||
|
|
||||||
srl := rl
|
srl := rl
|
||||||
@@ -278,7 +278,7 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
case *nostr.ReqEnvelope:
|
case *nostr.ReqEnvelope:
|
||||||
eose := sync.WaitGroup{}
|
eose := sync.WaitGroup{}
|
||||||
eose.Add(len(env.Filters))
|
eose.Add(1)
|
||||||
|
|
||||||
// a context just for the "stored events" request handler
|
// a context just for the "stored events" request handler
|
||||||
reqCtx, cancelReqCtx := context.WithCancelCause(ctx)
|
reqCtx, cancelReqCtx := context.WithCancelCause(ctx)
|
||||||
@@ -287,24 +287,22 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
reqCtx = context.WithValue(reqCtx, subscriptionIdKey, env.SubscriptionID)
|
reqCtx = context.WithValue(reqCtx, subscriptionIdKey, env.SubscriptionID)
|
||||||
|
|
||||||
// handle each filter separately -- dispatching events as they're loaded from databases
|
// handle each filter separately -- dispatching events as they're loaded from databases
|
||||||
for _, filter := range env.Filters {
|
srl := rl
|
||||||
srl := rl
|
if rl.getSubRelayFromFilter != nil {
|
||||||
if rl.getSubRelayFromFilter != nil {
|
srl = rl.getSubRelayFromFilter(env.Filter)
|
||||||
srl = rl.getSubRelayFromFilter(filter)
|
}
|
||||||
}
|
err := srl.handleRequest(reqCtx, env.SubscriptionID, &eose, ws, env.Filter)
|
||||||
err := srl.handleRequest(reqCtx, env.SubscriptionID, &eose, ws, filter)
|
if err != nil {
|
||||||
if err != nil {
|
// fail everything if any filter is rejected
|
||||||
// fail everything if any filter is rejected
|
reason := err.Error()
|
||||||
reason := err.Error()
|
if strings.HasPrefix(reason, "auth-required:") {
|
||||||
if strings.HasPrefix(reason, "auth-required:") {
|
RequestAuth(ctx)
|
||||||
RequestAuth(ctx)
|
|
||||||
}
|
|
||||||
ws.WriteJSON(nostr.ClosedEnvelope{SubscriptionID: env.SubscriptionID, Reason: reason})
|
|
||||||
cancelReqCtx(errors.New("filter rejected"))
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
rl.addListener(ws, env.SubscriptionID, srl, filter, cancelReqCtx)
|
|
||||||
}
|
}
|
||||||
|
ws.WriteJSON(nostr.ClosedEnvelope{SubscriptionID: env.SubscriptionID, Reason: reason})
|
||||||
|
cancelReqCtx(errors.New("filter rejected"))
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rl.addListener(ws, env.SubscriptionID, srl, env.Filter, cancelReqCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -317,7 +315,7 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
rl.removeListenerId(ws, id)
|
rl.removeListenerId(ws, id)
|
||||||
case *nostr.AuthEnvelope:
|
case *nostr.AuthEnvelope:
|
||||||
wsBaseUrl := strings.Replace(rl.getBaseURL(r), "http", "ws", 1)
|
wsBaseUrl := strings.Replace(rl.getBaseURL(r), "http", "ws", 1)
|
||||||
if pubkey, ok := nip42.ValidateAuthEvent(&env.Event, ws.Challenge, wsBaseUrl); ok {
|
if pubkey, ok := nip42.ValidateAuthEvent(env.Event, ws.Challenge, wsBaseUrl); ok {
|
||||||
ws.AuthedPublicKey = pubkey
|
ws.AuthedPublicKey = pubkey
|
||||||
ws.authLock.Lock()
|
ws.authLock.Lock()
|
||||||
if ws.Authed != nil {
|
if ws.Authed != nil {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package khatru
|
package khatru
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -10,7 +11,7 @@ import (
|
|||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
var privateMasks = func() []net.IPNet {
|
var privateMasks = func() []net.IPNet {
|
||||||
|
|||||||
@@ -133,17 +133,17 @@ func (rl *Relay) removeClientAndListeners(ws *WebSocket) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns how many listeners were notified
|
// returns how many listeners were notified
|
||||||
func (rl *Relay) notifyListeners(event *nostr.Event) int {
|
func (rl *Relay) notifyListeners(event nostr.Event) int {
|
||||||
count := 0
|
count := 0
|
||||||
listenersloop:
|
listenersloop:
|
||||||
for _, listener := range rl.listeners {
|
for _, listener := range rl.listeners {
|
||||||
if listener.filter.Matches(event) {
|
if listener.filter.Matches(event) {
|
||||||
for _, pb := range rl.PreventBroadcast {
|
if nil != rl.PreventBroadcast {
|
||||||
if pb(listener.ws, event) {
|
if rl.PreventBroadcast(listener.ws, event) {
|
||||||
continue listenersloop
|
continue listenersloop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
listener.ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &listener.id, Event: *event})
|
listener.ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &listener.id, Event: event})
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func FuzzRandomListenerClientRemoving(f *testing.F) {
|
|||||||
|
|
||||||
rl := NewRelay()
|
rl := NewRelay()
|
||||||
|
|
||||||
f := nostr.Filter{Kinds: []int{1}}
|
f := nostr.Filter{Kinds: []uint16{1}}
|
||||||
cancel := func(cause error) {}
|
cancel := func(cause error) {}
|
||||||
|
|
||||||
websockets := make([]*WebSocket, 0, totalWebsockets*baseSubs)
|
websockets := make([]*WebSocket, 0, totalWebsockets*baseSubs)
|
||||||
@@ -71,7 +71,7 @@ func FuzzRandomListenerIdRemoving(f *testing.F) {
|
|||||||
|
|
||||||
rl := NewRelay()
|
rl := NewRelay()
|
||||||
|
|
||||||
f := nostr.Filter{Kinds: []int{1}}
|
f := nostr.Filter{Kinds: []uint16{1}}
|
||||||
cancel := func(cause error) {}
|
cancel := func(cause error) {}
|
||||||
websockets := make([]*WebSocket, 0, totalWebsockets)
|
websockets := make([]*WebSocket, 0, totalWebsockets)
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ func FuzzRouterListenersPabloCrash(f *testing.F) {
|
|||||||
rl.clients[ws] = make([]listenerSpec, 0, subIterations)
|
rl.clients[ws] = make([]listenerSpec, 0, subIterations)
|
||||||
}
|
}
|
||||||
|
|
||||||
f := nostr.Filter{Kinds: []int{1}}
|
f := nostr.Filter{Kinds: []uint16{1}}
|
||||||
cancel := func(cause error) {}
|
cancel := func(cause error) {}
|
||||||
|
|
||||||
type wsid struct {
|
type wsid struct {
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ func TestListenerSetupAndRemoveOnce(t *testing.T) {
|
|||||||
ws1 := &WebSocket{}
|
ws1 := &WebSocket{}
|
||||||
ws2 := &WebSocket{}
|
ws2 := &WebSocket{}
|
||||||
|
|
||||||
f1 := nostr.Filter{Kinds: []int{1}}
|
f1 := nostr.Filter{Kinds: []uint16{1}}
|
||||||
f2 := nostr.Filter{Kinds: []int{2}}
|
f2 := nostr.Filter{Kinds: []uint16{2}}
|
||||||
f3 := nostr.Filter{Kinds: []int{3}}
|
f3 := nostr.Filter{Kinds: []uint16{3}}
|
||||||
|
|
||||||
rl.clients[ws1] = nil
|
rl.clients[ws1] = nil
|
||||||
rl.clients[ws2] = nil
|
rl.clients[ws2] = nil
|
||||||
@@ -86,9 +86,9 @@ func TestListenerMoreConvolutedCase(t *testing.T) {
|
|||||||
ws3 := &WebSocket{}
|
ws3 := &WebSocket{}
|
||||||
ws4 := &WebSocket{}
|
ws4 := &WebSocket{}
|
||||||
|
|
||||||
f1 := nostr.Filter{Kinds: []int{1}}
|
f1 := nostr.Filter{Kinds: []uint16{1}}
|
||||||
f2 := nostr.Filter{Kinds: []int{2}}
|
f2 := nostr.Filter{Kinds: []uint16{2}}
|
||||||
f3 := nostr.Filter{Kinds: []int{3}}
|
f3 := nostr.Filter{Kinds: []uint16{3}}
|
||||||
|
|
||||||
rl.clients[ws1] = nil
|
rl.clients[ws1] = nil
|
||||||
rl.clients[ws2] = nil
|
rl.clients[ws2] = nil
|
||||||
@@ -205,9 +205,9 @@ func TestListenerMoreStuffWithMultipleRelays(t *testing.T) {
|
|||||||
ws3 := &WebSocket{}
|
ws3 := &WebSocket{}
|
||||||
ws4 := &WebSocket{}
|
ws4 := &WebSocket{}
|
||||||
|
|
||||||
f1 := nostr.Filter{Kinds: []int{1}}
|
f1 := nostr.Filter{Kinds: []uint16{1}}
|
||||||
f2 := nostr.Filter{Kinds: []int{2}}
|
f2 := nostr.Filter{Kinds: []uint16{2}}
|
||||||
f3 := nostr.Filter{Kinds: []int{3}}
|
f3 := nostr.Filter{Kinds: []uint16{3}}
|
||||||
|
|
||||||
rlx := NewRelay()
|
rlx := NewRelay()
|
||||||
rly := NewRelay()
|
rly := NewRelay()
|
||||||
@@ -424,7 +424,7 @@ func TestListenerMoreStuffWithMultipleRelays(t *testing.T) {
|
|||||||
func TestRandomListenerClientRemoving(t *testing.T) {
|
func TestRandomListenerClientRemoving(t *testing.T) {
|
||||||
rl := NewRelay()
|
rl := NewRelay()
|
||||||
|
|
||||||
f := nostr.Filter{Kinds: []int{1}}
|
f := nostr.Filter{Kinds: []uint16{1}}
|
||||||
cancel := func(cause error) {}
|
cancel := func(cause error) {}
|
||||||
|
|
||||||
websockets := make([]*WebSocket, 0, 20)
|
websockets := make([]*WebSocket, 0, 20)
|
||||||
@@ -463,7 +463,7 @@ func TestRandomListenerClientRemoving(t *testing.T) {
|
|||||||
func TestRandomListenerIdRemoving(t *testing.T) {
|
func TestRandomListenerIdRemoving(t *testing.T) {
|
||||||
rl := NewRelay()
|
rl := NewRelay()
|
||||||
|
|
||||||
f := nostr.Filter{Kinds: []int{1}}
|
f := nostr.Filter{Kinds: []uint16{1}}
|
||||||
cancel := func(cause error) {}
|
cancel := func(cause error) {}
|
||||||
|
|
||||||
websockets := make([]*WebSocket, 0, 20)
|
websockets := make([]*WebSocket, 0, 20)
|
||||||
@@ -531,7 +531,7 @@ func TestRouterListenersPabloCrash(t *testing.T) {
|
|||||||
rl.clients[ws2] = nil
|
rl.clients[ws2] = nil
|
||||||
rl.clients[ws3] = nil
|
rl.clients[ws3] = nil
|
||||||
|
|
||||||
f := nostr.Filter{Kinds: []int{1}}
|
f := nostr.Filter{Kinds: []uint16{1}}
|
||||||
cancel := func(cause error) {}
|
cancel := func(cause error) {}
|
||||||
|
|
||||||
rl.addListener(ws1, ":1", rla, f, cancel)
|
rl.addListener(ws1, ":1", rla, f, cancel)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"fiatjaf.com/nostr/eventstore"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/nip77/negentropy"
|
"fiatjaf.com/nostr/nip77/negentropy"
|
||||||
"fiatjaf.com/nostr/nip77/negentropy/storage/vector"
|
"fiatjaf.com/nostr/nip77/negentropy/storage/vector"
|
||||||
@@ -17,33 +16,22 @@ type NegentropySession struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rl *Relay) startNegentropySession(ctx context.Context, filter nostr.Filter) (*vector.Vector, error) {
|
func (rl *Relay) startNegentropySession(ctx context.Context, filter nostr.Filter) (*vector.Vector, error) {
|
||||||
ctx = eventstore.SetNegentropy(ctx)
|
|
||||||
|
|
||||||
// do the same overwrite/reject flow we do in normal REQs
|
|
||||||
for _, ovw := range rl.OverwriteFilter {
|
|
||||||
ovw(ctx, &filter)
|
|
||||||
}
|
|
||||||
if filter.LimitZero {
|
if filter.LimitZero {
|
||||||
return nil, fmt.Errorf("invalid limit 0")
|
return nil, fmt.Errorf("invalid limit 0")
|
||||||
}
|
}
|
||||||
for _, reject := range rl.RejectFilter {
|
|
||||||
if reject, msg := reject(ctx, filter); reject {
|
ctx = SetNegentropy(ctx)
|
||||||
|
|
||||||
|
if nil != rl.OnRequest {
|
||||||
|
if reject, msg := rl.OnRequest(ctx, filter); reject {
|
||||||
return nil, errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
|
return nil, errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch events and add them to a negentropy Vector store
|
// fetch events and add them to a negentropy Vector store
|
||||||
vec := vector.New()
|
vec := vector.New()
|
||||||
for _, query := range rl.QueryEvents {
|
if nil != rl.QueryStored {
|
||||||
ch, err := query(ctx, filter)
|
for event := range rl.QueryStored(ctx, filter) {
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
} else if ch == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for event := range ch {
|
|
||||||
// since the goal here is to sync databases we won't do fancy stuff like overwrite events
|
|
||||||
vec.Insert(event.CreatedAt, event.ID)
|
vec.Insert(event.CreatedAt, event.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,3 +39,13 @@ func (rl *Relay) startNegentropySession(ctx context.Context, filter nostr.Filter
|
|||||||
|
|
||||||
return vec, nil
|
return vec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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{}{})
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ func (rl *Relay) HandleNIP11(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
info := *rl.Info
|
info := *rl.Info
|
||||||
|
|
||||||
if len(rl.DeleteEvent) > 0 {
|
if nil != rl.DeleteEvent {
|
||||||
info.AddSupportedNIP(9)
|
info.AddSupportedNIP(9)
|
||||||
}
|
}
|
||||||
if len(rl.CountEvents) > 0 {
|
if nil != rl.Count {
|
||||||
info.AddSupportedNIP(45)
|
info.AddSupportedNIP(45)
|
||||||
}
|
}
|
||||||
if rl.Negentropy {
|
if rl.Negentropy {
|
||||||
@@ -30,8 +30,8 @@ func (rl *Relay) HandleNIP11(w http.ResponseWriter, r *http.Request) {
|
|||||||
info.Banner = strings.TrimSuffix(baseURL, "/") + "/" + strings.TrimPrefix(info.Banner, "/")
|
info.Banner = strings.TrimSuffix(baseURL, "/") + "/" + strings.TrimPrefix(info.Banner, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ovw := range rl.OverwriteRelayInformation {
|
if nil != rl.OverwriteRelayInformation {
|
||||||
info = ovw(r.Context(), r, info)
|
info = rl.OverwriteRelayInformation(r.Context(), r, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(info)
|
json.NewEncoder(w).Encode(info)
|
||||||
|
|||||||
@@ -16,25 +16,25 @@ import (
|
|||||||
//
|
//
|
||||||
// If ignoreKinds is given this restriction will not apply to these kinds (useful for allowing a bigger).
|
// If ignoreKinds is given this restriction will not apply to these kinds (useful for allowing a bigger).
|
||||||
// If onlyKinds is given then all other kinds will be ignored.
|
// If onlyKinds is given then all other kinds will be ignored.
|
||||||
func PreventTooManyIndexableTags(max int, ignoreKinds []int, onlyKinds []int) func(context.Context, *nostr.Event) (bool, string) {
|
func PreventTooManyIndexableTags(max int, ignoreKinds []uint16, onlyKinds []uint16) func(context.Context, nostr.Event) (bool, string) {
|
||||||
slices.Sort(ignoreKinds)
|
slices.Sort(ignoreKinds)
|
||||||
slices.Sort(onlyKinds)
|
slices.Sort(onlyKinds)
|
||||||
|
|
||||||
ignore := func(kind int) bool { return false }
|
ignore := func(kind uint16) bool { return false }
|
||||||
if len(ignoreKinds) > 0 {
|
if len(ignoreKinds) > 0 {
|
||||||
ignore = func(kind int) bool {
|
ignore = func(kind uint16) bool {
|
||||||
_, isIgnored := slices.BinarySearch(ignoreKinds, kind)
|
_, isIgnored := slices.BinarySearch(ignoreKinds, kind)
|
||||||
return isIgnored
|
return isIgnored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(onlyKinds) > 0 {
|
if len(onlyKinds) > 0 {
|
||||||
ignore = func(kind int) bool {
|
ignore = func(kind uint16) bool {
|
||||||
_, isApplicable := slices.BinarySearch(onlyKinds, kind)
|
_, isApplicable := slices.BinarySearch(onlyKinds, kind)
|
||||||
return !isApplicable
|
return !isApplicable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
return func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
|
||||||
if ignore(event.Kind) {
|
if ignore(event.Kind) {
|
||||||
return false, ""
|
return false, ""
|
||||||
}
|
}
|
||||||
@@ -53,8 +53,8 @@ func PreventTooManyIndexableTags(max int, ignoreKinds []int, onlyKinds []int) fu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PreventLargeTags rejects events that have indexable tag values greater than maxTagValueLen.
|
// PreventLargeTags rejects events that have indexable tag values greater than maxTagValueLen.
|
||||||
func PreventLargeTags(maxTagValueLen int) func(context.Context, *nostr.Event) (bool, string) {
|
func PreventLargeTags(maxTagValueLen int) func(context.Context, nostr.Event) (bool, string) {
|
||||||
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
return func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
|
||||||
for _, tag := range event.Tags {
|
for _, tag := range event.Tags {
|
||||||
if len(tag) > 1 && len(tag[0]) == 1 {
|
if len(tag) > 1 && len(tag[0]) == 1 {
|
||||||
if len(tag[1]) > maxTagValueLen {
|
if len(tag[1]) > maxTagValueLen {
|
||||||
@@ -68,11 +68,11 @@ func PreventLargeTags(maxTagValueLen int) func(context.Context, *nostr.Event) (b
|
|||||||
|
|
||||||
// RestrictToSpecifiedKinds returns a function that can be used as a RejectFilter that will reject
|
// RestrictToSpecifiedKinds returns a function that can be used as a RejectFilter that will reject
|
||||||
// any events with kinds different than the specified ones.
|
// any events with kinds different than the specified ones.
|
||||||
func RestrictToSpecifiedKinds(allowEphemeral bool, kinds ...uint16) func(context.Context, *nostr.Event) (bool, string) {
|
func RestrictToSpecifiedKinds(allowEphemeral bool, kinds ...uint16) func(context.Context, nostr.Event) (bool, string) {
|
||||||
// sort the kinds in increasing order
|
// sort the kinds in increasing order
|
||||||
slices.Sort(kinds)
|
slices.Sort(kinds)
|
||||||
|
|
||||||
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
return func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
|
||||||
if allowEphemeral && nostr.IsEphemeralKind(event.Kind) {
|
if allowEphemeral && nostr.IsEphemeralKind(event.Kind) {
|
||||||
return false, ""
|
return false, ""
|
||||||
}
|
}
|
||||||
@@ -85,9 +85,9 @@ func RestrictToSpecifiedKinds(allowEphemeral bool, kinds ...uint16) func(context
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func PreventTimestampsInThePast(threshold time.Duration) func(context.Context, *nostr.Event) (bool, string) {
|
func PreventTimestampsInThePast(threshold time.Duration) func(context.Context, nostr.Event) (bool, string) {
|
||||||
thresholdSeconds := nostr.Timestamp(threshold.Seconds())
|
thresholdSeconds := nostr.Timestamp(threshold.Seconds())
|
||||||
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
return func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
|
||||||
if nostr.Now()-event.CreatedAt > thresholdSeconds {
|
if nostr.Now()-event.CreatedAt > thresholdSeconds {
|
||||||
return true, "event too old"
|
return true, "event too old"
|
||||||
}
|
}
|
||||||
@@ -95,9 +95,9 @@ func PreventTimestampsInThePast(threshold time.Duration) func(context.Context, *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func PreventTimestampsInTheFuture(threshold time.Duration) func(context.Context, *nostr.Event) (bool, string) {
|
func PreventTimestampsInTheFuture(threshold time.Duration) func(context.Context, nostr.Event) (bool, string) {
|
||||||
thresholdSeconds := nostr.Timestamp(threshold.Seconds())
|
thresholdSeconds := nostr.Timestamp(threshold.Seconds())
|
||||||
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
return func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
|
||||||
if event.CreatedAt-nostr.Now() > thresholdSeconds {
|
if event.CreatedAt-nostr.Now() > thresholdSeconds {
|
||||||
return true, "event too much in the future"
|
return true, "event too much in the future"
|
||||||
}
|
}
|
||||||
@@ -105,12 +105,12 @@ func PreventTimestampsInTheFuture(threshold time.Duration) func(context.Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RejectEventsWithBase64Media(ctx context.Context, evt *nostr.Event) (bool, string) {
|
func RejectEventsWithBase64Media(ctx context.Context, evt nostr.Event) (bool, string) {
|
||||||
return strings.Contains(evt.Content, "data:image/") || strings.Contains(evt.Content, "data:video/"), "event with base64 media"
|
return strings.Contains(evt.Content, "data:image/") || strings.Contains(evt.Content, "data:video/"), "event with base64 media"
|
||||||
}
|
}
|
||||||
|
|
||||||
func OnlyAllowNIP70ProtectedEvents(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
func OnlyAllowNIP70ProtectedEvents(ctx context.Context, event nostr.Event) (reject bool, msg string) {
|
||||||
if nip70.IsProtected(*event) {
|
if nip70.IsProtected(event) {
|
||||||
return false, ""
|
return false, ""
|
||||||
}
|
}
|
||||||
return true, "blocked: we only accept events protected with the nip70 \"-\" tag"
|
return true, "blocked: we only accept events protected with the nip70 \"-\" tag"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"fiatjaf.com/nostr/khatru"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/khatru"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NoComplexFilters disallows filters with more than 2 tags.
|
// NoComplexFilters disallows filters with more than 2 tags.
|
||||||
@@ -21,7 +21,7 @@ func NoComplexFilters(ctx context.Context, filter nostr.Filter) (reject bool, ms
|
|||||||
|
|
||||||
// MustAuth requires all subscribers to be authenticated
|
// MustAuth requires all subscribers to be authenticated
|
||||||
func MustAuth(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
func MustAuth(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||||
if khatru.GetAuthed(ctx) == "" {
|
if _, isAuthed := khatru.GetAuthed(ctx); !isAuthed {
|
||||||
return true, "auth-required: all requests must be authenticated"
|
return true, "auth-required: all requests must be authenticated"
|
||||||
}
|
}
|
||||||
return false, ""
|
return false, ""
|
||||||
@@ -63,7 +63,7 @@ func RemoveSearchQueries(ctx context.Context, filter *nostr.Filter) {
|
|||||||
func RemoveAllButKinds(kinds ...uint16) func(context.Context, *nostr.Filter) {
|
func RemoveAllButKinds(kinds ...uint16) func(context.Context, *nostr.Filter) {
|
||||||
return func(ctx context.Context, filter *nostr.Filter) {
|
return func(ctx context.Context, filter *nostr.Filter) {
|
||||||
if n := len(filter.Kinds); n > 0 {
|
if n := len(filter.Kinds); n > 0 {
|
||||||
newKinds := make([]int, 0, n)
|
newKinds := make([]uint16, 0, n)
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
if k := filter.Kinds[i]; slices.Contains(kinds, uint16(k)) {
|
if k := filter.Kinds[i]; slices.Contains(kinds, uint16(k)) {
|
||||||
newKinds = append(newKinds, k)
|
newKinds = append(newKinds, k)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ValidateKind(ctx context.Context, evt *nostr.Event) (bool, string) {
|
func ValidateKind(ctx context.Context, evt nostr.Event) (bool, string) {
|
||||||
switch evt.Kind {
|
switch evt.Kind {
|
||||||
case 0:
|
case 0:
|
||||||
var m struct {
|
var m struct {
|
||||||
|
|||||||
47
khatru/policies/multi.go
Normal file
47
khatru/policies/multi.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package policies
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SeqEvent(
|
||||||
|
funcs ...func(ctx context.Context, evt nostr.Event) (bool, string),
|
||||||
|
) func(context.Context, nostr.Event) (reject bool, reason string) {
|
||||||
|
return func(ctx context.Context, evt nostr.Event) (reject bool, reason string) {
|
||||||
|
for _, fn := range funcs {
|
||||||
|
reject, reason := fn(ctx, evt)
|
||||||
|
if reject {
|
||||||
|
return reject, reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SeqStore(funcs ...func(ctx context.Context, evt nostr.Event) error) func(context.Context, nostr.Event) error {
|
||||||
|
return func(ctx context.Context, evt nostr.Event) error {
|
||||||
|
for _, fn := range funcs {
|
||||||
|
err := fn(ctx, evt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SeqRequest(
|
||||||
|
funcs ...func(ctx context.Context, evt nostr.Filter) (bool, string),
|
||||||
|
) func(context.Context, nostr.Filter) (reject bool, reason string) {
|
||||||
|
return func(ctx context.Context, evt nostr.Filter) (reject bool, reason string) {
|
||||||
|
for _, fn := range funcs {
|
||||||
|
reject, reason := fn(ctx, evt)
|
||||||
|
if reject {
|
||||||
|
return reject, reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package policies
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"fiatjaf.com/nostr/khatru"
|
|
||||||
"fiatjaf.com/nostr"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RejectKind04Snoopers prevents reading NIP-04 messages from people not involved in the conversation.
|
|
||||||
func RejectKind04Snoopers(ctx context.Context, filter nostr.Filter) (bool, string) {
|
|
||||||
// prevent kind-4 events from being returned to unauthed users,
|
|
||||||
// only when authentication is a thing
|
|
||||||
if !slices.Contains(filter.Kinds, 4) {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
ws := khatru.GetConnection(ctx)
|
|
||||||
senders := filter.Authors
|
|
||||||
receivers, _ := filter.Tags["p"]
|
|
||||||
switch {
|
|
||||||
case ws.AuthedPublicKey == "":
|
|
||||||
// not authenticated
|
|
||||||
return true, "restricted: this relay does not serve kind-4 to unauthenticated users, does your client implement NIP-42?"
|
|
||||||
case len(senders) == 1 && len(receivers) < 2 && (senders[0] == ws.AuthedPublicKey):
|
|
||||||
// allowed filter: ws.authed is sole sender (filter specifies one or all receivers)
|
|
||||||
return false, ""
|
|
||||||
case len(receivers) == 1 && len(senders) < 2 && (receivers[0] == ws.AuthedPublicKey):
|
|
||||||
// allowed filter: ws.authed is sole receiver (filter specifies one or all senders)
|
|
||||||
return false, ""
|
|
||||||
default:
|
|
||||||
// restricted filter: do not return any events,
|
|
||||||
// even if other elements in filters array were not restricted).
|
|
||||||
// client should know better.
|
|
||||||
return true, "restricted: authenticated user does not have authorization for requested filters."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,14 +5,14 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fiatjaf.com/nostr/khatru"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/khatru"
|
||||||
)
|
)
|
||||||
|
|
||||||
func EventIPRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens int) func(ctx context.Context, _ *nostr.Event) (reject bool, msg string) {
|
func EventIPRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens int) func(ctx context.Context, _ nostr.Event) (reject bool, msg string) {
|
||||||
rl := startRateLimitSystem[string](tokensPerInterval, interval, maxTokens)
|
rl := startRateLimitSystem[string](tokensPerInterval, interval, maxTokens)
|
||||||
|
|
||||||
return func(ctx context.Context, _ *nostr.Event) (reject bool, msg string) {
|
return func(ctx context.Context, _ nostr.Event) (reject bool, msg string) {
|
||||||
ip := khatru.GetIP(ctx)
|
ip := khatru.GetIP(ctx)
|
||||||
if ip == "" {
|
if ip == "" {
|
||||||
return false, ""
|
return false, ""
|
||||||
@@ -21,11 +21,11 @@ func EventIPRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func EventPubKeyRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens int) func(ctx context.Context, _ *nostr.Event) (reject bool, msg string) {
|
func EventPubKeyRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens int) func(ctx context.Context, _ nostr.Event) (reject bool, msg string) {
|
||||||
rl := startRateLimitSystem[string](tokensPerInterval, interval, maxTokens)
|
rl := startRateLimitSystem[string](tokensPerInterval, interval, maxTokens)
|
||||||
|
|
||||||
return func(ctx context.Context, evt *nostr.Event) (reject bool, msg string) {
|
return func(ctx context.Context, evt nostr.Event) (reject bool, msg string) {
|
||||||
return rl(evt.PubKey), "rate-limited: slow down, please"
|
return rl(evt.PubKey.Hex()), "rate-limited: slow down, please"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ApplySaneDefaults(relay *khatru.Relay) {
|
func ApplySaneDefaults(relay *khatru.Relay) {
|
||||||
relay.RejectEvent = append(relay.RejectEvent,
|
relay.OnEvent = SeqEvent(
|
||||||
RejectEventsWithBase64Media,
|
RejectEventsWithBase64Media,
|
||||||
EventIPRateLimiter(2, time.Minute*3, 10),
|
EventIPRateLimiter(2, time.Minute*3, 10),
|
||||||
)
|
)
|
||||||
|
|
||||||
relay.RejectFilter = append(relay.RejectFilter,
|
relay.OnRequest = SeqRequest(
|
||||||
NoComplexFilters,
|
NoComplexFilters,
|
||||||
FilterIPRateLimiter(20, time.Minute, 100),
|
FilterIPRateLimiter(20, time.Minute, 100),
|
||||||
)
|
)
|
||||||
|
|
||||||
relay.RejectConnection = append(relay.RejectConnection,
|
relay.RejectConnection = ConnectionRateLimiter(1, time.Minute*5, 100)
|
||||||
ConnectionRateLimiter(1, time.Minute*5, 100),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/eventstore"
|
||||||
"fiatjaf.com/nostr/nip11"
|
"fiatjaf.com/nostr/nip11"
|
||||||
"fiatjaf.com/nostr/nip45/hyperloglog"
|
"fiatjaf.com/nostr/nip45/hyperloglog"
|
||||||
"github.com/fasthttp/websocket"
|
"github.com/fasthttp/websocket"
|
||||||
@@ -57,23 +58,22 @@ 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)
|
OnEvent func(ctx context.Context, event nostr.Event) (reject bool, msg string)
|
||||||
OverwriteDeletionOutcome func(ctx context.Context, target *nostr.Event, deletion *nostr.Event) (acceptDeletion bool, msg string)
|
|
||||||
StoreEvent func(ctx context.Context, event nostr.Event) error
|
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, id nostr.ID) error
|
DeleteEvent func(ctx context.Context, id nostr.ID) 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)
|
OnRequest func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
|
||||||
RejectCountFilter func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
|
OnCountFilter func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
|
||||||
QueryEvents func(ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event]
|
QueryStored func(ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event]
|
||||||
CountEvents func(ctx context.Context, filter nostr.Filter) (uint32, error)
|
Count func(ctx context.Context, filter nostr.Filter) (uint32, error)
|
||||||
CountEventsHLL func(ctx context.Context, filter nostr.Filter, offset int) (uint32, *hyperloglog.HyperLogLog, error)
|
CountHLL func(ctx context.Context, filter nostr.Filter, offset int) (uint32, *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
|
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
|
||||||
@@ -117,6 +117,24 @@ type Relay struct {
|
|||||||
expirationManager *expirationManager
|
expirationManager *expirationManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rl *Relay) UseEventstore(store eventstore.Store) {
|
||||||
|
rl.QueryStored = func(ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event] {
|
||||||
|
return store.QueryEvents(filter)
|
||||||
|
}
|
||||||
|
rl.Count = func(ctx context.Context, filter nostr.Filter) (uint32, error) {
|
||||||
|
return store.CountEvents(filter)
|
||||||
|
}
|
||||||
|
rl.StoreEvent = func(ctx context.Context, event nostr.Event) error {
|
||||||
|
return store.SaveEvent(event)
|
||||||
|
}
|
||||||
|
rl.ReplaceEvent = func(ctx context.Context, event nostr.Event) error {
|
||||||
|
return store.ReplaceEvent(event)
|
||||||
|
}
|
||||||
|
rl.DeleteEvent = func(ctx context.Context, id nostr.ID) error {
|
||||||
|
return store.DeleteEvent(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (rl *Relay) getBaseURL(r *http.Request) string {
|
func (rl *Relay) getBaseURL(r *http.Request) string {
|
||||||
if rl.ServiceURL != "" {
|
if rl.ServiceURL != "" {
|
||||||
return rl.ServiceURL
|
return rl.ServiceURL
|
||||||
|
|||||||
@@ -7,41 +7,31 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fiatjaf.com/nostr/eventstore/slicestore"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/eventstore/slicestore"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBasicRelayFunctionality(t *testing.T) {
|
func TestBasicRelayFunctionality(t *testing.T) {
|
||||||
// setup relay with in-memory store
|
// setup relay with in-memory store
|
||||||
relay := NewRelay()
|
relay := NewRelay()
|
||||||
store := slicestore.SliceStore{}
|
store := &slicestore.SliceStore{}
|
||||||
store.Init()
|
store.Init()
|
||||||
relay.StoreEvent = append(relay.StoreEvent, store.SaveEvent)
|
|
||||||
relay.QueryEvents = append(relay.QueryEvents, store.QueryEvents)
|
relay.UseEventstore(store)
|
||||||
relay.DeleteEvent = append(relay.DeleteEvent, store.DeleteEvent)
|
|
||||||
|
|
||||||
// start test server
|
// start test server
|
||||||
server := httptest.NewServer(relay)
|
server := httptest.NewServer(relay)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
// create test keys
|
// create test keys
|
||||||
sk1 := nostr.GeneratePrivateKey()
|
sk1 := nostr.Generate()
|
||||||
pk1, err := nostr.GetPublicKey(sk1)
|
pk1 := nostr.GetPublicKey(sk1)
|
||||||
if err != nil {
|
sk2 := nostr.Generate()
|
||||||
t.Fatalf("Failed to get public key 1: %v", err)
|
pk2 := nostr.GetPublicKey(sk2)
|
||||||
}
|
|
||||||
sk2 := nostr.GeneratePrivateKey()
|
|
||||||
pk2, err := nostr.GetPublicKey(sk2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get public key 2: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper to create signed events
|
// helper to create signed events
|
||||||
createEvent := func(sk string, kind int, content string, tags nostr.Tags) nostr.Event {
|
createEvent := func(sk nostr.SecretKey, kind uint16, content string, tags nostr.Tags) nostr.Event {
|
||||||
pk, err := nostr.GetPublicKey(sk)
|
pk := nostr.GetPublicKey(sk)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get public key: %v", err)
|
|
||||||
}
|
|
||||||
evt := nostr.Event{
|
evt := nostr.Event{
|
||||||
PubKey: pk,
|
PubKey: pk,
|
||||||
CreatedAt: nostr.Now(),
|
CreatedAt: nostr.Now(),
|
||||||
@@ -55,13 +45,13 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
|
|
||||||
// connect two test clients
|
// connect two test clients
|
||||||
url := "ws" + server.URL[4:]
|
url := "ws" + server.URL[4:]
|
||||||
client1, err := nostr.RelayConnect(context.Background(), url)
|
client1, err := nostr.RelayConnect(t.Context(), url, nostr.RelayOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to connect client1: %v", err)
|
t.Fatalf("failed to connect client1: %v", err)
|
||||||
}
|
}
|
||||||
defer client1.Close()
|
defer client1.Close()
|
||||||
|
|
||||||
client2, err := nostr.RelayConnect(context.Background(), url)
|
client2, err := nostr.RelayConnect(t.Context(), url, nostr.RelayOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to connect client2: %v", err)
|
t.Fatalf("failed to connect client2: %v", err)
|
||||||
}
|
}
|
||||||
@@ -69,7 +59,7 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
|
|
||||||
// test 1: store and query events
|
// test 1: store and query events
|
||||||
t.Run("store and query events", func(t *testing.T) {
|
t.Run("store and query events", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
evt1 := createEvent(sk1, 1, "hello world", nil)
|
evt1 := createEvent(sk1, 1, "hello world", nil)
|
||||||
@@ -79,10 +69,10 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Query the event back
|
// Query the event back
|
||||||
sub, err := client2.Subscribe(ctx, []nostr.Filter{{
|
sub, err := client2.Subscribe(ctx, nostr.Filter{
|
||||||
Authors: []string{pk1},
|
Authors: []nostr.PubKey{pk1},
|
||||||
Kinds: []int{1},
|
Kinds: []uint16{1},
|
||||||
}})
|
}, nostr.SubscriptionOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to subscribe: %v", err)
|
t.Fatalf("failed to subscribe: %v", err)
|
||||||
}
|
}
|
||||||
@@ -101,14 +91,14 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
|
|
||||||
// test 2: live event subscription
|
// test 2: live event subscription
|
||||||
t.Run("live event subscription", func(t *testing.T) {
|
t.Run("live event subscription", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Setup subscription first
|
// Setup subscription first
|
||||||
sub, err := client1.Subscribe(ctx, []nostr.Filter{{
|
sub, err := client1.Subscribe(ctx, nostr.Filter{
|
||||||
Authors: []string{pk2},
|
Authors: []nostr.PubKey{pk2},
|
||||||
Kinds: []int{1},
|
Kinds: []uint16{1},
|
||||||
}})
|
}, nostr.SubscriptionOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to subscribe: %v", err)
|
t.Fatalf("failed to subscribe: %v", err)
|
||||||
}
|
}
|
||||||
@@ -134,7 +124,7 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
|
|
||||||
// test 3: event deletion
|
// test 3: event deletion
|
||||||
t.Run("event deletion", func(t *testing.T) {
|
t.Run("event deletion", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Create an event to be deleted
|
// Create an event to be deleted
|
||||||
@@ -145,16 +135,16 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create deletion event
|
// Create deletion event
|
||||||
delEvent := createEvent(sk1, 5, "deleting", nostr.Tags{{"e", evt3.ID}})
|
delEvent := createEvent(sk1, 5, "deleting", nostr.Tags{{"e", evt3.ID.Hex()}})
|
||||||
err = client1.Publish(ctx, delEvent)
|
err = client1.Publish(ctx, delEvent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to publish deletion event: %v", err)
|
t.Fatalf("failed to publish deletion event: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to query the deleted event
|
// Try to query the deleted event
|
||||||
sub, err := client2.Subscribe(ctx, []nostr.Filter{{
|
sub, err := client2.Subscribe(ctx, nostr.Filter{
|
||||||
IDs: []string{evt3.ID},
|
IDs: []nostr.ID{evt3.ID},
|
||||||
}})
|
}, nostr.SubscriptionOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to subscribe: %v", err)
|
t.Fatalf("failed to subscribe: %v", err)
|
||||||
}
|
}
|
||||||
@@ -179,7 +169,7 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
|
|
||||||
// test 4: teplaceable events
|
// test 4: teplaceable events
|
||||||
t.Run("replaceable events", func(t *testing.T) {
|
t.Run("replaceable events", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// create initial kind:0 event
|
// create initial kind:0 event
|
||||||
@@ -210,17 +200,17 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// query to verify only the newest event exists
|
// query to verify only the newest event exists
|
||||||
sub, err := client2.Subscribe(ctx, []nostr.Filter{{
|
sub, err := client2.Subscribe(ctx, nostr.Filter{
|
||||||
Authors: []string{pk1},
|
Authors: []nostr.PubKey{pk1},
|
||||||
Kinds: []int{0},
|
Kinds: []uint16{0},
|
||||||
}})
|
}, nostr.SubscriptionOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to subscribe: %v", err)
|
t.Fatalf("failed to subscribe: %v", err)
|
||||||
}
|
}
|
||||||
defer sub.Unsub()
|
defer sub.Unsub()
|
||||||
|
|
||||||
// should only get one event back (the newest one)
|
// should only get one event back (the newest one)
|
||||||
var receivedEvents []*nostr.Event
|
var receivedEvents []nostr.Event
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case env := <-sub.Events:
|
case env := <-sub.Events:
|
||||||
@@ -241,17 +231,15 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
|
|
||||||
// test 5: event expiration
|
// test 5: event expiration
|
||||||
t.Run("event expiration", func(t *testing.T) {
|
t.Run("event expiration", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// create a new relay with shorter expiration check interval
|
// create a new relay with shorter expiration check interval
|
||||||
relay := NewRelay()
|
relay := NewRelay()
|
||||||
relay.expirationManager.interval = 3 * time.Second // check every 3 seconds
|
relay.expirationManager.interval = 3 * time.Second // check every 3 seconds
|
||||||
store := slicestore.SliceStore{}
|
store := &slicestore.SliceStore{}
|
||||||
store.Init()
|
store.Init()
|
||||||
relay.StoreEvent = append(relay.StoreEvent, store.SaveEvent)
|
relay.UseEventstore(store)
|
||||||
relay.QueryEvents = append(relay.QueryEvents, store.QueryEvents)
|
|
||||||
relay.DeleteEvent = append(relay.DeleteEvent, store.DeleteEvent)
|
|
||||||
|
|
||||||
// start test server
|
// start test server
|
||||||
server := httptest.NewServer(relay)
|
server := httptest.NewServer(relay)
|
||||||
@@ -259,7 +247,7 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
|
|
||||||
// connect test client
|
// connect test client
|
||||||
url := "ws" + server.URL[4:]
|
url := "ws" + server.URL[4:]
|
||||||
client, err := nostr.RelayConnect(context.Background(), url)
|
client, err := nostr.RelayConnect(t.Context(), url, nostr.RelayOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to connect client: %v", err)
|
t.Fatalf("failed to connect client: %v", err)
|
||||||
}
|
}
|
||||||
@@ -274,9 +262,9 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// verify event exists initially
|
// verify event exists initially
|
||||||
sub, err := client.Subscribe(ctx, []nostr.Filter{{
|
sub, err := client.Subscribe(ctx, nostr.Filter{
|
||||||
IDs: []string{evt.ID},
|
IDs: []nostr.ID{evt.ID},
|
||||||
}})
|
}, nostr.SubscriptionOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to subscribe: %v", err)
|
t.Fatalf("failed to subscribe: %v", err)
|
||||||
}
|
}
|
||||||
@@ -296,9 +284,9 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
time.Sleep(4 * time.Second)
|
time.Sleep(4 * time.Second)
|
||||||
|
|
||||||
// verify event no longer exists
|
// verify event no longer exists
|
||||||
sub, err = client.Subscribe(ctx, []nostr.Filter{{
|
sub, err = client.Subscribe(ctx, nostr.Filter{
|
||||||
IDs: []string{evt.ID},
|
IDs: []nostr.ID{evt.ID},
|
||||||
}})
|
}, nostr.SubscriptionOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to subscribe: %v", err)
|
t.Fatalf("failed to subscribe: %v", err)
|
||||||
}
|
}
|
||||||
@@ -323,7 +311,7 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
|
|
||||||
// test 6: unauthorized deletion
|
// test 6: unauthorized deletion
|
||||||
t.Run("unauthorized deletion", func(t *testing.T) {
|
t.Run("unauthorized deletion", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// create an event from client1
|
// create an event from client1
|
||||||
@@ -334,16 +322,16 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to delete it with client2
|
// Try to delete it with client2
|
||||||
delEvent := createEvent(sk2, 5, "trying to delete", nostr.Tags{{"e", evt4.ID}})
|
delEvent := createEvent(sk2, 5, "trying to delete", nostr.Tags{{"e", evt4.ID.Hex()}})
|
||||||
err = client2.Publish(ctx, delEvent)
|
err = client2.Publish(ctx, delEvent)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("should have failed to publish deletion event: %v", err)
|
t.Fatalf("should have failed to publish deletion event: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify event still exists
|
// Verify event still exists
|
||||||
sub, err := client1.Subscribe(ctx, []nostr.Filter{{
|
sub, err := client1.Subscribe(ctx, nostr.Filter{
|
||||||
IDs: []string{evt4.ID},
|
IDs: []nostr.ID{evt4.ID},
|
||||||
}})
|
}, nostr.SubscriptionOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to subscribe: %v", err)
|
t.Fatalf("failed to subscribe: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,53 +21,42 @@ 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)
|
||||||
if rl.RejectFilter != nil {
|
if nil != rl.OnRequest {
|
||||||
if reject, msg := rl.RejectFilter(ctx, filter); reject {
|
if reject, msg := rl.OnRequest(ctx, filter); reject {
|
||||||
return errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
|
return errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// run the function to query events
|
// run the function to query events
|
||||||
if rl.QueryEvents != nil {
|
if nil != rl.QueryStored {
|
||||||
ch, err := rl.QueryEvents(ctx, filter)
|
for event := range rl.QueryStored(ctx, filter) {
|
||||||
if err != nil {
|
ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &id, Event: event})
|
||||||
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
|
|
||||||
eose.Done()
|
|
||||||
} else if ch == nil {
|
|
||||||
eose.Done()
|
|
||||||
}
|
}
|
||||||
|
eose.Done()
|
||||||
go func(ch chan *nostr.Event) {
|
|
||||||
for event := range ch {
|
|
||||||
ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &id, Event: *event})
|
|
||||||
}
|
|
||||||
eose.Done()
|
|
||||||
}(ch)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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) uint32 {
|
||||||
// check if we'll reject this filter
|
// check if we'll reject this filter
|
||||||
if rl.RejectCountFilter != nil {
|
if nil != rl.OnCountFilter {
|
||||||
if rejecting, msg := rl.RejectCountFilter(ctx, filter); rejecting {
|
if rejecting, msg := rl.OnCountFilter(ctx, filter); rejecting {
|
||||||
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
if nil != rl.Count {
|
||||||
if rl.CountEvents != nil {
|
res, err := rl.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()))
|
||||||
}
|
}
|
||||||
subtotal += res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
return subtotal
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *Relay) handleCountRequestWithHLL(
|
func (rl *Relay) handleCountRequestWithHLL(
|
||||||
@@ -75,32 +64,22 @@ func (rl *Relay) handleCountRequestWithHLL(
|
|||||||
ws *WebSocket,
|
ws *WebSocket,
|
||||||
filter nostr.Filter,
|
filter nostr.Filter,
|
||||||
offset int,
|
offset int,
|
||||||
) (int64, *hyperloglog.HyperLogLog) {
|
) (uint32, *hyperloglog.HyperLogLog) {
|
||||||
// check if we'll reject this filter
|
// check if we'll reject this filter
|
||||||
if rl.RejectCountFilter != nil {
|
if nil != rl.OnCountFilter {
|
||||||
if rejecting, msg := rl.RejectCountFilter(ctx, filter); rejecting {
|
if rejecting, msg := rl.OnCountFilter(ctx, filter); rejecting {
|
||||||
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// run the functions to count (generally it will be just one)
|
if nil != rl.CountHLL {
|
||||||
var subtotal int64 = 0
|
res, hll, err := rl.CountHLL(ctx, filter, offset)
|
||||||
var hll *hyperloglog.HyperLogLog
|
|
||||||
if rl.CountEventsHLL != nil {
|
|
||||||
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()))
|
||||||
}
|
}
|
||||||
subtotal += res
|
return res, hll
|
||||||
if fhll != nil {
|
|
||||||
if hll == nil {
|
|
||||||
hll = fhll
|
|
||||||
} else {
|
|
||||||
hll.Merge(fhll)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return subtotal, hll
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fiatjaf/eventstore"
|
|
||||||
"github.com/fiatjaf/eventstore/slicestore"
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/eventstore/slicestore"
|
||||||
|
"fiatjaf.com/nostr/eventstore/wrappers"
|
||||||
"fiatjaf.com/nostr/nip77"
|
"fiatjaf.com/nostr/nip77"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,8 +17,8 @@ func main() {
|
|||||||
db := &slicestore.SliceStore{}
|
db := &slicestore.SliceStore{}
|
||||||
db.Init()
|
db.Init()
|
||||||
|
|
||||||
sk := nostr.GeneratePrivateKey()
|
sk := nostr.Generate()
|
||||||
local := eventstore.RelayWrapper{Store: db}
|
local := wrappers.StorePublisher{Store: db}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
for i := 0; i < 20; i++ {
|
for i := 0; i < 20; i++ {
|
||||||
@@ -29,7 +30,7 @@ func main() {
|
|||||||
Tags: nostr.Tags{},
|
Tags: nostr.Tags{},
|
||||||
}
|
}
|
||||||
evt.Sign(sk)
|
evt.Sign(sk)
|
||||||
db.SaveEvent(ctx, &evt)
|
db.SaveEvent(evt)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -40,7 +41,7 @@ func main() {
|
|||||||
Tags: nostr.Tags{},
|
Tags: nostr.Tags{},
|
||||||
}
|
}
|
||||||
evt.Sign(sk)
|
evt.Sign(sk)
|
||||||
db.SaveEvent(ctx, &evt)
|
db.SaveEvent(evt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,11 +51,7 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := local.QuerySync(ctx, nostr.Filter{})
|
data := slices.Collect(local.QueryEvents(nostr.Filter{}))
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("total local events:", len(data))
|
fmt.Println("total local events:", len(data))
|
||||||
time.Sleep(time.Second * 10)
|
time.Sleep(time.Second * 10)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func TestConnectContext(t *testing.T) {
|
|||||||
// relay client
|
// relay client
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
r, err := RelayConnect(ctx, testRelayURL)
|
r, err := RelayConnect(ctx, testRelayURL, RelayOptions{})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
@@ -34,7 +34,7 @@ func TestConnectContextCanceled(t *testing.T) {
|
|||||||
// relay client
|
// relay client
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
cancel() // make ctx expired
|
cancel() // make ctx expired
|
||||||
_, err := RelayConnect(ctx, testRelayURL)
|
_, err := RelayConnect(ctx, testRelayURL, RelayOptions{})
|
||||||
assert.ErrorIs(t, err, context.Canceled)
|
assert.ErrorIs(t, err, context.Canceled)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ func TestPublish(t *testing.T) {
|
|||||||
func makeKeyPair(t *testing.T) (priv, pub [32]byte) {
|
func makeKeyPair(t *testing.T) (priv, pub [32]byte) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
privkey := GeneratePrivateKey()
|
privkey := Generate()
|
||||||
pubkey := GetPublicKey(privkey)
|
pubkey := GetPublicKey(privkey)
|
||||||
|
|
||||||
return privkey, pubkey
|
return privkey, pubkey
|
||||||
@@ -69,7 +69,7 @@ func makeKeyPair(t *testing.T) (priv, pub [32]byte) {
|
|||||||
func mustRelayConnect(t *testing.T, url string) *Relay {
|
func mustRelayConnect(t *testing.T, url string) *Relay {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
rl, err := RelayConnect(context.Background(), url)
|
rl, err := RelayConnect(context.Background(), url, RelayOptions{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return rl
|
return rl
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"github.com/fiatjaf/eventstore/slicestore"
|
"fiatjaf.com/nostr/eventstore/slicestore"
|
||||||
"github.com/fiatjaf/khatru"
|
"github.com/fiatjaf/khatru"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import (
|
|||||||
"fiatjaf.com/nostr/sdk/hints/memoryh"
|
"fiatjaf.com/nostr/sdk/hints/memoryh"
|
||||||
"fiatjaf.com/nostr/sdk/kvstore"
|
"fiatjaf.com/nostr/sdk/kvstore"
|
||||||
kvstore_memory "fiatjaf.com/nostr/sdk/kvstore/memory"
|
kvstore_memory "fiatjaf.com/nostr/sdk/kvstore/memory"
|
||||||
"github.com/fiatjaf/eventstore"
|
"fiatjaf.com/nostr/eventstore"
|
||||||
"github.com/fiatjaf/eventstore/nullstore"
|
"fiatjaf.com/nostr/eventstore/nullstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
// System represents the core functionality of the SDK, providing access to
|
// System represents the core functionality of the SDK, providing access to
|
||||||
|
|||||||
Reference in New Issue
Block a user