diff --git a/README.md b/README.md index fa9b6d2..f57f73d 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ A set of useful things for [Nostr Protocol](https://github.com/fiatjaf/nostr) im ### Subscribing to a set of relays ```go -pool := relaypool.New() +pool := nostr.NewRelayPool() -pool.Add("wss://relay.nostr.com/", &relaypool.Policy{ - SimplePolicy: relaypool.SimplePolicy{Read: true, Write: true}, +pool.Add("wss://relay.nostr.com/", &nostr.RelayPoolPolicy{ + SimplePolicy: nostr.SimplePolicy{Read: true, Write: true}, }) -pool.Add("wss://nostrrelay.example.com/", &relaypool.Policy{ - SimplePolicy: relaypool.SimplePolicy{Read: true, Write: true}, +pool.Add("wss://nostrrelay.example.com/", &nostr.RelayPoolPolicy{ + SimplePolicy: nostr.SimplePolicy{Read: true, Write: true}, }) for notice := range pool.Notices { @@ -26,11 +26,10 @@ for notice := range pool.Notices { ### Listening for events ```go -kind1 := event.KindTextNote -sub := pool.Sub(filter.EventFilters{ +sub := pool.Sub(nostr.EventFilters{ { Authors: []string{"0ded86bf80c76847320b16f22b7451c08169434837a51ad5fe3b178af6c35f5d"}, - Kind: &kind1, // or 1 + Kinds: []string{nostr.KindTextNote}, // or {1} }, }) @@ -51,10 +50,10 @@ secretKey := "3f06a81e0a0c2ad34ee9df2a30d87a810da9e3c3881f780755ace5e5e64d30a7" pool.SecretKey = &secretKey -event, statuses, _ := pool.PublishEvent(&event.Event{ +event, statuses, _ := pool.PublishEvent(&nostr.Event{ CreatedAt: uint32(time.Now().Unix()), - Kind: 1, // or event.KindTextNote - Tags: make(event.Tags, 0), + Kind: nostr.KindTextNote, + Tags: make(nostr.Tags, 0), Content: "hello", }) @@ -64,11 +63,11 @@ log.Print(event.Sig) for status := range statuses { switch status.Status { - case relaypool.PublishStatusSent: + case nostr.PublishStatusSent: fmt.Printf("Sent event %s to '%s'.\n", event.ID, status.Relay) - case relaypool.PublishStatusFailed: + case nostr.PublishStatusFailed: fmt.Printf("Failed to send event %s to '%s'.\n", event.ID, status.Relay) - case relaypool.PublishStatusSucceeded: + case nostr.PublishStatusSucceeded: fmt.Printf("Event seen %s on '%s'.\n", event.ID, status.Relay) } } diff --git a/event/event.go b/event.go similarity index 88% rename from event/event.go rename to event.go index 8c46a05..fee07c0 100644 --- a/event/event.go +++ b/event.go @@ -1,4 +1,4 @@ -package event +package nostr import ( "bytes" @@ -6,7 +6,6 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" - "errors" "fmt" "github.com/fiatjaf/bip340" @@ -34,26 +33,6 @@ type Event struct { Sig string `json:"sig"` } -type Tags []Tag - -func (t *Tags) Scan(src interface{}) error { - var jtags []byte = make([]byte, 0) - - switch v := src.(type) { - case []byte: - jtags = v - case string: - jtags = []byte(v) - default: - return errors.New("couldn't scan tags, it's not a json string") - } - - json.Unmarshal(jtags, &t) - return nil -} - -type Tag []interface{} - // Serialize outputs a byte array that can be hashed/signed to identify/authenticate func (evt *Event) Serialize() []byte { // the serialization process is just putting everything into a JSON array diff --git a/filter.go b/filter.go new file mode 100644 index 0000000..8ef2da0 --- /dev/null +++ b/filter.go @@ -0,0 +1,90 @@ +package nostr + +type EventFilters []EventFilter + +type EventFilter struct { + IDs StringList `json:"ids,omitempty"` + Kinds IntList `json:"kinds,omitempty"` + Authors StringList `json:"authors,omitempty"` + Since uint32 `json:"since,omitempty"` + Until uint32 `json:"until,omitempty"` + TagE StringList `json:"#e,omitempty"` + TagP StringList `json:"#p,omitempty"` +} + +func (eff EventFilters) Match(event *Event) bool { + for _, filter := range eff { + if filter.Matches(event) { + return true + } + } + return false +} + +func (ef EventFilter) Matches(event *Event) bool { + if event == nil { + return false + } + + if ef.IDs != nil && !ef.IDs.Contains(event.ID) { + return false + } + + if ef.Kinds != nil && !ef.Kinds.Contains(event.Kind) { + return false + } + + if ef.Authors != nil && !ef.Authors.Contains(event.PubKey) { + return false + } + + if ef.TagE != nil && !event.Tags.ContainsAny("e", ef.TagE) { + return false + } + + if ef.TagP != nil && !event.Tags.ContainsAny("p", ef.TagP) { + return false + } + + if ef.Since != 0 && event.CreatedAt < ef.Since { + return false + } + + if ef.Until != 0 && event.CreatedAt >= ef.Until { + return false + } + + return true +} + +func Equal(a EventFilter, b EventFilter) bool { + if !a.Kinds.Equals(b.Kinds) { + return false + } + + if !a.IDs.Equals(b.IDs) { + return false + } + + if !a.Authors.Equals(b.Authors) { + return false + } + + if !a.TagE.Equals(b.TagE) { + return false + } + + if !a.TagP.Equals(b.TagP) { + return false + } + + if a.Since != b.Since { + return false + } + + if a.Until != b.Until { + return false + } + + return true +} diff --git a/filter/filter.go b/filter/filter.go deleted file mode 100644 index 6f38272..0000000 --- a/filter/filter.go +++ /dev/null @@ -1,92 +0,0 @@ -package filter - -import "github.com/fiatjaf/go-nostr/event" - -type EventFilters []EventFilter - -type EventFilter struct { - IDs []string `json:"ids,omitempty"` - Kinds []int `json:"kinds,omitempty"` - Authors []string `json:"authors,omitempty"` - Since uint32 `json:"since,omitempty"` - Until uint32 `json:"until,omitempty"` - TagE []string `json:"#e,omitempty"` - TagP []string `json:"#p,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 - } - - if ef.IDs != nil && !stringsContain(ef.IDs, event.ID) { - return false - } - - if ef.Kinds != nil && !intsContain(ef.Kinds, event.Kind) { - return false - } - - if ef.Authors != nil && !stringsContain(ef.Authors, event.PubKey) { - return false - } - - if ef.TagE != nil && !containsAnyTag("e", event.Tags, ef.TagE) { - return false - } - - if ef.TagP != nil && !containsAnyTag("p", event.Tags, ef.TagP) { - return false - } - - if ef.Since != 0 && event.CreatedAt < ef.Since { - return false - } - - if ef.Until != 0 && event.CreatedAt >= ef.Until { - return false - } - - return true -} - -func Equal(a EventFilter, b EventFilter) bool { - if !intsEqual(a.Kinds, b.Kinds) { - return false - } - - if !stringsEqual(a.IDs, b.IDs) { - return false - } - - if !stringsEqual(a.Authors, b.Authors) { - return false - } - - if !stringsEqual(a.TagE, b.TagE) { - return false - } - - if !stringsEqual(a.TagP, b.TagP) { - return false - } - - if a.Since != b.Since { - return false - } - - if a.Until != b.Until { - return false - } - - return true -} diff --git a/filter/utils.go b/filter_aux.go similarity index 50% rename from filter/utils.go rename to filter_aux.go index 9bb89a3..b18b824 100644 --- a/filter/utils.go +++ b/filter_aux.go @@ -1,8 +1,9 @@ -package filter +package nostr -import "github.com/fiatjaf/go-nostr/event" +type StringList []string +type IntList []int -func stringsEqual(as, bs []string) bool { +func (as StringList) Equals(bs StringList) bool { if len(as) != len(bs) { return false } @@ -23,7 +24,7 @@ func stringsEqual(as, bs []string) bool { return true } -func intsEqual(as, bs []int) bool { +func (as IntList) Equals(bs IntList) bool { if len(as) != len(bs) { return false } @@ -44,7 +45,7 @@ func intsEqual(as, bs []int) bool { return true } -func stringsContain(haystack []string, needle string) bool { +func (haystack StringList) Contains(needle string) bool { for _, hay := range haystack { if hay == needle { return true @@ -53,7 +54,7 @@ func stringsContain(haystack []string, needle string) bool { return false } -func intsContain(haystack []int, needle int) bool { +func (haystack IntList) Contains(needle int) bool { for _, hay := range haystack { if hay == needle { return true @@ -61,27 +62,3 @@ func intsContain(haystack []int, needle int) bool { } return false } - -func containsAnyTag(tagName string, tags event.Tags, values []string) bool { - for _, tag := range tags { - if len(tag) < 2 { - continue - } - - currentTagName, ok := tag[0].(string) - if !ok || currentTagName != tagName { - continue - } - - currentTagValue, ok := tag[1].(string) - if !ok { - continue - } - - if stringsContain(values, currentTagValue) { - return true - } - } - - return false -} diff --git a/utils/utils.go b/normalize.go similarity index 95% rename from utils/utils.go rename to normalize.go index 47f63d3..ba958a4 100644 --- a/utils/utils.go +++ b/normalize.go @@ -1,4 +1,4 @@ -package nostrutils +package nostr import ( "net/url" diff --git a/relaypool/relaypool.go b/relaypool.go similarity index 83% rename from relaypool/relaypool.go rename to relaypool.go index 6d8be18..540c34f 100644 --- a/relaypool/relaypool.go +++ b/relaypool.go @@ -1,4 +1,4 @@ -package relaypool +package nostr import ( "crypto/rand" @@ -10,23 +10,31 @@ import ( "time" "github.com/fiatjaf/bip340" - "github.com/fiatjaf/go-nostr/event" - "github.com/fiatjaf/go-nostr/filter" - nostrutils "github.com/fiatjaf/go-nostr/utils" "github.com/gorilla/websocket" ) +const ( + PublishStatusSent = 0 + PublishStatusFailed = -1 + PublishStatusSucceeded = 1 +) + +type PublishStatus struct { + Relay string + Status int +} + type RelayPool struct { SecretKey *string - Relays map[string]Policy + Relays map[string]RelayPoolPolicy websockets map[string]*websocket.Conn subscriptions map[string]*Subscription Notices chan *NoticeMessage } -type Policy struct { +type RelayPoolPolicy struct { SimplePolicy ReadSpecific map[string]SimplePolicy } @@ -42,9 +50,9 @@ type NoticeMessage struct { } // New creates a new RelayPool with no relays in it -func New() *RelayPool { +func NewRelayPool() *RelayPool { return &RelayPool{ - Relays: make(map[string]Policy), + Relays: make(map[string]RelayPoolPolicy), websockets: make(map[string]*websocket.Conn), subscriptions: make(map[string]*Subscription), @@ -54,17 +62,17 @@ func New() *RelayPool { // Add adds a new relay to the pool, if policy is nil, it will be a simple // read+write policy. -func (r *RelayPool) Add(url string, policy *Policy) error { +func (r *RelayPool) Add(url string, policy *RelayPoolPolicy) error { if policy == nil { - policy = &Policy{SimplePolicy: SimplePolicy{Read: true, Write: true}} + policy = &RelayPoolPolicy{SimplePolicy: SimplePolicy{Read: true, Write: true}} } - nm := nostrutils.NormalizeURL(url) + nm := NormalizeURL(url) if nm == "" { return fmt.Errorf("invalid relay URL '%s'", url) } - conn, _, err := websocket.DefaultDialer.Dial(nostrutils.NormalizeURL(url), nil) + conn, _, err := websocket.DefaultDialer.Dial(NormalizeURL(url), nil) if err != nil { return fmt.Errorf("error opening websocket to '%s': %w", nm, err) } @@ -120,7 +128,7 @@ func (r *RelayPool) Add(url string, policy *Policy) error { var channel string json.Unmarshal(jsonMessage[1], &channel) if subscription, ok := r.subscriptions[channel]; ok { - var event event.Event + var event Event json.Unmarshal(jsonMessage[2], &event) // check signature of all received events, ignore invalid @@ -148,7 +156,7 @@ func (r *RelayPool) Add(url string, policy *Policy) error { // Remove removes a relay from the pool. func (r *RelayPool) Remove(url string) { - nm := nostrutils.NormalizeURL(url) + nm := NormalizeURL(url) for _, sub := range r.subscriptions { sub.removeRelay(nm) @@ -161,7 +169,7 @@ func (r *RelayPool) Remove(url string) { delete(r.websockets, nm) } -func (r *RelayPool) Sub(filters filter.EventFilters) *Subscription { +func (r *RelayPool) Sub(filters EventFilters) *Subscription { random := make([]byte, 7) rand.Read(random) @@ -175,14 +183,14 @@ func (r *RelayPool) Sub(filters filter.EventFilters) *Subscription { } } subscription.Events = make(chan EventMessage) - subscription.UniqueEvents = make(chan event.Event) + subscription.UniqueEvents = make(chan Event) r.subscriptions[subscription.channel] = &subscription subscription.Sub(filters) return &subscription } -func (r *RelayPool) PublishEvent(evt *event.Event) (*event.Event, chan PublishStatus, error) { +func (r *RelayPool) PublishEvent(evt *Event) (*Event, chan PublishStatus, error) { status := make(chan PublishStatus, 1) if r.SecretKey == nil && (evt.PubKey == "" || evt.Sig == "") { @@ -213,7 +221,7 @@ func (r *RelayPool) PublishEvent(evt *event.Event) (*event.Event, chan PublishSt } status <- PublishStatus{relay, PublishStatusSent} - subscription := r.Sub(filter.EventFilters{{ID: evt.ID}}) + subscription := r.Sub(EventFilters{EventFilter{IDs: []string{evt.ID}}}) for { select { case event := <-subscription.UniqueEvents: diff --git a/relaypool/publishstatus.go b/relaypool/publishstatus.go deleted file mode 100644 index fe66436..0000000 --- a/relaypool/publishstatus.go +++ /dev/null @@ -1,12 +0,0 @@ -package relaypool - -const ( - PublishStatusSent = 0 - PublishStatusFailed = -1 - PublishStatusSucceeded = 1 -) - -type PublishStatus struct { - Relay string - Status int -} diff --git a/relaypool/subscription.go b/subscription.go similarity index 86% rename from relaypool/subscription.go rename to subscription.go index dc23eee..8548c9e 100644 --- a/relaypool/subscription.go +++ b/subscription.go @@ -1,8 +1,6 @@ -package relaypool +package nostr import ( - "github.com/fiatjaf/go-nostr/event" - "github.com/fiatjaf/go-nostr/filter" "github.com/gorilla/websocket" ) @@ -10,15 +8,15 @@ type Subscription struct { channel string relays map[string]*websocket.Conn - filters filter.EventFilters + filters EventFilters Events chan EventMessage started bool - UniqueEvents chan event.Event + UniqueEvents chan Event } type EventMessage struct { - Event event.Event + Event Event Relay string } @@ -38,7 +36,7 @@ func (subscription Subscription) Unsub() { } } -func (subscription Subscription) Sub(filters filter.EventFilters) { +func (subscription Subscription) Sub(filters EventFilters) { for _, ws := range subscription.relays { message := []interface{}{ "REQ", diff --git a/tags.go b/tags.go new file mode 100644 index 0000000..593a762 --- /dev/null +++ b/tags.go @@ -0,0 +1,49 @@ +package nostr + +import ( + "encoding/json" + "errors" +) + +type Tags []Tag +type Tag []interface{} + +func (t *Tags) Scan(src interface{}) error { + var jtags []byte = make([]byte, 0) + + switch v := src.(type) { + case []byte: + jtags = v + case string: + jtags = []byte(v) + default: + return errors.New("couldn't scan tags, it's not a json string") + } + + json.Unmarshal(jtags, &t) + return nil +} + +func (tags Tags) ContainsAny(tagName string, values StringList) bool { + for _, tag := range tags { + if len(tag) < 2 { + continue + } + + currentTagName, ok := tag[0].(string) + if !ok || currentTagName != tagName { + continue + } + + currentTagValue, ok := tag[1].(string) + if !ok { + continue + } + + if values.Contains(currentTagValue) { + return true + } + } + + return false +}