From 0a60285e30c31212af9e9e013750f73f5200633b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 16 Dec 2021 20:47:53 -0300 Subject: [PATCH] upgrade interfaces, add local filtering for subscriptions and some other things I forgot. also a README with examples. --- .gitignore | 1 + README.md | 75 +++++++++++++++++++++++++++++++++++++++ event/event.go | 32 +++++++---------- filter/filter.go | 12 +++++++ go.mod | 2 +- go.sum | 4 +-- relaypool/relaypool.go | 24 ++++++++++--- relaypool/subscription.go | 31 +++++++++------- 8 files changed, 141 insertions(+), 40 deletions(-) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c36ead --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +go-nostr diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa9b6d2 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +go-nostr +======== + +A set of useful things for [Nostr Protocol](https://github.com/fiatjaf/nostr) implementations. + +GoDoc + + +### Subscribing to a set of relays + +```go +pool := relaypool.New() + +pool.Add("wss://relay.nostr.com/", &relaypool.Policy{ + SimplePolicy: relaypool.SimplePolicy{Read: true, Write: true}, +}) +pool.Add("wss://nostrrelay.example.com/", &relaypool.Policy{ + SimplePolicy: relaypool.SimplePolicy{Read: true, Write: true}, +}) + +for notice := range pool.Notices { + log.Printf("%s has sent a notice: '%s'\n", notice.Relay, notice.Message) +} +``` + +### Listening for events + +```go +kind1 := event.KindTextNote +sub := pool.Sub(filter.EventFilters{ + { + Authors: []string{"0ded86bf80c76847320b16f22b7451c08169434837a51ad5fe3b178af6c35f5d"}, + Kind: &kind1, // or 1 + }, +}) + +go func() { + for event := range sub.UniqueEvents { + log.Print(event) + } +}() + +time.Sleep(5 * time.Second) +sub.Unsub() +``` + +### Publishing an event + +```go +secretKey := "3f06a81e0a0c2ad34ee9df2a30d87a810da9e3c3881f780755ace5e5e64d30a7" + +pool.SecretKey = &secretKey + +event, statuses, _ := pool.PublishEvent(&event.Event{ + CreatedAt: uint32(time.Now().Unix()), + Kind: 1, // or event.KindTextNote + Tags: make(event.Tags, 0), + Content: "hello", +}) + +log.Print(event.PubKey) +log.Print(event.ID) +log.Print(event.Sig) + +for status := range statuses { + switch status.Status { + case relaypool.PublishStatusSent: + fmt.Printf("Sent event %s to '%s'.\n", event.ID, status.Relay) + case relaypool.PublishStatusFailed: + fmt.Printf("Failed to send event %s to '%s'.\n", event.ID, status.Relay) + case relaypool.PublishStatusSucceeded: + fmt.Printf("Event seen %s on '%s'.\n", event.ID, status.Relay) + } +} +``` diff --git a/event/event.go b/event/event.go index b0755bd..ade3c11 100644 --- a/event/event.go +++ b/event/event.go @@ -8,7 +8,6 @@ import ( "encoding/json" "errors" "fmt" - "math/big" "github.com/fiatjaf/bip340" ) @@ -95,12 +94,9 @@ func (evt *Event) Serialize() []byte { // returns an error if the signature itself is invalid. func (evt Event) CheckSignature() (bool, error) { // read and check pubkey - pubkeyb, err := hex.DecodeString(evt.PubKey) + pubkey, err := bip340.ParsePublicKey(evt.PubKey) if err != nil { - return false, err - } - if len(pubkeyb) != 32 { - return false, fmt.Errorf("pubkey must be 32 bytes, not %d", len(pubkeyb)) + return false, fmt.Errorf("Event has invalid pubkey '%s': %w", evt.PubKey, err) } // check tags @@ -116,32 +112,28 @@ func (evt Event) CheckSignature() (bool, error) { } } - sig, err := hex.DecodeString(evt.Sig) + s, err := hex.DecodeString(evt.Sig) if err != nil { return false, fmt.Errorf("signature is invalid hex: %w", err) } - if len(sig) != 64 { - return false, fmt.Errorf("signature must be 64 bytes, not %d", len(sig)) + if len(s) != 64 { + return false, fmt.Errorf("signature must be 64 bytes, not %d", len(s)) } - var p [32]byte - copy(p[:], pubkeyb) + var sig [64]byte + copy(sig[:], s) - var s [64]byte - copy(s[:], sig) - - h := sha256.Sum256(evt.Serialize()) - - return bip340.Verify(p, h, s) + hash := sha256.Sum256(evt.Serialize()) + return bip340.Verify(pubkey, hash, sig) } // Sign signs an event with a given privateKey func (evt *Event) Sign(privateKey string) error { h := sha256.Sum256(evt.Serialize()) - s, _ := new(big.Int).SetString(privateKey, 16) - if s == nil { - return errors.New("invalid private key " + privateKey) + s, err := bip340.ParsePrivateKey(privateKey) + if err != nil { + return fmt.Errorf("Sign called with invalid private key '%s': %w", privateKey, err) } aux := make([]byte, 32) diff --git a/filter/filter.go b/filter/filter.go index 9a000bc..6ed991d 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -2,6 +2,8 @@ package filter import "github.com/fiatjaf/go-nostr/event" +type EventFilters []EventFilter + type EventFilter struct { ID string `json:"id,omitempty"` Kind *int `json:"kind,omitempty"` @@ -11,6 +13,16 @@ type EventFilter struct { Since uint32 `json:"since,omitempty"` } +func (eff EventFilters) Match(event *event.Event) bool { + for _, filter := range eff { + if filter.Matches(event) { + return true + } + } + + return false +} + func (ef EventFilter) Matches(event *event.Event) bool { if event == nil { return false diff --git a/go.mod b/go.mod index d730a1e..11f2b6b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module github.com/fiatjaf/go-nostr go 1.15 require ( - github.com/fiatjaf/bip340 v1.0.0 + github.com/fiatjaf/bip340 v1.1.0 github.com/gorilla/websocket v1.4.2 ) diff --git a/go.sum b/go.sum index 5e75145..3ab8015 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46f github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/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/fiatjaf/bip340 v1.0.0 h1:mpwbm+0KC9BXB/7/pnac4e4N1TiuppyEVXxtVAXj75k= -github.com/fiatjaf/bip340 v1.0.0/go.mod h1:MxAz+5FQUTW4OT2gnCBC6Our486wmqf72ykZIrh7+is= +github.com/fiatjaf/bip340 v1.1.0 h1:W+CnUU3RyqgMKS2S9t/r2l3L4D+sSkRtU4la7MlVBR8= +github.com/fiatjaf/bip340 v1.1.0/go.mod h1:MxAz+5FQUTW4OT2gnCBC6Our486wmqf72ykZIrh7+is= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= diff --git a/relaypool/relaypool.go b/relaypool/relaypool.go index 712ffec..6d8be18 100644 --- a/relaypool/relaypool.go +++ b/relaypool/relaypool.go @@ -9,6 +9,7 @@ import ( "log" "time" + "github.com/fiatjaf/bip340" "github.com/fiatjaf/go-nostr/event" "github.com/fiatjaf/go-nostr/filter" nostrutils "github.com/fiatjaf/go-nostr/utils" @@ -121,11 +122,18 @@ func (r *RelayPool) Add(url string, policy *Policy) error { if subscription, ok := r.subscriptions[channel]; ok { var event event.Event json.Unmarshal(jsonMessage[2], &event) + + // check signature of all received events, ignore invalid ok, _ := event.CheckSignature() if !ok { continue } + // check if the event matches the desired filter, ignore otherwise + if !subscription.filters.Match(&event) { + continue + } + subscription.Events <- EventMessage{ Relay: nm, Event: event, @@ -153,7 +161,7 @@ func (r *RelayPool) Remove(url string) { delete(r.websockets, nm) } -func (r *RelayPool) Sub(filter filter.EventFilter) *Subscription { +func (r *RelayPool) Sub(filters filter.EventFilters) *Subscription { random := make([]byte, 7) rand.Read(random) @@ -170,17 +178,25 @@ func (r *RelayPool) Sub(filter filter.EventFilter) *Subscription { subscription.UniqueEvents = make(chan event.Event) r.subscriptions[subscription.channel] = &subscription - subscription.Sub(&filter) + subscription.Sub(filters) return &subscription } func (r *RelayPool) PublishEvent(evt *event.Event) (*event.Event, chan PublishStatus, error) { status := make(chan PublishStatus, 1) - if r.SecretKey == nil && evt.Sig == "" { + if r.SecretKey == nil && (evt.PubKey == "" || evt.Sig == "") { return nil, status, errors.New("PublishEvent needs either a signed event to publish or to have been configured with a .SecretKey.") } + if evt.PubKey == "" { + secretKeyN, err := bip340.ParsePrivateKey(*r.SecretKey) + if err != nil { + return nil, status, fmt.Errorf("The pool's global SecretKey is invalid: %w", err) + } + evt.PubKey = fmt.Sprintf("%x", bip340.GetPublicKey(secretKeyN)) + } + if evt.Sig == "" { err := evt.Sign(*r.SecretKey) if err != nil { @@ -197,7 +213,7 @@ func (r *RelayPool) PublishEvent(evt *event.Event) (*event.Event, chan PublishSt } status <- PublishStatus{relay, PublishStatusSent} - subscription := r.Sub(filter.EventFilter{ID: evt.ID}) + subscription := r.Sub(filter.EventFilters{{ID: evt.ID}}) for { select { case event := <-subscription.UniqueEvents: diff --git a/relaypool/subscription.go b/relaypool/subscription.go index 09ac46c..dc23eee 100644 --- a/relaypool/subscription.go +++ b/relaypool/subscription.go @@ -10,8 +10,8 @@ type Subscription struct { channel string relays map[string]*websocket.Conn - filter *filter.EventFilter - Events chan EventMessage + filters filter.EventFilters + Events chan EventMessage started bool UniqueEvents chan event.Event @@ -38,17 +38,17 @@ func (subscription Subscription) Unsub() { } } -func (subscription Subscription) Sub(filter *filter.EventFilter) { - if filter != nil { - subscription.filter = filter - } - +func (subscription Subscription) Sub(filters filter.EventFilters) { for _, ws := range subscription.relays { - ws.WriteJSON([]interface{}{ + message := []interface{}{ "REQ", subscription.channel, - subscription.filter, - }) + } + for _, filter := range subscription.filters { + message = append(message, filter) + } + + ws.WriteJSON(message) } if !subscription.started { @@ -79,9 +79,14 @@ func (subscription Subscription) removeRelay(relay string) { func (subscription Subscription) addRelay(relay string, ws *websocket.Conn) { subscription.relays[relay] = ws - ws.WriteJSON([]interface{}{ + + message := []interface{}{ "REQ", subscription.channel, - subscription.filter, - }) + } + for _, filter := range subscription.filters { + message = append(message, filter) + } + + ws.WriteJSON(message) }