mirror of
https://github.com/fiatjaf/khatru.git
synced 2026-04-07 22:16:46 +02:00
Compare commits
60 Commits
fix-listen
...
v0.14.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abbe61d128 | ||
|
|
e9999feec0 | ||
|
|
6275f52134 | ||
|
|
5b88b9b087 | ||
|
|
1b4d81dde4 | ||
|
|
7bfdbb557c | ||
|
|
3f26a1f727 | ||
|
|
76ecf4f791 | ||
|
|
1498da09c8 | ||
|
|
3d4dd71510 | ||
|
|
582a74c000 | ||
|
|
bbcf948dd6 | ||
|
|
553d848362 | ||
|
|
2a80d4099d | ||
|
|
ad6635d86c | ||
|
|
c93441cd63 | ||
|
|
dc34dd7e90 | ||
|
|
a004f59187 | ||
|
|
a931a83370 | ||
|
|
1c15db2ca1 | ||
|
|
b617fea679 | ||
|
|
1d7bdccb3a | ||
|
|
92d1a5b671 | ||
|
|
7f878121fc | ||
|
|
a893dc2d2c | ||
|
|
91e7737ec1 | ||
|
|
1dc12e5d2e | ||
|
|
f3b18619c7 | ||
|
|
902e882d97 | ||
|
|
8f62b2fbbe | ||
|
|
d4334ac2bb | ||
|
|
9069341657 | ||
|
|
9137eb02b0 | ||
|
|
e9c9d0c3a7 | ||
|
|
39d882857c | ||
|
|
6e04cca511 | ||
|
|
cc891d06ed | ||
|
|
08be77b914 | ||
|
|
e25400a61a | ||
|
|
635e91f360 | ||
|
|
eb92c2aac5 | ||
|
|
18fc0dc1dd | ||
|
|
6d827ac89a | ||
|
|
b8bb58f77c | ||
|
|
79eaedcffe | ||
|
|
aa2ac552ea | ||
|
|
4e2b921af9 | ||
|
|
cf98cfc6d8 | ||
|
|
62a3b9b7d9 | ||
|
|
e8637afa38 | ||
|
|
e38036a5e6 | ||
|
|
6a9dcdcbd4 | ||
|
|
5ae4d1194a | ||
|
|
753a1191b9 | ||
|
|
3e6d763a79 | ||
|
|
3b03f6700c | ||
|
|
d7056f1515 | ||
|
|
4e0971bafe | ||
|
|
07ea3a35ec | ||
|
|
ef57f54a28 |
@@ -73,7 +73,7 @@ func main() {
|
||||
policies.ValidateKind,
|
||||
|
||||
// define your own policies
|
||||
policies.PreventLargeTags(80),
|
||||
policies.PreventLargeTags(100),
|
||||
func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
if event.PubKey == "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" {
|
||||
return true, "we don't allow this person to write here"
|
||||
|
||||
115
adding.go
115
adding.go
@@ -11,6 +11,9 @@ import (
|
||||
|
||||
// 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) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
if evt == nil {
|
||||
return false, errors.New("error: event is nil")
|
||||
}
|
||||
@@ -32,68 +35,70 @@ func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) (skipBroadcast
|
||||
}
|
||||
} else {
|
||||
// will store
|
||||
|
||||
// but first check if we already have it
|
||||
filter := nostr.Filter{IDs: []string{evt.ID}}
|
||||
for _, query := range rl.QueryEvents {
|
||||
ch, err := query(ctx, filter)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for range ch {
|
||||
// if we run this it means we already have this event, so we just return a success and exit
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// if it's replaceable we first delete old versions
|
||||
if evt.Kind == 0 || evt.Kind == 3 || (10000 <= evt.Kind && evt.Kind < 20000) {
|
||||
// replaceable event, delete before storing
|
||||
filter := nostr.Filter{Authors: []string{evt.PubKey}, Kinds: []int{evt.Kind}}
|
||||
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)
|
||||
}
|
||||
// regular kinds are just saved directly
|
||||
if nostr.IsRegularKind(evt.Kind) {
|
||||
for _, store := range rl.StoreEvent {
|
||||
if err := store(ctx, evt); err != nil {
|
||||
switch err {
|
||||
case eventstore.ErrDupEvent:
|
||||
return true, nil
|
||||
default:
|
||||
return false, fmt.Errorf(nostr.NormalizeOKMessage(err.Error(), "error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if 30000 <= evt.Kind && evt.Kind < 40000 {
|
||||
// parameterized replaceable event, delete before storing
|
||||
d := evt.Tags.GetFirst([]string{"d", ""})
|
||||
if d == nil {
|
||||
return false, fmt.Errorf("invalid: missing 'd' tag on parameterized replaceable event")
|
||||
}
|
||||
|
||||
filter := nostr.Filter{Authors: []string{evt.PubKey}, Kinds: []int{evt.Kind}, Tags: nostr.TagMap{"d": []string{(*d)[1]}}}
|
||||
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 {
|
||||
// otherwise it's a replaceable -- so we'll use the replacer functions if we have any
|
||||
if len(rl.ReplaceEvent) > 0 {
|
||||
for _, repl := range rl.ReplaceEvent {
|
||||
if err := repl(ctx, evt); err != nil {
|
||||
switch err {
|
||||
case eventstore.ErrDupEvent:
|
||||
return true, nil
|
||||
default:
|
||||
return false, fmt.Errorf(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()}}
|
||||
}
|
||||
|
||||
// store
|
||||
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(nostr.NormalizeOKMessage(saveErr.Error(), "error"))
|
||||
// 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(nostr.NormalizeOKMessage(saveErr.Error(), "error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
46
blossom/authorization.go
Normal file
46
blossom/authorization.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package blossom
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func readAuthorization(r *http.Request) (*nostr.Event, error) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(token, "Nostr ") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var reader io.Reader
|
||||
reader = bytes.NewReader([]byte(token)[6:])
|
||||
reader = base64.NewDecoder(base64.StdEncoding, reader)
|
||||
var evt nostr.Event
|
||||
err := json.NewDecoder(reader).Decode(&evt)
|
||||
|
||||
if err != nil || evt.Kind != 24242 || len(evt.ID) != 64 || !evt.CheckID() {
|
||||
return nil, fmt.Errorf("invalid event")
|
||||
}
|
||||
|
||||
if ok, _ := evt.CheckSignature(); !ok {
|
||||
return nil, fmt.Errorf("invalid signature")
|
||||
}
|
||||
|
||||
expirationTag := evt.Tags.GetFirst([]string{"expiration", ""})
|
||||
if expirationTag == nil {
|
||||
return nil, fmt.Errorf("missing \"expiration\" tag")
|
||||
}
|
||||
expiration, _ := strconv.ParseInt((*expirationTag)[1], 10, 64)
|
||||
if nostr.Timestamp(expiration) < nostr.Now() {
|
||||
return nil, fmt.Errorf("event expired")
|
||||
}
|
||||
|
||||
return &evt, nil
|
||||
}
|
||||
26
blossom/blob.go
Normal file
26
blossom/blob.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package blossom
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
type BlobDescriptor struct {
|
||||
URL string `json:"url"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Size int `json:"size"`
|
||||
Type string `json:"type"`
|
||||
Uploaded nostr.Timestamp `json:"uploaded"`
|
||||
|
||||
Owner string `json:"-"`
|
||||
}
|
||||
|
||||
type BlobIndex interface {
|
||||
Keep(ctx context.Context, blob BlobDescriptor, pubkey string) error
|
||||
List(ctx context.Context, pubkey string) (chan BlobDescriptor, error)
|
||||
Get(ctx context.Context, sha256 string) (*BlobDescriptor, error)
|
||||
Delete(ctx context.Context, sha256 string, pubkey string) error
|
||||
}
|
||||
|
||||
var _ BlobIndex = (*EventStoreBlobIndexWrapper)(nil)
|
||||
104
blossom/eventstorewrapper.go
Normal file
104
blossom/eventstorewrapper.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package blossom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/fiatjaf/eventstore"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
// EventStoreBlobIndexWrapper uses fake events to keep track of what blobs we have stored and who owns them
|
||||
type EventStoreBlobIndexWrapper struct {
|
||||
eventstore.Store
|
||||
|
||||
ServiceURL string
|
||||
}
|
||||
|
||||
func (es EventStoreBlobIndexWrapper) Keep(ctx context.Context, blob BlobDescriptor, pubkey string) error {
|
||||
ch, err := es.Store.QueryEvents(ctx, nostr.Filter{Authors: []string{pubkey}, Kinds: []int{24242}, Tags: nostr.TagMap{"x": []string{blob.SHA256}}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if <-ch == nil {
|
||||
// doesn't exist, save
|
||||
evt := &nostr.Event{
|
||||
PubKey: pubkey,
|
||||
Kind: 24242,
|
||||
Tags: nostr.Tags{
|
||||
{"x", blob.SHA256},
|
||||
{"type", blob.Type},
|
||||
{"size", strconv.Itoa(blob.Size)},
|
||||
},
|
||||
CreatedAt: blob.Uploaded,
|
||||
}
|
||||
evt.ID = evt.GetID()
|
||||
es.Store.SaveEvent(ctx, evt)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es EventStoreBlobIndexWrapper) List(ctx context.Context, pubkey string) (chan BlobDescriptor, error) {
|
||||
ech, err := es.Store.QueryEvents(ctx, nostr.Filter{Authors: []string{pubkey}, Kinds: []int{24242}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := make(chan BlobDescriptor)
|
||||
|
||||
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) {
|
||||
ech, err := es.Store.QueryEvents(ctx, nostr.Filter{Tags: nostr.TagMap{"x": []string{sha256}}, Kinds: []int{24242}, Limit: 1})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
evt := <-ech
|
||||
if evt != nil {
|
||||
bd := es.parseEvent(evt)
|
||||
return &bd, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (es EventStoreBlobIndexWrapper) Delete(ctx context.Context, sha256 string, pubkey string) error {
|
||||
ech, err := es.Store.QueryEvents(ctx, nostr.Filter{Authors: []string{pubkey}, Tags: nostr.TagMap{"x": []string{sha256}}, Kinds: []int{24242}, Limit: 1})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
evt := <-ech
|
||||
if evt != nil {
|
||||
return es.Store.DeleteEvent(ctx, evt)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es EventStoreBlobIndexWrapper) parseEvent(evt *nostr.Event) BlobDescriptor {
|
||||
hhash := evt.Tags[0][1]
|
||||
mimetype := evt.Tags[1][1]
|
||||
ext := getExtension(mimetype)
|
||||
size, _ := strconv.Atoi(evt.Tags[2][1])
|
||||
|
||||
return BlobDescriptor{
|
||||
Owner: evt.PubKey,
|
||||
Uploaded: evt.CreatedAt,
|
||||
URL: es.ServiceURL + "/" + hhash + ext,
|
||||
SHA256: hhash,
|
||||
Type: mimetype,
|
||||
Size: size,
|
||||
}
|
||||
}
|
||||
336
blossom/handlers.go
Normal file
336
blossom/handlers.go
Normal file
@@ -0,0 +1,336 @@
|
||||
package blossom
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/liamg/magic"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func (bs BlossomServer) handleUploadCheck(w http.ResponseWriter, r *http.Request) {
|
||||
auth, err := readAuthorization(r)
|
||||
if err != nil {
|
||||
blossomError(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
if auth == nil {
|
||||
blossomError(w, "missing \"Authorization\" header", 400)
|
||||
return
|
||||
}
|
||||
if auth.Tags.GetFirst([]string{"t", "upload"}) == nil {
|
||||
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
||||
return
|
||||
}
|
||||
|
||||
mimetype := r.Header.Get("X-Content-Type")
|
||||
exts, _ := mime.ExtensionsByType(mimetype)
|
||||
var ext string
|
||||
if len(exts) > 0 {
|
||||
ext = exts[0]
|
||||
}
|
||||
|
||||
// get the file size from the incoming header
|
||||
size, _ := strconv.Atoi(r.Header.Get("X-Content-Length"))
|
||||
|
||||
for _, rb := range bs.RejectUpload {
|
||||
reject, reason, code := rb(r.Context(), auth, size, ext)
|
||||
if reject {
|
||||
blossomError(w, reason, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
auth, err := readAuthorization(r)
|
||||
if err != nil {
|
||||
blossomError(w, "invalid \"Authorization\": "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
if auth == nil {
|
||||
blossomError(w, "missing \"Authorization\" header", 400)
|
||||
return
|
||||
}
|
||||
if auth.Tags.GetFirst([]string{"t", "upload"}) == nil {
|
||||
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
||||
return
|
||||
}
|
||||
|
||||
// get the file size from the incoming header
|
||||
size, _ := strconv.Atoi(r.Header.Get("Content-Length"))
|
||||
if size == 0 {
|
||||
blossomError(w, "missing \"Content-Length\" header", 400)
|
||||
return
|
||||
}
|
||||
|
||||
// read first bytes of upload so we can find out the filetype
|
||||
b := make([]byte, min(50, size), size)
|
||||
if _, err = r.Body.Read(b); err != nil {
|
||||
blossomError(w, "failed to read initial bytes of upload body: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
var ext string
|
||||
if ft, _ := magic.Lookup(b); ft != nil {
|
||||
ext = "." + ft.Extension
|
||||
} else {
|
||||
// if we can't find, use the filetype given by the upload header
|
||||
mimetype := r.Header.Get("Content-Type")
|
||||
ext = getExtension(mimetype)
|
||||
}
|
||||
|
||||
// run the reject hooks
|
||||
for _, ru := range bs.RejectUpload {
|
||||
reject, reason, code := ru(r.Context(), auth, size, ext)
|
||||
if reject {
|
||||
blossomError(w, reason, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// if it passes then we have to read the entire thing into memory so we can compute the sha256
|
||||
for {
|
||||
var n int
|
||||
n, err = r.Body.Read(b[len(b):cap(b)])
|
||||
b = b[:len(b)+n]
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
if len(b) == cap(b) {
|
||||
// add more capacity (let append pick how much)
|
||||
// if Content-Length was correct we shouldn't reach this
|
||||
b = append(b, 0)[:len(b)]
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
blossomError(w, "failed to read upload body: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(b)
|
||||
hhash := hex.EncodeToString(hash[:])
|
||||
|
||||
// keep track of the blob descriptor
|
||||
bd := BlobDescriptor{
|
||||
URL: bs.ServiceURL + "/" + hhash + ext,
|
||||
SHA256: hhash,
|
||||
Size: len(b),
|
||||
Type: mime.TypeByExtension(ext),
|
||||
Uploaded: nostr.Now(),
|
||||
}
|
||||
if err := bs.Store.Keep(r.Context(), bd, auth.PubKey); err != nil {
|
||||
blossomError(w, "failed to save event: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
// save actual blob
|
||||
for _, sb := range bs.StoreBlob {
|
||||
if err := sb(r.Context(), hhash, b); err != nil {
|
||||
blossomError(w, "failed to save: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// return response
|
||||
json.NewEncoder(w).Encode(bd)
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) {
|
||||
spl := strings.SplitN(r.URL.Path, ".", 2)
|
||||
hhash := spl[0]
|
||||
if len(hhash) != 65 {
|
||||
blossomError(w, "invalid /<sha256>[.ext] path", 400)
|
||||
return
|
||||
}
|
||||
hhash = hhash[1:]
|
||||
|
||||
// check for an authorization tag, if any
|
||||
auth, err := readAuthorization(r)
|
||||
if err != nil {
|
||||
blossomError(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
// if there is one, we check if it has the extra requirements
|
||||
if auth != nil {
|
||||
if auth.Tags.GetFirst([]string{"t", "get"}) == nil {
|
||||
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
||||
return
|
||||
}
|
||||
|
||||
if auth.Tags.GetFirst([]string{"x", hhash}) == nil &&
|
||||
auth.Tags.GetFirst([]string{"server", bs.ServiceURL}) == nil {
|
||||
blossomError(w, "invalid \"Authorization\" event \"x\" or \"server\" tag", 403)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, rg := range bs.RejectGet {
|
||||
reject, reason, code := rg(r.Context(), auth, hhash)
|
||||
if reject {
|
||||
blossomError(w, reason, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var ext string
|
||||
if len(spl) == 2 {
|
||||
ext = "." + spl[1]
|
||||
}
|
||||
|
||||
for _, lb := range bs.LoadBlob {
|
||||
reader, _ := lb(r.Context(), hhash)
|
||||
if reader != nil {
|
||||
// use unix epoch as the time if we can't find the descriptor
|
||||
// as described in the http.ServeContent documentation
|
||||
t := time.Unix(0, 0)
|
||||
descriptor, err := bs.Store.Get(r.Context(), hhash)
|
||||
if err == nil && descriptor != nil {
|
||||
t = descriptor.Uploaded.Time()
|
||||
}
|
||||
http.ServeContent(w, r, hhash+ext, t, reader)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
blossomError(w, "file not found", 404)
|
||||
return
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleHasBlob(w http.ResponseWriter, r *http.Request) {
|
||||
spl := strings.SplitN(r.URL.Path, ".", 2)
|
||||
hhash := spl[0]
|
||||
if len(hhash) != 65 {
|
||||
blossomError(w, "invalid /<sha256>[.ext] path", 400)
|
||||
return
|
||||
}
|
||||
hhash = hhash[1:]
|
||||
|
||||
bd, err := bs.Store.Get(r.Context(), hhash)
|
||||
if err != nil {
|
||||
blossomError(w, "failed to query: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
if bd == nil {
|
||||
blossomError(w, "file not found", 404)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
// check for an authorization tag, if any
|
||||
auth, err := readAuthorization(r)
|
||||
if err != nil {
|
||||
blossomError(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
// if there is one, we check if it has the extra requirements
|
||||
if auth != nil {
|
||||
if auth.Tags.GetFirst([]string{"t", "list"}) == nil {
|
||||
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pubkey := r.URL.Path[6:]
|
||||
|
||||
for _, rl := range bs.RejectList {
|
||||
reject, reason, code := rl(r.Context(), auth, pubkey)
|
||||
if reject {
|
||||
blossomError(w, reason, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ch, err := bs.Store.List(r.Context(), pubkey)
|
||||
if err != nil {
|
||||
blossomError(w, "failed to query: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte{'['})
|
||||
enc := json.NewEncoder(w)
|
||||
first := true
|
||||
for bd := range ch {
|
||||
if !first {
|
||||
w.Write([]byte{','})
|
||||
} else {
|
||||
first = false
|
||||
}
|
||||
enc.Encode(bd)
|
||||
}
|
||||
w.Write([]byte{']'})
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
auth, err := readAuthorization(r)
|
||||
if err != nil {
|
||||
blossomError(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
if auth != nil {
|
||||
if auth.Tags.GetFirst([]string{"t", "delete"}) == nil {
|
||||
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
spl := strings.SplitN(r.URL.Path, ".", 2)
|
||||
hhash := spl[0]
|
||||
if len(hhash) != 65 {
|
||||
blossomError(w, "invalid /<sha256>[.ext] path", 400)
|
||||
return
|
||||
}
|
||||
hhash = hhash[1:]
|
||||
if auth.Tags.GetFirst([]string{"x", hhash}) == nil &&
|
||||
auth.Tags.GetFirst([]string{"server", bs.ServiceURL}) == nil {
|
||||
blossomError(w, "invalid \"Authorization\" event \"x\" or \"server\" tag", 403)
|
||||
return
|
||||
}
|
||||
|
||||
// should we accept this delete?
|
||||
for _, rd := range bs.RejectDelete {
|
||||
reject, reason, code := rd(r.Context(), auth, hhash)
|
||||
if reject {
|
||||
blossomError(w, reason, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// delete the entry that links this blob to this author
|
||||
if err := bs.Store.Delete(r.Context(), hhash, auth.PubKey); err != nil {
|
||||
blossomError(w, "delete of blob entry failed: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// 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 {
|
||||
for _, del := range bs.DeleteBlob {
|
||||
if err := del(r.Context(), hhash); err != nil {
|
||||
blossomError(w, "failed to delete blob: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleMirror(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleNegentropy(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
70
blossom/server.go
Normal file
70
blossom/server.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package blossom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
type BlossomServer struct {
|
||||
ServiceURL string
|
||||
Store BlobIndex
|
||||
|
||||
StoreBlob []func(ctx context.Context, sha256 string, body []byte) error
|
||||
LoadBlob []func(ctx context.Context, sha256 string) (io.ReadSeeker, error)
|
||||
DeleteBlob []func(ctx context.Context, sha256 string) error
|
||||
|
||||
RejectUpload []func(ctx context.Context, auth *nostr.Event, size int, ext string) (bool, string, int)
|
||||
RejectGet []func(ctx context.Context, auth *nostr.Event, sha256 string) (bool, string, int)
|
||||
RejectList []func(ctx context.Context, auth *nostr.Event, pubkey string) (bool, string, int)
|
||||
RejectDelete []func(ctx context.Context, auth *nostr.Event, sha256 string) (bool, string, int)
|
||||
}
|
||||
|
||||
func New(rl *khatru.Relay, serviceURL string) *BlossomServer {
|
||||
bs := &BlossomServer{
|
||||
ServiceURL: serviceURL,
|
||||
}
|
||||
|
||||
base := rl.Router()
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/upload" {
|
||||
if r.Method == "PUT" {
|
||||
bs.handleUpload(w, r)
|
||||
return
|
||||
} else if r.Method == "HEAD" {
|
||||
bs.handleUploadCheck(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.URL.Path, "/list/") && r.Method == "GET" {
|
||||
bs.handleList(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if len(strings.SplitN(r.URL.Path, ".", 2)[0]) == 65 {
|
||||
if r.Method == "HEAD" {
|
||||
bs.handleHasBlob(w, r)
|
||||
return
|
||||
} else if r.Method == "GET" {
|
||||
bs.handleGetBlob(w, r)
|
||||
return
|
||||
} else if r.Method == "DELETE" {
|
||||
bs.handleDelete(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
base.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
rl.SetRouter(mux)
|
||||
|
||||
return bs
|
||||
}
|
||||
37
blossom/utils.go
Normal file
37
blossom/utils.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package blossom
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func blossomError(w http.ResponseWriter, msg string, code int) {
|
||||
w.Header().Add("X-Reason", msg)
|
||||
w.WriteHeader(code)
|
||||
}
|
||||
|
||||
func getExtension(mimetype string) string {
|
||||
if mimetype == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch mimetype {
|
||||
case "image/jpeg":
|
||||
return ".jpg"
|
||||
case "image/gif":
|
||||
return ".gif"
|
||||
case "image/png":
|
||||
return ".png"
|
||||
case "image/webp":
|
||||
return ".webp"
|
||||
case "video/mp4":
|
||||
return ".mp4"
|
||||
}
|
||||
|
||||
exts, _ := mime.ExtensionsByType(mimetype)
|
||||
if len(exts) > 0 {
|
||||
return exts[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
36
deleting.go
36
deleting.go
@@ -3,6 +3,8 @@ package khatru
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
@@ -10,10 +12,35 @@ import (
|
||||
func (rl *Relay) handleDeleteRequest(ctx context.Context, evt *nostr.Event) error {
|
||||
// event deletion -- nip09
|
||||
for _, tag := range evt.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "e" {
|
||||
// first we fetch the event
|
||||
if len(tag) >= 2 {
|
||||
var f nostr.Filter
|
||||
|
||||
switch tag[0] {
|
||||
case "e":
|
||||
f = nostr.Filter{IDs: []string{tag[1]}}
|
||||
case "a":
|
||||
spl := strings.Split(tag[1], ":")
|
||||
if len(spl) != 3 {
|
||||
continue
|
||||
}
|
||||
kind, err := strconv.Atoi(spl[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
author := spl[1]
|
||||
identifier := spl[2]
|
||||
f = nostr.Filter{
|
||||
Kinds: []int{kind},
|
||||
Authors: []string{author},
|
||||
Tags: nostr.TagMap{"d": []string{identifier}},
|
||||
Until: &evt.CreatedAt,
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
for _, query := range rl.QueryEvents {
|
||||
ch, err := query(ctx, nostr.Filter{IDs: []string{tag[1]}})
|
||||
ch, err := query(ctx, f)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -24,13 +51,14 @@ func (rl *Relay) handleDeleteRequest(ctx context.Context, evt *nostr.Event) erro
|
||||
// got the event, now check if the user can delete it
|
||||
acceptDeletion := target.PubKey == evt.PubKey
|
||||
var msg string
|
||||
if acceptDeletion == false {
|
||||
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 {
|
||||
// delete it
|
||||
for _, del := range rl.DeleteEvent {
|
||||
|
||||
@@ -30,7 +30,7 @@ If on `RejectFilter` or `RejectEvent` you prefix the message with `auth-required
|
||||
relay.RejectFilter = append(relay.RejectFilter, func(ctx context.Context, filter nostr.Filter) (bool, string) {
|
||||
return true, "auth-required: this query requires you to be authenticated"
|
||||
})
|
||||
relay.RejectEvent = append(relay.RejectFilter, func(ctx context.Context, event *nostr.Event) (bool, string) {
|
||||
relay.RejectEvent = append(relay.RejectEvent, func(ctx context.Context, event *nostr.Event) (bool, string) {
|
||||
return true, "auth-required: publishing this event requires authentication"
|
||||
})
|
||||
```
|
||||
@@ -60,3 +60,26 @@ relay.RejectFilter = append(relay.RejectFilter, func(ctx context.Context, filter
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Reacting to a successful authentication
|
||||
|
||||
Each `khatru.WebSocket` object has an `.Authed` channel that is closed whenever that connection performs a successful authentication.
|
||||
|
||||
You can use that to emulate a listener for these events in case you want to keep track of who is authenticating in real time and not only check it when they request for something.
|
||||
|
||||
```go
|
||||
relay.OnConnect = append(relay.OnConnect,
|
||||
khatru.RequestAuth,
|
||||
func(ctx context.Context) {
|
||||
go func(ctx context.Context) {
|
||||
conn := khatru.GetConnection(ctx)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
fmt.Println("connection closed")
|
||||
case <-conn.Authed:
|
||||
fmt.Println("authenticated as", conn.AuthedPublicKey)
|
||||
}
|
||||
}(ctx)
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
@@ -44,7 +44,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
Other local key-value embedded databases that work the same way are [LMDB](https://pkg.go.dev/github.com/fiatjaf/eventstore/lmdb) and [BoltDB](https://pkg.go.dev/github.com/fiatjaf/eventstore/bolt).
|
||||
[LMDB](https://pkg.go.dev/github.com/fiatjaf/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`.
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ We can also make use of some default policies that come bundled with Khatru:
|
||||
```go
|
||||
import "github.com/fiatjaf/khatru" // implied
|
||||
|
||||
relay.RejectEvent = append(relay.RejectEvent, policies.PreventLargeTags, policies.PreventTimestampsInThePast(time.Hour * 2), policies.PreventTimestampsInTheFuture(time.Minute * 30))
|
||||
relay.RejectEvent = append(relay.RejectEvent, policies.PreventLargeTags(120), policies.PreventTimestampsInThePast(time.Hour * 2), policies.PreventTimestampsInTheFuture(time.Minute * 30))
|
||||
```
|
||||
|
||||
There are many other ways to customize the relay behavior. Take a look at the [`Relay` struct docs](https://pkg.go.dev/github.com/fiatjaf/khatru#Relay) for more, or see the [cookbook](/cookbook/).
|
||||
|
||||
@@ -39,3 +39,21 @@ features:
|
||||
link: https://pkg.go.dev/github.com/fiatjaf/khatru
|
||||
details: That means it is fast and lightweight, you can learn the language in 5 minutes and it builds your relay into a single binary that's easy to ship and deploy.
|
||||
---
|
||||
|
||||
## A glimpse of `khatru`'s power
|
||||
|
||||
It allows you to create a fully-functional relay in 7 lines of code:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
relay := khatru.NewRelay()
|
||||
db := badger.BadgerBackend{Path: "/tmp/khatru-badgern-tmp"}
|
||||
db.Init()
|
||||
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
||||
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
||||
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
||||
http.ListenAndServe(":3334", relay)
|
||||
}
|
||||
```
|
||||
|
||||
After that you can customize it in infinite ways. See the links above.
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* [song](https://git.fiatjaf.com/song), a personal git server that comes with an embedded relay dedicated to dealing with [NIP-34](https://nips.nostr.com/34) git-related Nostr events
|
||||
* [relay29](https://github.com/fiatjaf/relay29), a relay that powers most of the [NIP-29](https://nips.nostr.com/29) Nostr groups ecosystem
|
||||
* [fiatjaf.com](https://fiatjaf.com), a personal website that serves the same content as HTML but also as Nostr events.
|
||||
* [gm-relay](https://github.com/ptrio42/gm-relay), a relay that only accepts GM notes once a day.
|
||||
|
||||
## Other possible use cases
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ func main() {
|
||||
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
||||
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
||||
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
||||
relay.Negentropy = true
|
||||
|
||||
fmt.Println("running on :3334")
|
||||
http.ListenAndServe(":3334", relay)
|
||||
|
||||
42
examples/blossom/main.go
Normal file
42
examples/blossom/main.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/fiatjaf/eventstore/badger"
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/khatru/blossom"
|
||||
)
|
||||
|
||||
func main() {
|
||||
relay := khatru.NewRelay()
|
||||
|
||||
db := &badger.BadgerBackend{Path: "/tmp/khatru-badger-blossom-tmp"}
|
||||
if err := db.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
||||
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
||||
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
||||
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
||||
|
||||
bl := blossom.New(relay, "http://localhost:3334")
|
||||
bl.Store = blossom.EventStoreBlobIndexWrapper{Store: db, ServiceURL: bl.ServiceURL}
|
||||
bl.StoreBlob = append(bl.StoreBlob, func(ctx context.Context, sha256 string, body []byte) error {
|
||||
fmt.Println("storing", sha256, len(body))
|
||||
return nil
|
||||
})
|
||||
bl.LoadBlob = append(bl.LoadBlob, func(ctx context.Context, sha256 string) (io.ReadSeeker, error) {
|
||||
fmt.Println("loading", sha256)
|
||||
blob := strings.NewReader("aaaaa")
|
||||
return blob, nil
|
||||
})
|
||||
|
||||
fmt.Println("running on :3334")
|
||||
http.ListenAndServe(":3334", relay)
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func main() {
|
||||
policies.ValidateKind,
|
||||
|
||||
// define your own policies
|
||||
policies.PreventLargeTags(80),
|
||||
policies.PreventLargeTags(100),
|
||||
func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
if event.PubKey == "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" {
|
||||
return true, "we don't allow this person to write here"
|
||||
|
||||
@@ -15,6 +15,10 @@ func (rl *Relay) Router() *http.ServeMux {
|
||||
return rl.serveMux
|
||||
}
|
||||
|
||||
func (rl *Relay) SetRouter(mux *http.ServeMux) {
|
||||
rl.serveMux = mux
|
||||
}
|
||||
|
||||
// Start creates an http server and starts listening on given host and port.
|
||||
func (rl *Relay) Start(host string, port int, started ...chan bool) error {
|
||||
addr := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
@@ -53,6 +57,7 @@ func (rl *Relay) Shutdown(ctx context.Context) {
|
||||
defer rl.clientsMutex.Unlock()
|
||||
for ws := range rl.clients {
|
||||
ws.conn.WriteControl(websocket.CloseMessage, nil, time.Now().Add(time.Second))
|
||||
ws.cancel()
|
||||
ws.conn.Close()
|
||||
}
|
||||
clear(rl.clients)
|
||||
|
||||
69
go.mod
69
go.mod
@@ -1,56 +1,67 @@
|
||||
module github.com/fiatjaf/khatru
|
||||
|
||||
go 1.21.4
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/bep/debounce v1.2.1
|
||||
github.com/fasthttp/websocket v1.5.7
|
||||
github.com/fiatjaf/eventstore v0.5.1
|
||||
github.com/nbd-wtf/go-nostr v0.34.5
|
||||
github.com/puzpuzpuz/xsync/v3 v3.0.2
|
||||
github.com/rs/cors v1.7.0
|
||||
github.com/fiatjaf/eventstore v0.14.2
|
||||
github.com/liamg/magic v0.0.1
|
||||
github.com/nbd-wtf/go-nostr v0.43.0
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
)
|
||||
|
||||
require (
|
||||
fiatjaf.com/lib v0.2.0 // indirect
|
||||
github.com/PowerDNS/lmdb-go v1.9.2 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/aquasecurity/esquery v0.2.0 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/dgraph-io/badger/v4 v4.2.0 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||
github.com/dgraph-io/badger/v4 v4.5.0 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.0.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elastic/elastic-transport-go/v8 v8.3.0 // indirect
|
||||
github.com/elastic/elastic-transport-go/v8 v8.6.0 // indirect
|
||||
github.com/elastic/go-elasticsearch/v7 v7.17.10 // indirect
|
||||
github.com/elastic/go-elasticsearch/v8 v8.10.1 // indirect
|
||||
github.com/elastic/go-elasticsearch/v8 v8.16.0 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.3.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/glog v1.1.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/flatbuffers v23.5.26+incompatible // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
||||
github.com/jmoiron/sqlx v1.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.8 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.18 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/tidwall/gjson v1.17.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/net v0.18.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect
|
||||
golang.org/x/net v0.32.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
google.golang.org/protobuf v1.35.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
183
go.sum
183
go.sum
@@ -1,4 +1,8 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
fiatjaf.com/lib v0.2.0 h1:TgIJESbbND6GjOgGHxF5jsO6EMjuAxIzZHPo5DXYexs=
|
||||
fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/PowerDNS/lmdb-go v1.9.2 h1:Cmgerh9y3ZKBZGz1irxSShhfmFyRUh+Zdk4cZk7ZJvU=
|
||||
github.com/PowerDNS/lmdb-go v1.9.2/go.mod h1:TE0l+EZK8Z1B4dx070ZxkWTlp8RG1mjN0/+FkFRQMtU=
|
||||
@@ -6,39 +10,39 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/aquasecurity/esquery v0.2.0 h1:9WWXve95TE8hbm3736WB7nS6Owl8UGDeu+0jiyE9ttA=
|
||||
github.com/aquasecurity/esquery v0.2.0/go.mod h1:VU+CIFR6C+H142HHZf9RUkp4Eedpo9UrEKeCQHWf9ao=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs=
|
||||
github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak=
|
||||
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
|
||||
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/dgraph-io/badger/v4 v4.5.0 h1:TeJE3I1pIWLBjYhIYCA1+uxrjWEoJXImFBMEBVSm16g=
|
||||
github.com/dgraph-io/badger/v4 v4.5.0/go.mod h1:ysgYmIeG8dS/E8kwxT7xHyc7MkmwNYLRoYnFbr7387A=
|
||||
github.com/dgraph-io/ristretto/v2 v2.0.0 h1:l0yiSOtlJvc0otkqyMaDNysg8E9/F/TYZwMbxscNOAQ=
|
||||
github.com/dgraph-io/ristretto/v2 v2.0.0/go.mod h1:FVFokF2dRqXyPyeMnK1YDy8Fc6aTe0IKgbcd03CYeEk=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.3.0 h1:DJGxovyQLXGr62e9nDMPSxRyWION0Bh6d9eCFBriiHo=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.3.0/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.6.0 h1:Y2S/FBjx1LlCv5m6pWAF2kDJAHoSjSRSJCApolgfthA=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.6.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk=
|
||||
github.com/elastic/go-elasticsearch/v7 v7.6.0/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
|
||||
github.com/elastic/go-elasticsearch/v7 v7.17.10 h1:TCQ8i4PmIJuBunvBS6bwT2ybzVFxxUhhltAs3Gyu1yo=
|
||||
github.com/elastic/go-elasticsearch/v7 v7.17.10/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
|
||||
github.com/elastic/go-elasticsearch/v8 v8.10.1 h1:JJ3i2DimYTsJcUoEGbg6tNB0eehTNdid9c5kTR1TGuI=
|
||||
github.com/elastic/go-elasticsearch/v8 v8.10.1/go.mod h1:GU1BJHO7WeamP7UhuElYwzzHtvf9SDmeVpSSy9+o6Qg=
|
||||
github.com/elastic/go-elasticsearch/v8 v8.16.0 h1:f7bR+iBz8GTAVhwyFO3hm4ixsz2eMaEy0QroYnXV3jE=
|
||||
github.com/elastic/go-elasticsearch/v8 v8.16.0/go.mod h1:lGMlgKIbYoRvay3xWBeKahAiJOgmFDsjZC39nmO3H64=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -47,25 +51,25 @@ github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KH
|
||||
github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fiatjaf/eventstore v0.5.1 h1:tTh+JYP0RME51VY2QB2Gvtzj6QTaZAnSVhgZtrYqY2A=
|
||||
github.com/fiatjaf/eventstore v0.5.1/go.mod h1:r5yCFmrVNT2b1xUOuMnDVS3xPGh97y8IgTcLyY2rYP8=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/fiatjaf/eventstore v0.14.2 h1:1XOLLEBCGQNQ1rLaO8mcfTQydcetPOqb/uRK8zOnOSI=
|
||||
github.com/fiatjaf/eventstore v0.14.2/go.mod h1:XmYZSdFxsY+cwfpdzVG61M7pemcmqlZwDfDGFC8WwWo=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
|
||||
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
|
||||
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -76,67 +80,67 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
|
||||
github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI=
|
||||
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jgroeneveld/schema v1.0.0 h1:J0E10CrOkiSEsw6dfb1IfrDJD14pf6QLVJ3tRPl/syI=
|
||||
github.com/jgroeneveld/schema v1.0.0/go.mod h1:M14lv7sNMtGvo3ops1MwslaSYgDYxrSmbzWIQ0Mr5rs=
|
||||
github.com/jgroeneveld/trial v2.0.0+incompatible h1:d59ctdgor+VqdZCAiUfVN8K13s0ALDioG5DWwZNtRuQ=
|
||||
github.com/jgroeneveld/trial v2.0.0+incompatible/go.mod h1:I6INLW96EN8WysNBXUFI3M4RIC8ePg9ntAc/Wy+U/+M=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM=
|
||||
github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
|
||||
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/nbd-wtf/go-nostr v0.34.5 h1:vti8WqvGWbVoWAPniaz7li2TpCyC+7ZS62Gmy7ib/z0=
|
||||
github.com/nbd-wtf/go-nostr v0.34.5/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nbd-wtf/go-nostr v0.43.0 h1:KJh/HXjKkhNCejRswHWVg8IgoAyLjw4iWxu/JDUZzqM=
|
||||
github.com/nbd-wtf/go-nostr v0.43.0/go.mod h1:8YfmT9tBuRT+4nWHuMBDh+xSIZqAdZC6QIOgQfBgWxU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
|
||||
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
@@ -146,46 +150,43 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
|
||||
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
|
||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
|
||||
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
|
||||
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
|
||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -193,13 +194,7 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
@@ -219,14 +214,14 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
187
handlers.go
187
handlers.go
@@ -3,7 +3,6 @@ package khatru
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
@@ -11,9 +10,15 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
"github.com/fasthttp/websocket"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip42"
|
||||
"github.com/nbd-wtf/go-nostr/nip45"
|
||||
"github.com/nbd-wtf/go-nostr/nip45/hyperloglog"
|
||||
"github.com/nbd-wtf/go-nostr/nip77"
|
||||
"github.com/nbd-wtf/go-nostr/nip77/negentropy"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
@@ -23,14 +28,28 @@ func (rl *Relay) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
rl.ServiceURL = getServiceBaseURL(r)
|
||||
}
|
||||
|
||||
corsMiddleware := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{
|
||||
http.MethodHead,
|
||||
http.MethodGet,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete,
|
||||
},
|
||||
AllowedHeaders: []string{"Authorization", "*"},
|
||||
MaxAge: 86400,
|
||||
})
|
||||
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
rl.HandleWebsocket(w, r)
|
||||
} else if r.Header.Get("Accept") == "application/nostr+json" {
|
||||
cors.AllowAll().Handler(http.HandlerFunc(rl.HandleNIP11)).ServeHTTP(w, r)
|
||||
corsMiddleware.Handler(http.HandlerFunc(rl.HandleNIP11)).ServeHTTP(w, r)
|
||||
} else if r.Header.Get("Content-Type") == "application/nostr+json+rpc" {
|
||||
cors.AllowAll().Handler(http.HandlerFunc(rl.HandleNIP86)).ServeHTTP(w, r)
|
||||
corsMiddleware.Handler(http.HandlerFunc(rl.HandleNIP86)).ServeHTTP(w, r)
|
||||
} else {
|
||||
rl.serveMux.ServeHTTP(w, r)
|
||||
corsMiddleware.Handler(rl.serveMux).ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +74,12 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
rand.Read(challenge)
|
||||
|
||||
ws := &WebSocket{
|
||||
conn: conn,
|
||||
Request: r,
|
||||
Challenge: hex.EncodeToString(challenge),
|
||||
conn: conn,
|
||||
Request: r,
|
||||
Challenge: hex.EncodeToString(challenge),
|
||||
negentropySessions: xsync.NewMapOf[string, *NegentropySession](),
|
||||
}
|
||||
ws.Context, ws.cancel = context.WithCancel(context.Background())
|
||||
|
||||
rl.clientsMutex.Lock()
|
||||
rl.clients[ws] = make([]listenerSpec, 0, 2)
|
||||
@@ -78,31 +99,19 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ticker.Stop()
|
||||
cancel()
|
||||
conn.Close()
|
||||
ws.cancel()
|
||||
ws.conn.Close()
|
||||
|
||||
rl.clientsMutex.Lock()
|
||||
defer rl.clientsMutex.Unlock()
|
||||
if specs, ok := rl.clients[ws]; ok {
|
||||
// swap delete listeners and delete client
|
||||
for s, spec := range specs {
|
||||
// no need to cancel contexts since they inherit from the main connection context
|
||||
// just delete the listeners
|
||||
srl := spec.subrelay
|
||||
srl.listeners[spec.index] = srl.listeners[len(srl.listeners)-1]
|
||||
specs[s] = specs[len(specs)-1]
|
||||
srl.listeners = srl.listeners[0:len(srl.listeners)]
|
||||
}
|
||||
}
|
||||
delete(rl.clients, ws)
|
||||
rl.removeClientAndListeners(ws)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer kill()
|
||||
|
||||
conn.SetReadLimit(rl.MaxMessageSize)
|
||||
conn.SetReadDeadline(time.Now().Add(rl.PongWait))
|
||||
conn.SetPongHandler(func(string) error {
|
||||
conn.SetReadDeadline(time.Now().Add(rl.PongWait))
|
||||
ws.conn.SetReadLimit(rl.MaxMessageSize)
|
||||
ws.conn.SetReadDeadline(time.Now().Add(rl.PongWait))
|
||||
ws.conn.SetPongHandler(func(string) error {
|
||||
ws.conn.SetReadDeadline(time.Now().Add(rl.PongWait))
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -111,7 +120,7 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
for {
|
||||
typ, message, err := conn.ReadMessage()
|
||||
typ, message, err := ws.conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(
|
||||
err,
|
||||
@@ -123,6 +132,7 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
) {
|
||||
rl.Log.Printf("unexpected close error from %s: %v\n", r.Header.Get("X-Forwarded-For"), err)
|
||||
}
|
||||
ws.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -134,16 +144,20 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
go func(message []byte) {
|
||||
envelope := nostr.ParseMessage(message)
|
||||
if envelope == nil {
|
||||
// stop silently
|
||||
return
|
||||
if !rl.Negentropy {
|
||||
// stop silently
|
||||
return
|
||||
}
|
||||
envelope = nip77.ParseNegMessage(message)
|
||||
if envelope == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch env := envelope.(type) {
|
||||
case *nostr.EventEnvelope:
|
||||
// check id
|
||||
hash := sha256.Sum256(env.Event.Serialize())
|
||||
id := hex.EncodeToString(hash[:])
|
||||
if id != env.Event.ID {
|
||||
if !env.Event.CheckID() {
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "invalid: id is computed incorrectly"})
|
||||
return
|
||||
}
|
||||
@@ -190,6 +204,7 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
var ok bool
|
||||
var writeErr error
|
||||
var skipBroadcast bool
|
||||
|
||||
if env.Event.Kind == 5 {
|
||||
// this always returns "blocked: " whenever it returns an error
|
||||
writeErr = srl.handleDeleteRequest(ctx, &env.Event)
|
||||
@@ -215,20 +230,53 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: ok, Reason: reason})
|
||||
case *nostr.CountEnvelope:
|
||||
if rl.CountEvents == nil {
|
||||
if rl.CountEvents == nil && rl.CountEventsHLL == nil {
|
||||
ws.WriteJSON(nostr.ClosedEnvelope{SubscriptionID: env.SubscriptionID, Reason: "unsupported: this relay does not support NIP-45"})
|
||||
return
|
||||
}
|
||||
|
||||
var total int64
|
||||
var hll *hyperloglog.HyperLogLog
|
||||
uneligibleForHLL := false
|
||||
|
||||
for _, filter := range env.Filters {
|
||||
srl := rl
|
||||
if rl.getSubRelayFromFilter != nil {
|
||||
srl = rl.getSubRelayFromFilter(filter)
|
||||
}
|
||||
total += srl.handleCountRequest(ctx, ws, filter)
|
||||
|
||||
if offset := nip45.HyperLogLogEventPubkeyOffsetForFilter(filter); offset != -1 && !uneligibleForHLL {
|
||||
partial, phll := srl.handleCountRequestWithHLL(ctx, ws, filter, offset)
|
||||
if phll != nil {
|
||||
if hll == nil {
|
||||
// in the first iteration (which should be the only case of the times)
|
||||
// we optimize slightly by assigning instead of merging
|
||||
hll = phll
|
||||
} else {
|
||||
hll.Merge(phll)
|
||||
}
|
||||
} else {
|
||||
// if any of the filters is uneligible then we will discard previous HLL results
|
||||
// and refuse to do HLL at all anymore for this query
|
||||
uneligibleForHLL = true
|
||||
hll = nil
|
||||
}
|
||||
total += partial
|
||||
} else {
|
||||
total += srl.handleCountRequest(ctx, ws, filter)
|
||||
}
|
||||
}
|
||||
ws.WriteJSON(nostr.CountEnvelope{SubscriptionID: env.SubscriptionID, Count: &total})
|
||||
|
||||
resp := nostr.CountEnvelope{
|
||||
SubscriptionID: env.SubscriptionID,
|
||||
Count: &total,
|
||||
}
|
||||
if hll != nil {
|
||||
resp.HyperLogLog = hll.GetRegisters()
|
||||
}
|
||||
|
||||
ws.WriteJSON(resp)
|
||||
|
||||
case *nostr.ReqEnvelope:
|
||||
eose := sync.WaitGroup{}
|
||||
eose.Add(len(env.Filters))
|
||||
@@ -284,6 +332,75 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "error: failed to authenticate"})
|
||||
}
|
||||
case *nip77.OpenEnvelope:
|
||||
srl := rl
|
||||
if rl.getSubRelayFromFilter != nil {
|
||||
srl = rl.getSubRelayFromFilter(env.Filter)
|
||||
if !srl.Negentropy {
|
||||
// ignore
|
||||
return
|
||||
}
|
||||
}
|
||||
vec, err := srl.startNegentropySession(ctx, env.Filter)
|
||||
if err != nil {
|
||||
// fail everything if any filter is rejected
|
||||
reason := err.Error()
|
||||
if strings.HasPrefix(reason, "auth-required:") {
|
||||
RequestAuth(ctx)
|
||||
}
|
||||
ws.WriteJSON(nip77.ErrorEnvelope{SubscriptionID: env.SubscriptionID, Reason: reason})
|
||||
return
|
||||
}
|
||||
|
||||
// reconcile to get the next message and return it
|
||||
neg := negentropy.New(vec, 1024*1024)
|
||||
out, err := neg.Reconcile(env.Message)
|
||||
if err != nil {
|
||||
ws.WriteJSON(nip77.ErrorEnvelope{SubscriptionID: env.SubscriptionID, Reason: err.Error()})
|
||||
return
|
||||
}
|
||||
ws.WriteJSON(nip77.MessageEnvelope{SubscriptionID: env.SubscriptionID, Message: out})
|
||||
|
||||
// if the message is not empty that means we'll probably have more reconciliation sessions, so store this
|
||||
if out != "" {
|
||||
deb := debounce.New(time.Second * 7)
|
||||
negSession := &NegentropySession{
|
||||
neg: neg,
|
||||
postponeClose: func() {
|
||||
deb(func() {
|
||||
ws.negentropySessions.Delete(env.SubscriptionID)
|
||||
})
|
||||
},
|
||||
}
|
||||
negSession.postponeClose()
|
||||
|
||||
ws.negentropySessions.Store(env.SubscriptionID, negSession)
|
||||
}
|
||||
case *nip77.MessageEnvelope:
|
||||
negSession, ok := ws.negentropySessions.Load(env.SubscriptionID)
|
||||
if !ok {
|
||||
// bad luck, your request was destroyed
|
||||
ws.WriteJSON(nip77.ErrorEnvelope{SubscriptionID: env.SubscriptionID, Reason: "CLOSED"})
|
||||
return
|
||||
}
|
||||
// reconcile to get the next message and return it
|
||||
out, err := negSession.neg.Reconcile(env.Message)
|
||||
if err != nil {
|
||||
ws.WriteJSON(nip77.ErrorEnvelope{SubscriptionID: env.SubscriptionID, Reason: err.Error()})
|
||||
ws.negentropySessions.Delete(env.SubscriptionID)
|
||||
return
|
||||
}
|
||||
ws.WriteJSON(nip77.MessageEnvelope{SubscriptionID: env.SubscriptionID, Message: out})
|
||||
|
||||
// if there is more reconciliation to do, postpone this
|
||||
if out != "" {
|
||||
negSession.postponeClose()
|
||||
} else {
|
||||
// otherwise we can just close it
|
||||
ws.negentropySessions.Delete(env.SubscriptionID)
|
||||
}
|
||||
case *nip77.CloseEnvelope:
|
||||
ws.negentropySessions.Delete(env.SubscriptionID)
|
||||
}
|
||||
}(message)
|
||||
}
|
||||
|
||||
95
listener.go
95
listener.go
@@ -3,6 +3,7 @@ package khatru
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
@@ -10,16 +11,16 @@ import (
|
||||
var ErrSubscriptionClosedByClient = errors.New("subscription closed by client")
|
||||
|
||||
type listenerSpec struct {
|
||||
subscriptionId string // kept here so we can easily match against it removeListenerId
|
||||
cancel context.CancelCauseFunc
|
||||
index int
|
||||
subrelay *Relay // this is important when we're dealing with routing, otherwise it will be always the same
|
||||
id string // kept here so we can easily match against it removeListenerId
|
||||
cancel context.CancelCauseFunc
|
||||
index int
|
||||
subrelay *Relay // this is important when we're dealing with routing, otherwise it will be always the same
|
||||
}
|
||||
|
||||
type listener struct {
|
||||
subscriptionId string // duplicated here so we can easily send it on notifyListeners
|
||||
filter nostr.Filter
|
||||
ws *WebSocket
|
||||
id string // duplicated here so we can easily send it on notifyListeners
|
||||
filter nostr.Filter
|
||||
ws *WebSocket
|
||||
}
|
||||
|
||||
func (rl *Relay) GetListeningFilters() []nostr.Filter {
|
||||
@@ -45,15 +46,15 @@ func (rl *Relay) addListener(
|
||||
if specs, ok := rl.clients[ws]; ok /* this will always be true unless client has disconnected very rapidly */ {
|
||||
idx := len(subrelay.listeners)
|
||||
rl.clients[ws] = append(specs, listenerSpec{
|
||||
subscriptionId: id,
|
||||
cancel: cancel,
|
||||
subrelay: subrelay,
|
||||
index: idx,
|
||||
id: id,
|
||||
cancel: cancel,
|
||||
subrelay: subrelay,
|
||||
index: idx,
|
||||
})
|
||||
subrelay.listeners = append(subrelay.listeners, listener{
|
||||
ws: ws,
|
||||
subscriptionId: id,
|
||||
filter: filter,
|
||||
ws: ws,
|
||||
id: id,
|
||||
filter: filter,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -66,23 +67,71 @@ func (rl *Relay) removeListenerId(ws *WebSocket, id string) {
|
||||
|
||||
if specs, ok := rl.clients[ws]; ok {
|
||||
// swap delete specs that match this id
|
||||
nswaps := 0
|
||||
for s, spec := range specs {
|
||||
if spec.subscriptionId == id {
|
||||
for s := len(specs) - 1; s >= 0; s-- {
|
||||
spec := specs[s]
|
||||
if spec.id == id {
|
||||
spec.cancel(ErrSubscriptionClosedByClient)
|
||||
specs[s] = specs[len(specs)-1-nswaps]
|
||||
nswaps++
|
||||
specs[s] = specs[len(specs)-1]
|
||||
specs = specs[0 : len(specs)-1]
|
||||
rl.clients[ws] = specs
|
||||
|
||||
// swap delete listeners one at a time, as they may be each in a different subrelay
|
||||
srl := spec.subrelay // == rl in normal cases, but different when this came from a route
|
||||
srl.listeners[spec.index] = srl.listeners[len(srl.listeners)-1]
|
||||
srl.listeners = srl.listeners[0 : len(srl.listeners)-1]
|
||||
|
||||
if spec.index != len(srl.listeners)-1 {
|
||||
movedFromIndex := len(srl.listeners) - 1
|
||||
moved := srl.listeners[movedFromIndex] // this wasn't removed, but will be moved
|
||||
srl.listeners[spec.index] = moved
|
||||
|
||||
// now we must update the the listener we just moved
|
||||
// so its .index reflects its new position on srl.listeners
|
||||
movedSpecs := rl.clients[moved.ws]
|
||||
idx := slices.IndexFunc(movedSpecs, func(ls listenerSpec) bool {
|
||||
return ls.index == movedFromIndex && ls.subrelay == srl
|
||||
})
|
||||
movedSpecs[idx].index = spec.index
|
||||
rl.clients[moved.ws] = movedSpecs
|
||||
}
|
||||
srl.listeners = srl.listeners[0 : len(srl.listeners)-1] // finally reduce the slice length
|
||||
}
|
||||
}
|
||||
rl.clients[ws] = specs[0 : len(specs)-nswaps]
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *Relay) removeClientAndListeners(ws *WebSocket) {
|
||||
rl.clientsMutex.Lock()
|
||||
defer rl.clientsMutex.Unlock()
|
||||
if specs, ok := rl.clients[ws]; ok {
|
||||
// swap delete listeners and delete client (all specs will be deleted)
|
||||
for s, spec := range specs {
|
||||
// no need to cancel contexts since they inherit from the main connection context
|
||||
// just delete the listeners (swap-delete)
|
||||
srl := spec.subrelay
|
||||
|
||||
if spec.index != len(srl.listeners)-1 {
|
||||
movedFromIndex := len(srl.listeners) - 1
|
||||
moved := srl.listeners[movedFromIndex] // this wasn't removed, but will be moved
|
||||
srl.listeners[spec.index] = moved
|
||||
|
||||
// temporarily update the spec of the listener being removed to have index == -1
|
||||
// (since it was removed) so it doesn't match in the search below
|
||||
rl.clients[ws][s].index = -1
|
||||
|
||||
// now we must update the the listener we just moved
|
||||
// so its .index reflects its new position on srl.listeners
|
||||
movedSpecs := rl.clients[moved.ws]
|
||||
idx := slices.IndexFunc(movedSpecs, func(ls listenerSpec) bool {
|
||||
return ls.index == movedFromIndex && ls.subrelay == srl
|
||||
})
|
||||
movedSpecs[idx].index = spec.index
|
||||
rl.clients[moved.ws] = movedSpecs
|
||||
}
|
||||
srl.listeners = srl.listeners[0 : len(srl.listeners)-1] // finally reduce the slice length
|
||||
}
|
||||
}
|
||||
delete(rl.clients, ws)
|
||||
}
|
||||
|
||||
func (rl *Relay) notifyListeners(event *nostr.Event) {
|
||||
for _, listener := range rl.listeners {
|
||||
if listener.filter.Matches(event) {
|
||||
@@ -91,7 +140,7 @@ func (rl *Relay) notifyListeners(event *nostr.Event) {
|
||||
return
|
||||
}
|
||||
}
|
||||
listener.ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &listener.subscriptionId, Event: *event})
|
||||
listener.ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &listener.id, Event: *event})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
188
listener_fuzz_test.go
Normal file
188
listener_fuzz_test.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func FuzzRandomListenerClientRemoving(f *testing.F) {
|
||||
f.Add(uint(20), uint(20), uint(1))
|
||||
f.Fuzz(func(t *testing.T, utw uint, ubs uint, ualf uint) {
|
||||
totalWebsockets := int(utw)
|
||||
baseSubs := int(ubs)
|
||||
addListenerFreq := int(ualf) + 1
|
||||
|
||||
rl := NewRelay()
|
||||
|
||||
f := nostr.Filter{Kinds: []int{1}}
|
||||
cancel := func(cause error) {}
|
||||
|
||||
websockets := make([]*WebSocket, 0, totalWebsockets*baseSubs)
|
||||
|
||||
l := 0
|
||||
|
||||
for i := 0; i < totalWebsockets; i++ {
|
||||
ws := &WebSocket{}
|
||||
websockets = append(websockets, ws)
|
||||
rl.clients[ws] = nil
|
||||
}
|
||||
|
||||
s := 0
|
||||
for j := 0; j < baseSubs; j++ {
|
||||
for i := 0; i < totalWebsockets; i++ {
|
||||
ws := websockets[i]
|
||||
w := idFromSeqUpper(i)
|
||||
|
||||
if s%addListenerFreq == 0 {
|
||||
l++
|
||||
rl.addListener(ws, w+":"+idFromSeqLower(j), rl, f, cancel)
|
||||
}
|
||||
|
||||
s++
|
||||
}
|
||||
}
|
||||
|
||||
require.Len(t, rl.clients, totalWebsockets)
|
||||
require.Len(t, rl.listeners, l)
|
||||
|
||||
for ws := range rl.clients {
|
||||
rl.removeClientAndListeners(ws)
|
||||
}
|
||||
|
||||
require.Len(t, rl.clients, 0)
|
||||
require.Len(t, rl.listeners, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzRandomListenerIdRemoving(f *testing.F) {
|
||||
f.Add(uint(20), uint(20), uint(1), uint(4))
|
||||
f.Fuzz(func(t *testing.T, utw uint, ubs uint, ualf uint, ualef uint) {
|
||||
totalWebsockets := int(utw)
|
||||
baseSubs := int(ubs)
|
||||
addListenerFreq := int(ualf) + 1
|
||||
addExtraListenerFreq := int(ualef) + 1
|
||||
|
||||
if totalWebsockets > 1024 || baseSubs > 1024 {
|
||||
return
|
||||
}
|
||||
|
||||
rl := NewRelay()
|
||||
|
||||
f := nostr.Filter{Kinds: []int{1}}
|
||||
cancel := func(cause error) {}
|
||||
websockets := make([]*WebSocket, 0, totalWebsockets)
|
||||
|
||||
type wsid struct {
|
||||
ws *WebSocket
|
||||
id string
|
||||
}
|
||||
|
||||
subs := make([]wsid, 0, totalWebsockets*baseSubs)
|
||||
extra := 0
|
||||
|
||||
for i := 0; i < totalWebsockets; i++ {
|
||||
ws := &WebSocket{}
|
||||
websockets = append(websockets, ws)
|
||||
rl.clients[ws] = nil
|
||||
}
|
||||
|
||||
s := 0
|
||||
for j := 0; j < baseSubs; j++ {
|
||||
for i := 0; i < totalWebsockets; i++ {
|
||||
ws := websockets[i]
|
||||
w := idFromSeqUpper(i)
|
||||
|
||||
if s%addListenerFreq == 0 {
|
||||
id := w + ":" + idFromSeqLower(j)
|
||||
rl.addListener(ws, id, rl, f, cancel)
|
||||
subs = append(subs, wsid{ws, id})
|
||||
|
||||
if s%addExtraListenerFreq == 0 {
|
||||
rl.addListener(ws, id, rl, f, cancel)
|
||||
extra++
|
||||
}
|
||||
}
|
||||
|
||||
s++
|
||||
}
|
||||
}
|
||||
|
||||
require.Len(t, rl.clients, totalWebsockets)
|
||||
require.Len(t, rl.listeners, len(subs)+extra)
|
||||
|
||||
rand.Shuffle(len(subs), func(i, j int) {
|
||||
subs[i], subs[j] = subs[j], subs[i]
|
||||
})
|
||||
for _, wsidToRemove := range subs {
|
||||
rl.removeListenerId(wsidToRemove.ws, wsidToRemove.id)
|
||||
}
|
||||
|
||||
require.Len(t, rl.listeners, 0)
|
||||
require.Len(t, rl.clients, totalWebsockets)
|
||||
for _, specs := range rl.clients {
|
||||
require.Len(t, specs, 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzRouterListenersPabloCrash(f *testing.F) {
|
||||
f.Add(uint(3), uint(6), uint(2), uint(20))
|
||||
f.Fuzz(func(t *testing.T, totalRelays uint, totalConns uint, subFreq uint, subIterations uint) {
|
||||
totalRelays++
|
||||
totalConns++
|
||||
subFreq++
|
||||
subIterations++
|
||||
|
||||
rl := NewRelay()
|
||||
|
||||
relays := make([]*Relay, int(totalRelays))
|
||||
for i := 0; i < int(totalRelays); i++ {
|
||||
relays[i] = NewRelay()
|
||||
}
|
||||
|
||||
conns := make([]*WebSocket, int(totalConns))
|
||||
for i := 0; i < int(totalConns); i++ {
|
||||
ws := &WebSocket{}
|
||||
conns[i] = ws
|
||||
rl.clients[ws] = make([]listenerSpec, 0, subIterations)
|
||||
}
|
||||
|
||||
f := nostr.Filter{Kinds: []int{1}}
|
||||
cancel := func(cause error) {}
|
||||
|
||||
type wsid struct {
|
||||
ws *WebSocket
|
||||
id string
|
||||
}
|
||||
|
||||
s := 0
|
||||
subs := make([]wsid, 0, subIterations*totalConns*totalRelays)
|
||||
for i, conn := range conns {
|
||||
w := idFromSeqUpper(i)
|
||||
for j := 0; j < int(subIterations); j++ {
|
||||
id := w + ":" + idFromSeqLower(j)
|
||||
for _, rlt := range relays {
|
||||
if s%int(subFreq) == 0 {
|
||||
rl.addListener(conn, id, rlt, f, cancel)
|
||||
subs = append(subs, wsid{conn, id})
|
||||
}
|
||||
s++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, wsid := range subs {
|
||||
rl.removeListenerId(wsid.ws, wsid.id)
|
||||
}
|
||||
|
||||
for _, wsid := range subs {
|
||||
require.Len(t, rl.clients[wsid.ws], 0)
|
||||
}
|
||||
for _, rlt := range relays {
|
||||
require.Len(t, rlt.listeners, 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
545
listener_test.go
Normal file
545
listener_test.go
Normal file
@@ -0,0 +1,545 @@
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func idFromSeqUpper(seq int) string { return idFromSeq(seq, 65, 90) }
|
||||
func idFromSeqLower(seq int) string { return idFromSeq(seq, 97, 122) }
|
||||
func idFromSeq(seq int, min, max int) string {
|
||||
maxSeq := max - min + 1
|
||||
nLetters := seq/maxSeq + 1
|
||||
result := strings.Builder{}
|
||||
result.Grow(nLetters)
|
||||
for l := 0; l < nLetters; l++ {
|
||||
letter := rune(seq%maxSeq + min)
|
||||
result.WriteRune(letter)
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func TestListenerSetupAndRemoveOnce(t *testing.T) {
|
||||
rl := NewRelay()
|
||||
|
||||
ws1 := &WebSocket{}
|
||||
ws2 := &WebSocket{}
|
||||
|
||||
f1 := nostr.Filter{Kinds: []int{1}}
|
||||
f2 := nostr.Filter{Kinds: []int{2}}
|
||||
f3 := nostr.Filter{Kinds: []int{3}}
|
||||
|
||||
rl.clients[ws1] = nil
|
||||
rl.clients[ws2] = nil
|
||||
|
||||
var cancel func(cause error) = nil
|
||||
|
||||
t.Run("adding listeners", func(t *testing.T) {
|
||||
rl.addListener(ws1, "1a", rl, f1, cancel)
|
||||
rl.addListener(ws1, "1b", rl, f2, cancel)
|
||||
rl.addListener(ws2, "2a", rl, f3, cancel)
|
||||
rl.addListener(ws1, "1c", rl, f3, cancel)
|
||||
|
||||
require.Equal(t, map[*WebSocket][]listenerSpec{
|
||||
ws1: {
|
||||
{"1a", cancel, 0, rl},
|
||||
{"1b", cancel, 1, rl},
|
||||
{"1c", cancel, 3, rl},
|
||||
},
|
||||
ws2: {
|
||||
{"2a", cancel, 2, rl},
|
||||
},
|
||||
}, rl.clients)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"1a", f1, ws1},
|
||||
{"1b", f2, ws1},
|
||||
{"2a", f3, ws2},
|
||||
{"1c", f3, ws1},
|
||||
}, rl.listeners)
|
||||
})
|
||||
|
||||
t.Run("removing a client", func(t *testing.T) {
|
||||
rl.removeClientAndListeners(ws1)
|
||||
|
||||
require.Equal(t, map[*WebSocket][]listenerSpec{
|
||||
ws2: {
|
||||
{"2a", cancel, 0, rl},
|
||||
},
|
||||
}, rl.clients)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"2a", f3, ws2},
|
||||
}, rl.listeners)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListenerMoreConvolutedCase(t *testing.T) {
|
||||
rl := NewRelay()
|
||||
|
||||
ws1 := &WebSocket{}
|
||||
ws2 := &WebSocket{}
|
||||
ws3 := &WebSocket{}
|
||||
ws4 := &WebSocket{}
|
||||
|
||||
f1 := nostr.Filter{Kinds: []int{1}}
|
||||
f2 := nostr.Filter{Kinds: []int{2}}
|
||||
f3 := nostr.Filter{Kinds: []int{3}}
|
||||
|
||||
rl.clients[ws1] = nil
|
||||
rl.clients[ws2] = nil
|
||||
rl.clients[ws3] = nil
|
||||
rl.clients[ws4] = nil
|
||||
|
||||
var cancel func(cause error) = nil
|
||||
|
||||
t.Run("adding listeners", func(t *testing.T) {
|
||||
rl.addListener(ws1, "c", rl, f1, cancel)
|
||||
rl.addListener(ws2, "b", rl, f2, cancel)
|
||||
rl.addListener(ws3, "a", rl, f3, cancel)
|
||||
rl.addListener(ws4, "d", rl, f3, cancel)
|
||||
rl.addListener(ws2, "b", rl, f1, cancel)
|
||||
|
||||
require.Equal(t, map[*WebSocket][]listenerSpec{
|
||||
ws1: {
|
||||
{"c", cancel, 0, rl},
|
||||
},
|
||||
ws2: {
|
||||
{"b", cancel, 1, rl},
|
||||
{"b", cancel, 4, rl},
|
||||
},
|
||||
ws3: {
|
||||
{"a", cancel, 2, rl},
|
||||
},
|
||||
ws4: {
|
||||
{"d", cancel, 3, rl},
|
||||
},
|
||||
}, rl.clients)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"c", f1, ws1},
|
||||
{"b", f2, ws2},
|
||||
{"a", f3, ws3},
|
||||
{"d", f3, ws4},
|
||||
{"b", f1, ws2},
|
||||
}, rl.listeners)
|
||||
})
|
||||
|
||||
t.Run("removing a client", func(t *testing.T) {
|
||||
rl.removeClientAndListeners(ws2)
|
||||
|
||||
require.Equal(t, map[*WebSocket][]listenerSpec{
|
||||
ws1: {
|
||||
{"c", cancel, 0, rl},
|
||||
},
|
||||
ws3: {
|
||||
{"a", cancel, 2, rl},
|
||||
},
|
||||
ws4: {
|
||||
{"d", cancel, 1, rl},
|
||||
},
|
||||
}, rl.clients)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"c", f1, ws1},
|
||||
{"d", f3, ws4},
|
||||
{"a", f3, ws3},
|
||||
}, rl.listeners)
|
||||
})
|
||||
|
||||
t.Run("reorganize the first case differently and then remove again", func(t *testing.T) {
|
||||
rl.clients = map[*WebSocket][]listenerSpec{
|
||||
ws1: {
|
||||
{"c", cancel, 1, rl},
|
||||
},
|
||||
ws2: {
|
||||
{"b", cancel, 2, rl},
|
||||
{"b", cancel, 4, rl},
|
||||
},
|
||||
ws3: {
|
||||
{"a", cancel, 0, rl},
|
||||
},
|
||||
ws4: {
|
||||
{"d", cancel, 3, rl},
|
||||
},
|
||||
}
|
||||
rl.listeners = []listener{
|
||||
{"a", f3, ws3},
|
||||
{"c", f1, ws1},
|
||||
{"b", f2, ws2},
|
||||
{"d", f3, ws4},
|
||||
{"b", f1, ws2},
|
||||
}
|
||||
|
||||
rl.removeClientAndListeners(ws2)
|
||||
|
||||
require.Equal(t, map[*WebSocket][]listenerSpec{
|
||||
ws1: {
|
||||
{"c", cancel, 1, rl},
|
||||
},
|
||||
ws3: {
|
||||
{"a", cancel, 0, rl},
|
||||
},
|
||||
ws4: {
|
||||
{"d", cancel, 2, rl},
|
||||
},
|
||||
}, rl.clients)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"a", f3, ws3},
|
||||
{"c", f1, ws1},
|
||||
{"d", f3, ws4},
|
||||
}, rl.listeners)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListenerMoreStuffWithMultipleRelays(t *testing.T) {
|
||||
rl := NewRelay()
|
||||
|
||||
ws1 := &WebSocket{}
|
||||
ws2 := &WebSocket{}
|
||||
ws3 := &WebSocket{}
|
||||
ws4 := &WebSocket{}
|
||||
|
||||
f1 := nostr.Filter{Kinds: []int{1}}
|
||||
f2 := nostr.Filter{Kinds: []int{2}}
|
||||
f3 := nostr.Filter{Kinds: []int{3}}
|
||||
|
||||
rlx := NewRelay()
|
||||
rly := NewRelay()
|
||||
rlz := NewRelay()
|
||||
|
||||
rl.clients[ws1] = nil
|
||||
rl.clients[ws2] = nil
|
||||
rl.clients[ws3] = nil
|
||||
rl.clients[ws4] = nil
|
||||
|
||||
var cancel func(cause error) = nil
|
||||
|
||||
t.Run("adding listeners", func(t *testing.T) {
|
||||
rl.addListener(ws1, "c", rlx, f1, cancel)
|
||||
rl.addListener(ws2, "b", rly, f2, cancel)
|
||||
rl.addListener(ws3, "a", rlz, f3, cancel)
|
||||
rl.addListener(ws4, "d", rlx, f3, cancel)
|
||||
rl.addListener(ws4, "e", rlx, f3, cancel)
|
||||
rl.addListener(ws3, "a", rlx, f3, cancel)
|
||||
rl.addListener(ws4, "e", rly, f3, cancel)
|
||||
rl.addListener(ws3, "f", rly, f3, cancel)
|
||||
rl.addListener(ws1, "g", rlz, f1, cancel)
|
||||
rl.addListener(ws2, "g", rlz, f2, cancel)
|
||||
|
||||
require.Equal(t, map[*WebSocket][]listenerSpec{
|
||||
ws1: {
|
||||
{"c", cancel, 0, rlx},
|
||||
{"g", cancel, 1, rlz},
|
||||
},
|
||||
ws2: {
|
||||
{"b", cancel, 0, rly},
|
||||
{"g", cancel, 2, rlz},
|
||||
},
|
||||
ws3: {
|
||||
{"a", cancel, 0, rlz},
|
||||
{"a", cancel, 3, rlx},
|
||||
{"f", cancel, 2, rly},
|
||||
},
|
||||
ws4: {
|
||||
{"d", cancel, 1, rlx},
|
||||
{"e", cancel, 2, rlx},
|
||||
{"e", cancel, 1, rly},
|
||||
},
|
||||
}, rl.clients)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"c", f1, ws1},
|
||||
{"d", f3, ws4},
|
||||
{"e", f3, ws4},
|
||||
{"a", f3, ws3},
|
||||
}, rlx.listeners)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"b", f2, ws2},
|
||||
{"e", f3, ws4},
|
||||
{"f", f3, ws3},
|
||||
}, rly.listeners)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"a", f3, ws3},
|
||||
{"g", f1, ws1},
|
||||
{"g", f2, ws2},
|
||||
}, rlz.listeners)
|
||||
})
|
||||
|
||||
t.Run("removing a subscription id", func(t *testing.T) {
|
||||
// removing 'd' from ws4
|
||||
rl.clients[ws4][0].cancel = func(cause error) {} // set since removing will call it
|
||||
rl.removeListenerId(ws4, "d")
|
||||
|
||||
require.Equal(t, map[*WebSocket][]listenerSpec{
|
||||
ws1: {
|
||||
{"c", cancel, 0, rlx},
|
||||
{"g", cancel, 1, rlz},
|
||||
},
|
||||
ws2: {
|
||||
{"b", cancel, 0, rly},
|
||||
{"g", cancel, 2, rlz},
|
||||
},
|
||||
ws3: {
|
||||
{"a", cancel, 0, rlz},
|
||||
{"a", cancel, 1, rlx},
|
||||
{"f", cancel, 2, rly},
|
||||
},
|
||||
ws4: {
|
||||
{"e", cancel, 1, rly},
|
||||
{"e", cancel, 2, rlx},
|
||||
},
|
||||
}, rl.clients)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"c", f1, ws1},
|
||||
{"a", f3, ws3},
|
||||
{"e", f3, ws4},
|
||||
}, rlx.listeners)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"b", f2, ws2},
|
||||
{"e", f3, ws4},
|
||||
{"f", f3, ws3},
|
||||
}, rly.listeners)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"a", f3, ws3},
|
||||
{"g", f1, ws1},
|
||||
{"g", f2, ws2},
|
||||
}, rlz.listeners)
|
||||
})
|
||||
|
||||
t.Run("removing another subscription id", func(t *testing.T) {
|
||||
// removing 'a' from ws3
|
||||
rl.clients[ws3][0].cancel = func(cause error) {} // set since removing will call it
|
||||
rl.clients[ws3][1].cancel = func(cause error) {} // set since removing will call it
|
||||
rl.removeListenerId(ws3, "a")
|
||||
|
||||
require.Equal(t, map[*WebSocket][]listenerSpec{
|
||||
ws1: {
|
||||
{"c", cancel, 0, rlx},
|
||||
{"g", cancel, 1, rlz},
|
||||
},
|
||||
ws2: {
|
||||
{"b", cancel, 0, rly},
|
||||
{"g", cancel, 0, rlz},
|
||||
},
|
||||
ws3: {
|
||||
{"f", cancel, 2, rly},
|
||||
},
|
||||
ws4: {
|
||||
{"e", cancel, 1, rly},
|
||||
{"e", cancel, 1, rlx},
|
||||
},
|
||||
}, rl.clients)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"c", f1, ws1},
|
||||
{"e", f3, ws4},
|
||||
}, rlx.listeners)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"b", f2, ws2},
|
||||
{"e", f3, ws4},
|
||||
{"f", f3, ws3},
|
||||
}, rly.listeners)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"g", f2, ws2},
|
||||
{"g", f1, ws1},
|
||||
}, rlz.listeners)
|
||||
})
|
||||
|
||||
t.Run("removing a connection", func(t *testing.T) {
|
||||
rl.removeClientAndListeners(ws2)
|
||||
|
||||
require.Equal(t, map[*WebSocket][]listenerSpec{
|
||||
ws1: {
|
||||
{"c", cancel, 0, rlx},
|
||||
{"g", cancel, 0, rlz},
|
||||
},
|
||||
ws3: {
|
||||
{"f", cancel, 0, rly},
|
||||
},
|
||||
ws4: {
|
||||
{"e", cancel, 1, rly},
|
||||
{"e", cancel, 1, rlx},
|
||||
},
|
||||
}, rl.clients)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"c", f1, ws1},
|
||||
{"e", f3, ws4},
|
||||
}, rlx.listeners)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"f", f3, ws3},
|
||||
{"e", f3, ws4},
|
||||
}, rly.listeners)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"g", f1, ws1},
|
||||
}, rlz.listeners)
|
||||
})
|
||||
|
||||
t.Run("removing another subscription id", func(t *testing.T) {
|
||||
// removing 'e' from ws4
|
||||
rl.clients[ws4][0].cancel = func(cause error) {} // set since removing will call it
|
||||
rl.clients[ws4][1].cancel = func(cause error) {} // set since removing will call it
|
||||
rl.removeListenerId(ws4, "e")
|
||||
|
||||
require.Equal(t, map[*WebSocket][]listenerSpec{
|
||||
ws1: {
|
||||
{"c", cancel, 0, rlx},
|
||||
{"g", cancel, 0, rlz},
|
||||
},
|
||||
ws3: {
|
||||
{"f", cancel, 0, rly},
|
||||
},
|
||||
ws4: {},
|
||||
}, rl.clients)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"c", f1, ws1},
|
||||
}, rlx.listeners)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"f", f3, ws3},
|
||||
}, rly.listeners)
|
||||
|
||||
require.Equal(t, []listener{
|
||||
{"g", f1, ws1},
|
||||
}, rlz.listeners)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRandomListenerClientRemoving(t *testing.T) {
|
||||
rl := NewRelay()
|
||||
|
||||
f := nostr.Filter{Kinds: []int{1}}
|
||||
cancel := func(cause error) {}
|
||||
|
||||
websockets := make([]*WebSocket, 0, 20)
|
||||
|
||||
l := 0
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
ws := &WebSocket{}
|
||||
websockets = append(websockets, ws)
|
||||
rl.clients[ws] = nil
|
||||
}
|
||||
|
||||
for j := 0; j < 20; j++ {
|
||||
for i := 0; i < 20; i++ {
|
||||
ws := websockets[i]
|
||||
w := idFromSeqUpper(i)
|
||||
|
||||
if rand.Intn(2) < 1 {
|
||||
l++
|
||||
rl.addListener(ws, w+":"+idFromSeqLower(j), rl, f, cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require.Len(t, rl.clients, 20)
|
||||
require.Len(t, rl.listeners, l)
|
||||
|
||||
for ws := range rl.clients {
|
||||
rl.removeClientAndListeners(ws)
|
||||
}
|
||||
|
||||
require.Len(t, rl.clients, 0)
|
||||
require.Len(t, rl.listeners, 0)
|
||||
}
|
||||
|
||||
func TestRandomListenerIdRemoving(t *testing.T) {
|
||||
rl := NewRelay()
|
||||
|
||||
f := nostr.Filter{Kinds: []int{1}}
|
||||
cancel := func(cause error) {}
|
||||
|
||||
websockets := make([]*WebSocket, 0, 20)
|
||||
|
||||
type wsid struct {
|
||||
ws *WebSocket
|
||||
id string
|
||||
}
|
||||
|
||||
subs := make([]wsid, 0, 20*20)
|
||||
extra := 0
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
ws := &WebSocket{}
|
||||
websockets = append(websockets, ws)
|
||||
rl.clients[ws] = nil
|
||||
}
|
||||
|
||||
for j := 0; j < 20; j++ {
|
||||
for i := 0; i < 20; i++ {
|
||||
ws := websockets[i]
|
||||
w := idFromSeqUpper(i)
|
||||
|
||||
if rand.Intn(2) < 1 {
|
||||
id := w + ":" + idFromSeqLower(j)
|
||||
rl.addListener(ws, id, rl, f, cancel)
|
||||
subs = append(subs, wsid{ws, id})
|
||||
|
||||
if rand.Intn(5) < 1 {
|
||||
rl.addListener(ws, id, rl, f, cancel)
|
||||
extra++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require.Len(t, rl.clients, 20)
|
||||
require.Len(t, rl.listeners, len(subs)+extra)
|
||||
|
||||
rand.Shuffle(len(subs), func(i, j int) {
|
||||
subs[i], subs[j] = subs[j], subs[i]
|
||||
})
|
||||
for _, wsidToRemove := range subs {
|
||||
rl.removeListenerId(wsidToRemove.ws, wsidToRemove.id)
|
||||
}
|
||||
|
||||
require.Len(t, rl.listeners, 0)
|
||||
require.Len(t, rl.clients, 20)
|
||||
for _, specs := range rl.clients {
|
||||
require.Len(t, specs, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterListenersPabloCrash(t *testing.T) {
|
||||
rl := NewRelay()
|
||||
|
||||
rla := NewRelay()
|
||||
rlb := NewRelay()
|
||||
|
||||
ws1 := &WebSocket{}
|
||||
ws2 := &WebSocket{}
|
||||
ws3 := &WebSocket{}
|
||||
|
||||
rl.clients[ws1] = nil
|
||||
rl.clients[ws2] = nil
|
||||
rl.clients[ws3] = nil
|
||||
|
||||
f := nostr.Filter{Kinds: []int{1}}
|
||||
cancel := func(cause error) {}
|
||||
|
||||
rl.addListener(ws1, ":1", rla, f, cancel)
|
||||
rl.addListener(ws2, ":1", rlb, f, cancel)
|
||||
rl.addListener(ws3, "a", rlb, f, cancel)
|
||||
rl.addListener(ws3, "b", rla, f, cancel)
|
||||
rl.addListener(ws3, "c", rlb, f, cancel)
|
||||
|
||||
rl.removeClientAndListeners(ws1)
|
||||
rl.removeClientAndListeners(ws3)
|
||||
}
|
||||
53
negentropy.go
Normal file
53
negentropy.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/fiatjaf/eventstore"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip77/negentropy"
|
||||
"github.com/nbd-wtf/go-nostr/nip77/negentropy/storage/vector"
|
||||
)
|
||||
|
||||
type NegentropySession struct {
|
||||
neg *negentropy.Negentropy
|
||||
postponeClose func()
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, fmt.Errorf("invalid limit 0")
|
||||
}
|
||||
for _, reject := range rl.RejectFilter {
|
||||
if reject, msg := reject(ctx, filter); reject {
|
||||
return nil, errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
|
||||
}
|
||||
}
|
||||
|
||||
// fetch events and add them to a negentropy Vector store
|
||||
vec := vector.New()
|
||||
for _, query := range rl.QueryEvents {
|
||||
ch, err := query(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.Seal()
|
||||
|
||||
return vec, nil
|
||||
}
|
||||
7
nip11.go
7
nip11.go
@@ -11,10 +11,13 @@ func (rl *Relay) HandleNIP11(w http.ResponseWriter, r *http.Request) {
|
||||
info := *rl.Info
|
||||
|
||||
if len(rl.DeleteEvent) > 0 {
|
||||
info.SupportedNIPs = append(info.SupportedNIPs, 9)
|
||||
info.AddSupportedNIP(9)
|
||||
}
|
||||
if len(rl.CountEvents) > 0 {
|
||||
info.SupportedNIPs = append(info.SupportedNIPs, 45)
|
||||
info.AddSupportedNIP(45)
|
||||
}
|
||||
if rl.Negentropy {
|
||||
info.AddSupportedNIP(77)
|
||||
}
|
||||
|
||||
for _, ovw := range rl.OverwriteRelayInformation {
|
||||
|
||||
14
nip86.go
14
nip86.go
@@ -65,16 +65,22 @@ func (rl *Relay) HandleNIP86(w http.ResponseWriter, r *http.Request) {
|
||||
resp.Error = "missing auth"
|
||||
goto respond
|
||||
}
|
||||
if evtj, err := base64.StdEncoding.DecodeString(spl[1]); err != nil {
|
||||
|
||||
evtj, err := base64.StdEncoding.DecodeString(spl[1])
|
||||
if err != nil {
|
||||
resp.Error = "invalid base64 auth"
|
||||
goto respond
|
||||
} else if err := json.Unmarshal(evtj, &evt); err != nil {
|
||||
}
|
||||
if err := json.Unmarshal(evtj, &evt); err != nil {
|
||||
resp.Error = "invalid auth event json"
|
||||
goto respond
|
||||
} else if ok, _ := evt.CheckSignature(); !ok {
|
||||
}
|
||||
if ok, _ := evt.CheckSignature(); !ok {
|
||||
resp.Error = "invalid auth event"
|
||||
goto respond
|
||||
} else if uTag := evt.Tags.GetFirst([]string{"u", ""}); uTag == nil || getServiceBaseURL(r) != (*uTag)[1] {
|
||||
}
|
||||
|
||||
if uTag := evt.Tags.GetFirst([]string{"u", ""}); uTag == nil || rl.ServiceURL != (*uTag)[1] {
|
||||
resp.Error = "invalid 'u' tag"
|
||||
goto respond
|
||||
} else if pht := evt.Tags.GetFirst([]string{"payload", hex.EncodeToString(payloadHash[:])}); pht == nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
@@ -66,11 +67,15 @@ func PreventLargeTags(maxTagValueLen int) func(context.Context, *nostr.Event) (b
|
||||
|
||||
// RestrictToSpecifiedKinds returns a function that can be used as a RejectFilter that will reject
|
||||
// any events with kinds different than the specified ones.
|
||||
func RestrictToSpecifiedKinds(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
|
||||
slices.Sort(kinds)
|
||||
|
||||
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
if allowEphemeral && nostr.IsEphemeralKind(event.Kind) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
if _, allowed := slices.BinarySearch(kinds, uint16(event.Kind)); allowed {
|
||||
return false, ""
|
||||
}
|
||||
@@ -79,7 +84,8 @@ func RestrictToSpecifiedKinds(kinds ...uint16) func(context.Context, *nostr.Even
|
||||
}
|
||||
}
|
||||
|
||||
func PreventTimestampsInThePast(thresholdSeconds nostr.Timestamp) func(context.Context, *nostr.Event) (bool, string) {
|
||||
func PreventTimestampsInThePast(threshold time.Duration) func(context.Context, *nostr.Event) (bool, string) {
|
||||
thresholdSeconds := nostr.Timestamp(threshold.Seconds())
|
||||
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
if nostr.Now()-event.CreatedAt > thresholdSeconds {
|
||||
return true, "event too old"
|
||||
@@ -88,7 +94,8 @@ func PreventTimestampsInThePast(thresholdSeconds nostr.Timestamp) func(context.C
|
||||
}
|
||||
}
|
||||
|
||||
func PreventTimestampsInTheFuture(thresholdSeconds nostr.Timestamp) func(context.Context, *nostr.Event) (bool, string) {
|
||||
func PreventTimestampsInTheFuture(threshold time.Duration) func(context.Context, *nostr.Event) (bool, string) {
|
||||
thresholdSeconds := nostr.Timestamp(threshold.Seconds())
|
||||
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
if event.CreatedAt-nostr.Now() > thresholdSeconds {
|
||||
return true, "event too much in the future"
|
||||
|
||||
@@ -18,13 +18,12 @@ func startRateLimitSystem[K comparable](
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(interval)
|
||||
negativeBuckets.Range(func(key K, bucket *atomic.Int32) bool {
|
||||
for key, bucket := range negativeBuckets.Range {
|
||||
newv := bucket.Add(int32(-tokensPerInterval))
|
||||
if newv <= 0 {
|
||||
negativeBuckets.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ func EventIPRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens
|
||||
rl := startRateLimitSystem[string](tokensPerInterval, interval, maxTokens)
|
||||
|
||||
return func(ctx context.Context, _ *nostr.Event) (reject bool, msg string) {
|
||||
return rl(khatru.GetIP(ctx)), "rate-limited: slow down, please"
|
||||
ip := khatru.GetIP(ctx)
|
||||
if ip == "" {
|
||||
return false, ""
|
||||
}
|
||||
return rl(ip), "rate-limited: slow down, please"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,11 @@ func ApplySaneDefaults(relay *khatru.Relay) {
|
||||
)
|
||||
|
||||
relay.RejectFilter = append(relay.RejectFilter,
|
||||
NoEmptyFilters,
|
||||
NoComplexFilters,
|
||||
FilterIPRateLimiter(20, time.Minute, 100),
|
||||
)
|
||||
|
||||
relay.RejectConnection = append(relay.RejectConnection,
|
||||
ConnectionRateLimiter(1, time.Minute*5, 3),
|
||||
ConnectionRateLimiter(1, time.Minute*5, 10),
|
||||
)
|
||||
}
|
||||
|
||||
11
relay.go
11
relay.go
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/fasthttp/websocket"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip11"
|
||||
"github.com/nbd-wtf/go-nostr/nip45/hyperloglog"
|
||||
)
|
||||
|
||||
func NewRelay() *Relay {
|
||||
@@ -20,7 +21,7 @@ func NewRelay() *Relay {
|
||||
Info: &nip11.RelayInformationDocument{
|
||||
Software: "https://github.com/fiatjaf/khatru",
|
||||
Version: "n/a",
|
||||
SupportedNIPs: []int{1, 11, 42, 70, 86},
|
||||
SupportedNIPs: []any{1, 11, 42, 70, 86},
|
||||
},
|
||||
|
||||
upgrader: websocket.Upgrader{
|
||||
@@ -46,19 +47,20 @@ func NewRelay() *Relay {
|
||||
type Relay struct {
|
||||
ServiceURL string
|
||||
|
||||
// these structs keeps track of all the things that can be customized when handling events or requests
|
||||
// hooks that will be called at various times
|
||||
RejectEvent []func(ctx context.Context, event *nostr.Event) (reject bool, msg string)
|
||||
OverwriteDeletionOutcome []func(ctx context.Context, target *nostr.Event, deletion *nostr.Event) (acceptDeletion bool, msg string)
|
||||
StoreEvent []func(ctx context.Context, event *nostr.Event) error
|
||||
ReplaceEvent []func(ctx context.Context, event *nostr.Event) error
|
||||
DeleteEvent []func(ctx context.Context, event *nostr.Event) error
|
||||
OnEventSaved []func(ctx context.Context, event *nostr.Event)
|
||||
OnEphemeralEvent []func(ctx context.Context, event *nostr.Event)
|
||||
RejectFilter []func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
|
||||
RejectCountFilter []func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
|
||||
OverwriteFilter []func(ctx context.Context, filter *nostr.Filter)
|
||||
OverwriteCountFilter []func(ctx context.Context, filter *nostr.Filter)
|
||||
QueryEvents []func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)
|
||||
CountEvents []func(ctx context.Context, filter nostr.Filter) (int64, error)
|
||||
CountEventsHLL []func(ctx context.Context, filter nostr.Filter, offset int) (int64, *hyperloglog.HyperLogLog, error)
|
||||
RejectConnection []func(r *http.Request) bool
|
||||
OnConnect []func(ctx context.Context)
|
||||
OnDisconnect []func(ctx context.Context)
|
||||
@@ -90,6 +92,9 @@ type Relay struct {
|
||||
listeners []listener
|
||||
clientsMutex sync.Mutex
|
||||
|
||||
// set this to true to support negentropy
|
||||
Negentropy bool
|
||||
|
||||
// in case you call Server.Start
|
||||
Addr string
|
||||
serveMux *http.ServeMux
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip45/hyperloglog"
|
||||
)
|
||||
|
||||
func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGroup, ws *WebSocket, filter nostr.Filter) error {
|
||||
@@ -61,12 +62,7 @@ func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGr
|
||||
}
|
||||
|
||||
func (rl *Relay) handleCountRequest(ctx context.Context, ws *WebSocket, filter nostr.Filter) int64 {
|
||||
// overwrite the filter (for example, to eliminate some kinds or tags that we know we don't support)
|
||||
for _, ovw := range rl.OverwriteCountFilter {
|
||||
ovw(ctx, &filter)
|
||||
}
|
||||
|
||||
// then check if we'll reject this filter
|
||||
// check if we'll reject this filter
|
||||
for _, reject := range rl.RejectCountFilter {
|
||||
if rejecting, msg := reject(ctx, filter); rejecting {
|
||||
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
||||
@@ -86,3 +82,38 @@ func (rl *Relay) handleCountRequest(ctx context.Context, ws *WebSocket, filter n
|
||||
|
||||
return subtotal
|
||||
}
|
||||
|
||||
func (rl *Relay) handleCountRequestWithHLL(
|
||||
ctx context.Context,
|
||||
ws *WebSocket,
|
||||
filter nostr.Filter,
|
||||
offset int,
|
||||
) (int64, *hyperloglog.HyperLogLog) {
|
||||
// check if we'll reject this filter
|
||||
for _, reject := range rl.RejectCountFilter {
|
||||
if rejecting, msg := reject(ctx, filter); rejecting {
|
||||
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
// run the functions to count (generally it will be just one)
|
||||
var subtotal int64 = 0
|
||||
var hll *hyperloglog.HyperLogLog
|
||||
for _, countHLL := range rl.CountEventsHLL {
|
||||
res, fhll, err := countHLL(ctx, filter, offset)
|
||||
if err != nil {
|
||||
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
|
||||
}
|
||||
subtotal += res
|
||||
if fhll != nil {
|
||||
if hll == nil {
|
||||
hll = fhll
|
||||
} else {
|
||||
hll.Merge(fhll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return subtotal, hll
|
||||
}
|
||||
|
||||
7
utils.go
7
utils.go
@@ -41,7 +41,12 @@ func GetAuthed(ctx context.Context) string {
|
||||
}
|
||||
|
||||
func GetIP(ctx context.Context) string {
|
||||
return GetIPFromRequest(GetConnection(ctx).Request)
|
||||
conn := GetConnection(ctx)
|
||||
if conn == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return GetIPFromRequest(conn.Request)
|
||||
}
|
||||
|
||||
func GetSubscriptionID(ctx context.Context) string {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/fasthttp/websocket"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
)
|
||||
|
||||
type WebSocket struct {
|
||||
@@ -14,11 +16,18 @@ type WebSocket struct {
|
||||
// original request
|
||||
Request *http.Request
|
||||
|
||||
// this Context will be canceled whenever the connection is closed from the client side or server-side.
|
||||
Context context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
// nip42
|
||||
Challenge string
|
||||
AuthedPublicKey string
|
||||
Authed chan struct{}
|
||||
|
||||
// nip77
|
||||
negentropySessions *xsync.MapOf[string, *NegentropySession]
|
||||
|
||||
authLock sync.Mutex
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user