From b1e475c865beda238a78d96be5161ebe78cf847c Mon Sep 17 00:00:00 2001 From: K Date: Sun, 8 Sep 2024 22:11:49 +0330 Subject: [PATCH] feat(types): add basic Nostr types (#9) --- CONTRIBUTING.md | 15 +- go.mod | 15 ++ go.sum | 20 ++ types/envelope/envelope.go | 407 ++++++++++++++++++++++++++++++++ types/envelope/envelope_test.go | 26 ++ types/event.go | 119 ---------- types/event/event.go | 95 ++++++++ types/event/event_easyjson.go | 192 +++++++++++++++ types/event/event_test.go | 93 ++++++++ types/filter.go | 15 -- types/filter/filter.go | 41 ++++ types/filter/filter_easyjson.go | 293 +++++++++++++++++++++++ types/filter/filter_test.go | 76 ++++++ types/kind.go | 101 ++++++++ types/message.go | 6 - types/tag.go | 3 + types/utils.go | 4 +- 17 files changed, 1374 insertions(+), 147 deletions(-) create mode 100644 go.sum create mode 100644 types/envelope/envelope.go create mode 100644 types/envelope/envelope_test.go delete mode 100644 types/event.go create mode 100644 types/event/event.go create mode 100644 types/event/event_easyjson.go create mode 100644 types/event/event_test.go delete mode 100644 types/filter.go create mode 100644 types/filter/filter.go create mode 100644 types/filter/filter_easyjson.go create mode 100644 types/filter/filter_test.go create mode 100644 types/kind.go delete mode 100644 types/message.go create mode 100644 types/tag.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 98dd65b..d51bdaf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,14 @@ Please follow these guidelines when contributing to the project: Error and log messages should not start with a capital letter (unless it's a proper noun or acronym) and should not end with punctuation. +#### Examples + +- Correct ✅: "unable to connect to client" +- Incorrect ❌: "Unable to connect to client" +- Incorrect ❌: "unable to connect to client." + +### Testing + All changes on core must contain proper and well-defined unit-tests, also previous tests must be passed as well. This codebase used `testify` for unit tests, make sure you follow these guide for tests: @@ -33,12 +41,9 @@ This codebase used `testify` for unit tests, make sure you follow these guide fo - For checking err using `assert.ErrorIs` make sure you pass expected error as second argument. - For checking equality using `assert.Equal` make sure you pass expected value as the first argument. +### Benchmarking -#### Examples - -- Correct ✅: "unable to connect to client" -- Incorrect ❌: "Unable to connect to client" -- Incorrect ❌: "unable to connect to client." +Make sure you follow [this guide](https://100go.co/89-benchmarks) when you write or change benchmarks to reach an accurate result. ### Help Messages diff --git a/go.mod b/go.mod index 456ec8b..60876d6 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,18 @@ module github.com/dezh-tech/immortal go 1.22.5 + +require ( + github.com/mailru/easyjson v0.7.7 + github.com/stretchr/testify v1.9.0 + github.com/tidwall/gjson v1.17.3 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f540fdf --- /dev/null +++ b/go.sum @@ -0,0 +1,20 @@ +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/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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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/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.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= +github.com/tidwall/gjson v1.17.3/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 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/types/envelope/envelope.go b/types/envelope/envelope.go new file mode 100644 index 0000000..1380097 --- /dev/null +++ b/types/envelope/envelope.go @@ -0,0 +1,407 @@ +package envelope + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + + "github.com/dezh-tech/immortal/types/event" + "github.com/dezh-tech/immortal/types/filter" + "github.com/mailru/easyjson" + jwriter "github.com/mailru/easyjson/jwriter" + "github.com/tidwall/gjson" // TODO::: remove/replace me! +) + +func ParseMessage(message []byte) Envelope { + firstComma := bytes.Index(message, []byte{','}) + if firstComma == -1 { + return nil + } + label := message[0:firstComma] + + var v Envelope + switch { + case bytes.Contains(label, []byte("EVENT")): + v = &EventEnvelope{} + case bytes.Contains(label, []byte("REQ")): + v = &ReqEnvelope{} + case bytes.Contains(label, []byte("COUNT")): + v = &CountEnvelope{} + case bytes.Contains(label, []byte("NOTICE")): + x := NoticeEnvelope("") + v = &x + case bytes.Contains(label, []byte("EOSE")): + x := EOSEEnvelope("") + v = &x + case bytes.Contains(label, []byte("OK")): + v = &OKEnvelope{} + case bytes.Contains(label, []byte("AUTH")): + v = &AuthEnvelope{} + case bytes.Contains(label, []byte("CLOSED")): + v = &ClosedEnvelope{} + case bytes.Contains(label, []byte("CLOSE")): + x := CloseEnvelope("") + v = &x + default: + return nil + } + + if err := v.UnmarshalJSON(message); err != nil { + return nil + } + return v +} + +type Envelope interface { + Label() string + UnmarshalJSON([]byte) error + MarshalJSON() ([]byte, error) + String() string +} + +var ( + _ Envelope = (*EventEnvelope)(nil) + _ Envelope = (*ReqEnvelope)(nil) + _ Envelope = (*CountEnvelope)(nil) + _ Envelope = (*NoticeEnvelope)(nil) + _ Envelope = (*EOSEEnvelope)(nil) + _ Envelope = (*CloseEnvelope)(nil) + _ Envelope = (*OKEnvelope)(nil) + _ Envelope = (*AuthEnvelope)(nil) +) + +type EventEnvelope struct { + SubscriptionID *string + Event event.Event +} + +func (_ EventEnvelope) Label() string { return "EVENT" } + +func (c EventEnvelope) String() string { + v, _ := json.Marshal(c) + return string(v) +} + +func (v *EventEnvelope) UnmarshalJSON(data []byte) error { + r := gjson.ParseBytes(data) + arr := r.Array() + switch len(arr) { + case 2: + return easyjson.Unmarshal([]byte(arr[1].Raw), &v.Event) + case 3: + v.SubscriptionID = &arr[1].Str + return easyjson.Unmarshal([]byte(arr[2].Raw), &v.Event) + default: + return fmt.Errorf("failed to decode EVENT envelope") + } +} + +func (v EventEnvelope) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["EVENT",`) + if v.SubscriptionID != nil { + w.RawString(`"` + *v.SubscriptionID + `",`) + } + v.Event.MarshalEasyJSON(&w) + w.RawString(`]`) + return w.BuildBytes() +} + +type ReqEnvelope struct { + SubscriptionID string + Filters []filter.Filter +} + +func (_ ReqEnvelope) Label() string { return "REQ" } + +func (c ReqEnvelope) String() string { + v, _ := json.Marshal(c) + return string(v) +} + +func (v *ReqEnvelope) UnmarshalJSON(data []byte) error { + r := gjson.ParseBytes(data) + arr := r.Array() + if len(arr) < 3 { + return fmt.Errorf("failed to decode REQ envelope: missing filters") + } + v.SubscriptionID = arr[1].Str + v.Filters = make([]filter.Filter, len(arr)-2) + f := 0 + for i := 2; i < len(arr); i++ { + if err := easyjson.Unmarshal([]byte(arr[i].Raw), &v.Filters[f]); err != nil { + return fmt.Errorf("%w -- on filter %d", err, f) + } + f++ + } + + return nil +} + +func (v ReqEnvelope) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["REQ",`) + w.RawString(`"` + v.SubscriptionID + `"`) + for _, filter := range v.Filters { + w.RawString(`,`) + filter.MarshalEasyJSON(&w) + } + w.RawString(`]`) + return w.BuildBytes() +} + +type CountEnvelope struct { + SubscriptionID string + Filters []filter.Filter + Count *int64 +} + +func (_ CountEnvelope) Label() string { return "COUNT" } +func (c CountEnvelope) String() string { + v, _ := json.Marshal(c) + return string(v) +} + +func (v *CountEnvelope) UnmarshalJSON(data []byte) error { + r := gjson.ParseBytes(data) + arr := r.Array() + if len(arr) < 3 { + return fmt.Errorf("failed to decode COUNT envelope: missing filters") + } + v.SubscriptionID = arr[1].Str + + if len(arr) < 3 { + return fmt.Errorf("COUNT array must have at least 3 items") + } + + var countResult struct { + Count *int64 `json:"count"` + } + if err := json.Unmarshal([]byte(arr[2].Raw), &countResult); err == nil && countResult.Count != nil { + v.Count = countResult.Count + return nil + } + + v.Filters = make([]filter.Filter, len(arr)-2) + f := 0 + for i := 2; i < len(arr); i++ { + item := []byte(arr[i].Raw) + + if err := easyjson.Unmarshal(item, &v.Filters[f]); err != nil { + return fmt.Errorf("%w -- on filter %d", err, f) + } + + f++ + } + + return nil +} + +func (v CountEnvelope) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["COUNT",`) + w.RawString(`"` + v.SubscriptionID + `"`) + if v.Count != nil { + w.RawString(`,{"count":`) + w.RawString(strconv.FormatInt(*v.Count, 10)) + w.RawString(`}`) + } else { + for _, filter := range v.Filters { + w.RawString(`,`) + filter.MarshalEasyJSON(&w) + } + } + w.RawString(`]`) + return w.BuildBytes() +} + +type NoticeEnvelope string + +func (_ NoticeEnvelope) Label() string { return "NOTICE" } +func (n NoticeEnvelope) String() string { + v, _ := json.Marshal(n) + return string(v) +} + +func (v *NoticeEnvelope) UnmarshalJSON(data []byte) error { + r := gjson.ParseBytes(data) + arr := r.Array() + if len(arr) < 2 { + return fmt.Errorf("failed to decode NOTICE envelope") + } + *v = NoticeEnvelope(arr[1].Str) + return nil +} + +func (v NoticeEnvelope) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["NOTICE",`) + w.Raw(json.Marshal(string(v))) + w.RawString(`]`) + return w.BuildBytes() +} + +type EOSEEnvelope string + +func (_ EOSEEnvelope) Label() string { return "EOSE" } +func (e EOSEEnvelope) String() string { + v, _ := json.Marshal(e) + return string(v) +} + +func (v *EOSEEnvelope) UnmarshalJSON(data []byte) error { + r := gjson.ParseBytes(data) + arr := r.Array() + if len(arr) < 2 { + return fmt.Errorf("failed to decode EOSE envelope") + } + *v = EOSEEnvelope(arr[1].Str) + return nil +} + +func (v EOSEEnvelope) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["EOSE",`) + w.Raw(json.Marshal(string(v))) + w.RawString(`]`) + return w.BuildBytes() +} + +type CloseEnvelope string + +func (_ CloseEnvelope) Label() string { return "CLOSE" } +func (c CloseEnvelope) String() string { + v, _ := json.Marshal(c) + return string(v) +} + +func (v *CloseEnvelope) UnmarshalJSON(data []byte) error { + r := gjson.ParseBytes(data) + arr := r.Array() + switch len(arr) { + case 2: + *v = CloseEnvelope(arr[1].Str) + return nil + default: + return fmt.Errorf("failed to decode CLOSE envelope") + } +} + +func (v CloseEnvelope) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["CLOSE",`) + w.Raw(json.Marshal(string(v))) + w.RawString(`]`) + return w.BuildBytes() +} + +type ClosedEnvelope struct { + SubscriptionID string + Reason string +} + +func (_ ClosedEnvelope) Label() string { return "CLOSED" } +func (c ClosedEnvelope) String() string { + v, _ := json.Marshal(c) + return string(v) +} + +func (v *ClosedEnvelope) UnmarshalJSON(data []byte) error { + r := gjson.ParseBytes(data) + arr := r.Array() + switch len(arr) { + case 3: + *v = ClosedEnvelope{arr[1].Str, arr[2].Str} + return nil + default: + return fmt.Errorf("failed to decode CLOSED envelope") + } +} + +func (v ClosedEnvelope) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["CLOSED",`) + w.Raw(json.Marshal(string(v.SubscriptionID))) + w.RawString(`,`) + w.Raw(json.Marshal(v.Reason)) + w.RawString(`]`) + return w.BuildBytes() +} + +type OKEnvelope struct { + EventID string + OK bool + Reason string +} + +func (_ OKEnvelope) Label() string { return "OK" } +func (o OKEnvelope) String() string { + v, _ := json.Marshal(o) + return string(v) +} + +func (v *OKEnvelope) UnmarshalJSON(data []byte) error { + r := gjson.ParseBytes(data) + arr := r.Array() + if len(arr) < 4 { + return fmt.Errorf("failed to decode OK envelope: missing fields") + } + v.EventID = arr[1].Str + v.OK = arr[2].Raw == "true" + v.Reason = arr[3].Str + + return nil +} + +func (v OKEnvelope) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["OK",`) + w.RawString(`"` + v.EventID + `",`) + ok := "false" + if v.OK { + ok = "true" + } + w.RawString(ok) + w.RawString(`,`) + w.Raw(json.Marshal(v.Reason)) + w.RawString(`]`) + return w.BuildBytes() +} + +type AuthEnvelope struct { + Challenge *string + Event event.Event +} + +func (_ AuthEnvelope) Label() string { return "AUTH" } +func (a AuthEnvelope) String() string { + v, _ := json.Marshal(a) + return string(v) +} + +func (v *AuthEnvelope) UnmarshalJSON(data []byte) error { + r := gjson.ParseBytes(data) + arr := r.Array() + if len(arr) < 2 { + return fmt.Errorf("failed to decode Auth envelope: missing fields") + } + if arr[1].IsObject() { + return easyjson.Unmarshal([]byte(arr[1].Raw), &v.Event) + } else { + v.Challenge = &arr[1].Str + } + return nil +} + +func (v AuthEnvelope) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + w.RawString(`["AUTH",`) + if v.Challenge != nil { + w.Raw(json.Marshal(*v.Challenge)) + } else { + v.Event.MarshalEasyJSON(&w) + } + w.RawString(`]`) + return w.BuildBytes() +} diff --git a/types/envelope/envelope_test.go b/types/envelope/envelope_test.go new file mode 100644 index 0000000..dce3607 --- /dev/null +++ b/types/envelope/envelope_test.go @@ -0,0 +1,26 @@ +package envelope_test + +import ( + "testing" + + "github.com/dezh-tech/immortal/types/envelope" + "github.com/stretchr/testify/assert" +) + +// TODO::: write test for all cases. + +func TestEventEnvelopeEncodingAndDecoding(t *testing.T) { + eventEnvelopes := []string{ + `["EVENT","_",{"id":"dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1644271588,"kind":1,"tags":[],"content":"now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?","sig":"230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524"}]`, + } + + for _, raw := range eventEnvelopes { + var env envelope.EventEnvelope + err := env.UnmarshalJSON([]byte(raw)) + assert.NoError(t, err, "failed to parse event envelope json: %v", err) + + res, err := env.MarshalJSON() + assert.NoError(t, err, "failed to re marshal event as json: %v", err) + assert.Equal(t, raw, string(res)) + } +} diff --git a/types/event.go b/types/event.go deleted file mode 100644 index 044ad5a..0000000 --- a/types/event.go +++ /dev/null @@ -1,119 +0,0 @@ -package types - -// Range is the events kind ranges. -type Range uint8 - -type Tag [2]string - -const ( - Regular Range = iota - Replaceable - Ephemeral - ParameterizedReplaceable -) - -// Event reperesents an event structure defined on NIP-01. -type Event struct { - ID string `json:"id"` - PublicKey string `json:"pubkey"` - CreatedAt int64 `json:"created_at"` - Kind uint16 `json:"kind"` - Tags []Tag `json:"tags"` - Content string `json:"content"` - Signature string `json:"sig"` -} - -// IsRegular checks if the gived event kind is in Regular range. -func (e *Event) IsRegular() bool { - return 1000 <= e.Kind || e.Kind < 10000 || 4 <= e.Kind || e.Kind < 45 || e.Kind == 1 || e.Kind == 2 -} - -// IsReplaceable checks if the gived event kind is in Replaceable range. -func (e *Event) IsReplaceable() bool { - return 10000 <= e.Kind || e.Kind < 20000 || e.Kind == 0 || e.Kind == 3 -} - -// IsEphemeral checks if the gived event kind is in Ephemeral range. -func (e *Event) IsEphemeral() bool { - return 20000 <= e.Kind || e.Kind < 30000 -} - -// IsParameterizedReplaceable checks if the gived event kind is in ParameterizedReplaceable range. -func (e *Event) IsParameterizedReplaceable() bool { - return 30000 <= e.Kind || e.Kind < 40000 -} - -// Range returns the events kind range based on NIP-01. -func (e *Event) Range() Range { - if e.IsRegular() { - return Regular - } else if e.IsReplaceable() { - return Replaceable - } else if e.IsParameterizedReplaceable() { - return ParameterizedReplaceable - } - - return Ephemeral -} - -// Match checks if the event is match with given filter. -// Note: this method intended to be used for already open subscriptions and recently received events. -// For new subscriptions and queries for stored data use the database query and don't use this to verify the result. -func (e *Event) Match(f Filter) bool { - if e == nil { - return false - } - - if f.IDs != nil && !ContainsString(e.ID, f.IDs) { - return false - } - - if f.Authors != nil && !ContainsString(e.PublicKey, f.Authors) { - return false - } - - if f.Kinds != nil && !ContainsUint16(e.Kind, f.Kinds) { - return false - } - - if e.CreatedAt >= f.Since || e.CreatedAt <= f.Until { - return false - } - - for f, vals := range f.Tags { - for _, t := range e.Tags { - if f != "#"+t[0] { // should we change it(+)? - return false - } - - var containsValue bool - for _, v := range vals { - if v == t[1] { - containsValue = true - break - } - } - - if !containsValue { - return false - } - } - } - - return true -} - -// Decode decodes a byte array into event structure. -func Decode(b []byte) (*Event, error) { - return nil, nil // TODO::: -} - -// Encode encodes an event to a byte array. -func (e *Event) Encode() ([]byte, error) { - return nil, nil // TODO::: -} - -// IsValid function validats an event Signature and ID. -func (e *Event) IsValid() bool { - return false // TODO::: -} diff --git a/types/event/event.go b/types/event/event.go new file mode 100644 index 0000000..6778ddf --- /dev/null +++ b/types/event/event.go @@ -0,0 +1,95 @@ +package event + +import ( + "github.com/dezh-tech/immortal/types" + "github.com/dezh-tech/immortal/types/filter" + "github.com/mailru/easyjson" +) + +// Event represents an event structure defined on NIP-01. +type Event struct { + ID string `json:"id"` + PublicKey string `json:"pubkey"` + CreatedAt int64 `json:"created_at"` + Kind types.Kind `json:"kind"` + Tags []types.Tag `json:"tags"` + Content string `json:"content"` + Signature string `json:"sig"` +} + +// Match checks if the event is match with given filter. +// Note: this method intended to be used for already open subscriptions and recently received events. +// For new subscriptions and queries for stored data use the database query and don't use this to verify the result. +func (e *Event) Match(f filter.Filter) bool { + if e == nil { + return false + } + + if f.IDs != nil && !types.ContainsString(e.ID, f.IDs) { + return false + } + + if f.Authors != nil && !types.ContainsString(e.PublicKey, f.Authors) { + return false + } + + if f.Kinds != nil && !types.ContainsKind(e.Kind, f.Kinds) { + return false + } + + if e.CreatedAt >= f.Since || e.CreatedAt <= f.Until { + return false + } + + for f, vals := range f.Tags { + for _, t := range e.Tags { + if len(t) < 2 { + continue + } + + if f != "#"+t[0] { // TODO:: should we replace + with strings.Builder? + return false + } + + var containsValue bool + for _, v := range vals { + if v == t[1] { + containsValue = true + break + } + } + + if !containsValue { + return false + } + } + } + + return true +} + +// Decode decodes a byte array into Event structure. +func Decode(b []byte) (*Event, error) { + e := new(Event) + + if err := easyjson.Unmarshal(b, e); err != nil { + return nil, err + } + + return e, nil +} + +// Encode encodes an Event to a byte array. +func (e *Event) Encode() ([]byte, error) { + b, err := easyjson.Marshal(e) + if err != nil { + return nil, err + } + + return b, nil +} + +// IsValid function validats an event Signature and ID. +func (e *Event) IsValid() bool { + return false // TODO::: +} diff --git a/types/event/event_easyjson.go b/types/event/event_easyjson.go new file mode 100644 index 0000000..57d81bb --- /dev/null +++ b/types/event/event_easyjson.go @@ -0,0 +1,192 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package event + +import ( + json "encoding/json" + types "github.com/dezh-tech/immortal/types" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjsonF642ad3eDecodeGithubComDezhTechImmortalTypesEvent(in *jlexer.Lexer, out *Event) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.ID = string(in.String()) + case "pubkey": + out.PublicKey = string(in.String()) + case "created_at": + out.CreatedAt = int64(in.Int64()) + case "kind": + out.Kind = types.Kind(in.Uint16()) + case "tags": + if in.IsNull() { + in.Skip() + out.Tags = nil + } else { + in.Delim('[') + if out.Tags == nil { + if !in.IsDelim(']') { + out.Tags = make([]types.Tag, 0, 2) + } else { + out.Tags = []types.Tag{} + } + } else { + out.Tags = (out.Tags)[:0] + } + for !in.IsDelim(']') { + var v1 types.Tag + if in.IsNull() { + in.Skip() + v1 = nil + } else { + in.Delim('[') + if v1 == nil { + if !in.IsDelim(']') { + v1 = make(types.Tag, 0, 4) + } else { + v1 = types.Tag{} + } + } else { + v1 = (v1)[:0] + } + for !in.IsDelim(']') { + var v2 string + v2 = string(in.String()) + v1 = append(v1, v2) + in.WantComma() + } + in.Delim(']') + } + out.Tags = append(out.Tags, v1) + in.WantComma() + } + in.Delim(']') + } + case "content": + out.Content = string(in.String()) + case "sig": + out.Signature = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonF642ad3eEncodeGithubComDezhTechImmortalTypesEvent(out *jwriter.Writer, in Event) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.String(string(in.ID)) + } + { + const prefix string = ",\"pubkey\":" + out.RawString(prefix) + out.String(string(in.PublicKey)) + } + { + const prefix string = ",\"created_at\":" + out.RawString(prefix) + out.Int64(int64(in.CreatedAt)) + } + { + const prefix string = ",\"kind\":" + out.RawString(prefix) + out.Uint16(uint16(in.Kind)) + } + { + const prefix string = ",\"tags\":" + out.RawString(prefix) + if in.Tags == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v3, v4 := range in.Tags { + if v3 > 0 { + out.RawByte(',') + } + if v4 == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v5, v6 := range v4 { + if v5 > 0 { + out.RawByte(',') + } + out.String(string(v6)) + } + out.RawByte(']') + } + } + out.RawByte(']') + } + } + { + const prefix string = ",\"content\":" + out.RawString(prefix) + out.String(string(in.Content)) + } + { + const prefix string = ",\"sig\":" + out.RawString(prefix) + out.String(string(in.Signature)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Event) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonF642ad3eEncodeGithubComDezhTechImmortalTypesEvent(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Event) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonF642ad3eEncodeGithubComDezhTechImmortalTypesEvent(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Event) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonF642ad3eDecodeGithubComDezhTechImmortalTypesEvent(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Event) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonF642ad3eDecodeGithubComDezhTechImmortalTypesEvent(l, v) +} diff --git a/types/event/event_test.go b/types/event/event_test.go new file mode 100644 index 0000000..4963d33 --- /dev/null +++ b/types/event/event_test.go @@ -0,0 +1,93 @@ +package event_test + +import ( + "testing" + + "github.com/dezh-tech/immortal/types/event" + "github.com/dezh-tech/immortal/types/filter" + "github.com/stretchr/testify/assert" +) + +// TODO::: Use table test. +// TODO::: Add error cases. + +var ( + validRawEvents = []string{ + `{"kind":1,"id":"dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1644271588,"tags":[],"content":"now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?","sig":"230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524"}`, + `{"kind":1,"id":"dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1644271588,"tags":[],"content":"now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?","sig":"230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524","extrakey":55}`, + `{"kind":1,"id":"dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1644271588,"tags":[],"content":"now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?","sig":"230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524","extrakey":"aaa"}`, + `{"kind":3,"id":"9e662bdd7d8abc40b5b15ee1ff5e9320efc87e9274d8d440c58e6eed2dddfbe2","pubkey":"373ebe3d45ec91977296a178d9f19f326c70631d2a1b0bbba5c5ecc2eb53b9e7","created_at":1644844224,"tags":[["p","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"],["p","75fc5ac2487363293bd27fb0d14fb966477d0f1dbc6361d37806a6a740eda91e"],["p","46d0dfd3a724a302ca9175163bdf788f3606b3fd1bb12d5fe055d1e418cb60ea"]],"content":"{\"wss://nostr-pub.wellorder.net\":{\"read\":true,\"write\":true},\"wss://nostr.bitcoiner.social\":{\"read\":false,\"write\":true},\"wss://expensive-relay.fiatjaf.com\":{\"read\":true,\"write\":true},\"wss://relayer.fiatjaf.com\":{\"read\":true,\"write\":true},\"wss://relay.bitid.nz\":{\"read\":true,\"write\":true},\"wss://nostr.rocks\":{\"read\":true,\"write\":true}}","sig":"811355d3484d375df47581cb5d66bed05002c2978894098304f20b595e571b7e01b2efd906c5650080ffe49cf1c62b36715698e9d88b9e8be43029a2f3fa66be"}`, + `{"id":"6ea18dd9156305d7716348a459683642b0a35693c301d99665dc0bd4c58872a2","pubkey":"bd4ae3e67e29964d494172261dc45395c89f6bd2e774642e366127171dfb81f5","content":"OK, this is a test case for Immortal.","kind":1,"created_at":1725802966,"tags":[],"sig":"4ec9407243f41ca0b1b44e3b61ca2e43d8a20ed088357b075ee123a720ecd9b1734526d79f3c97cd77828fe1176e37104cce1270eb739499fcb202fced766e72","relays":[]}`, + `{"id":"6ea18dd9156305d7716348a459683642b0a35693c301d99665dc0bd4c58872a2","pubkey":"bd4ae3e67e29964d494172261dc45395c89f6bd2e774642e366127171dfb81f5","content":"OK, this is a test case for Immortal.","kind":1,"created_at":1725802966,"tags":[],"sig":"4ec9407243f41ca0b1b44e3b61ca2e43d8a20ed088357b075ee123a720ecd9b1734526d79f3c97cd77828fe1176e37104cce1270eb739499fcb202fced766e72","relays":[]}`, + `{"content":"GOOD MORNING.\n\nLIVE FREE.\n\nhttps://cdn.satellite.earth/fbd7f2d73469c95ca7ef0f6f66cf9456c4dcce5cb46d69dcc1d3243fe817faf3.mp4","created_at":1725802688,"id":"b8b2d7f724e3e774226ba84a621155a3656b58baf08c12c56f5452fe71b4fec9","kind":1,"pubkey":"04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9","sig":"c26537743dcd6a9adbec078d1394953ce55ca8c5af1b1d8a38ce716f946c5c6b59a3f9129b4df41ee537fcd332dbfddecd1126e30f65e1feff6dd426032a3e60","tags":[]}`, + `{"content":"{\"id\":\"1807a9e2d51e8e04fa6257fb1c5746df57c83ac6127b4c6462f9d986d5c98736\",\"kind\":1,\"pubkey\":\"c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11\",\"content\":\" https:\\/\\/i.nostr.build\\/llzsmQs5gOtF1r8d.jpg \",\"tags\":[[\"imeta\",\"url https:\\/\\/i.nostr.build\\/llzsmQs5gOtF1r8d.jpg\",\"blurhash enQlLDaJysoy-;nMWBozs.kWKQtRs8ayenx^oyW;V[enNxV@i^WBfl\",\"dim 1259x1259\"],[\"r\",\"https:\\/\\/i.nostr.build\\/llzsmQs5gOtF1r8d.jpg\"]],\"sig\":\"5612c5a2ee8224e6d4b386698b1a9ae137cb9fa1c91ba7f15c4a14ac7896550f82caeffab7376e48e6fb81acf17597959c7658cabcf30c7484aa84cd740f2fee\",\"created_at\":1725639394}","created_at":1725802348,"id":"d2c8db3990efc74a56c4e9602bdcd5763e586b24b0708c14a171923f5e36e184","kind":6,"pubkey":"c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11","sig":"5d3fd5cff3628905178c829416f793304688596e0304b95dd94976f55885adf2afe13d6120d932867c40e240429cdd2c4c1930b41e2beea852ce4626f51fc35a","tags":[["e","1807a9e2d51e8e04fa6257fb1c5746df57c83ac6127b4c6462f9d986d5c98736","","root"],["p","c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11"]]}`, + `{"content":"Just add ReplyGuy now in Amethyst. \nnostr:nevent1qqsyjv4z7ns6frwnfrcn0lqk227chnrcnat476yaaadg8ev8jgn6p4gpz4mhxue69uhhyetvv9ujuerpd46hxtnfduhsygqpmhhz3xc696ggwn9rg2985s28vjnv45dtl25ctsspu74d59kn3spsgqqqqqqsa49ewc","created_at":1725799931,"id":"66e9072f5b2e7c4c6c6d8b9de2a81f78983549d2f56b7dc11737bba3a6a71408","kind":1,"pubkey":"01ddee289b1a2e90874ca3428a7a414764a6cad1abfaa985c201e7aada16d38c","sig":"2b17c30db9f7e246281d1dabcea12b40d19b6a6358d040a9b18a7b0fda72cca67adb260a7fe9b125a1ade5672cd922aa5a8d8d32fa4f18dc0efc80ae3f2deebe","tags":[["e","4932a2f4e1a48dd348f137fc1652bd8bcc789f575f689def5a83e5879227a0d5","","mention"],["p","01ddee289b1a2e90874ca3428a7a414764a6cad1abfaa985c201e7aada16d38c","","mention"],["q","4932a2f4e1a48dd348f137fc1652bd8bcc789f575f689def5a83e5879227a0d5"]]}`, + `{"content":"Kid: I want to be your age\nMe: no you don’t","created_at":1725799866,"id":"d4516dd8eda1c6235f6ca919b3ad6bdc7fe0a97d9bc7388b0444d9362f166522","kind":1,"pubkey":"1bc70a0148b3f316da33fe3c89f23e3e71ac4ff998027ec712b905cd24f6a411","sig":"17ec79e0e82e4ac3b3a32cfeac745774da90983d293435df9fa3225f31db87d4c855b2aee9d1c51a9bd64308262c1c7bf4274af20798b14b085d4dfc8f21ef9c","tags":[]}`, + `{"content":"Wen nostr hobby apps?","created_at":1725798384,"id":"43795c6b71168e6973223751d92f0904785a272eb40ae505243ae211cebddfa1","kind":1,"pubkey":"1bc70a0148b3f316da33fe3c89f23e3e71ac4ff998027ec712b905cd24f6a411","sig":"6b5309d3580b1f128279e66ea822cb153cfb7da14b1d130e86cb4855fb31b48d3f9ae6f5bd57732f96cf5914542c5f90d5fddb67e509fbd0efe534b41e568fdc","tags":[]}`, + `{"content":"GM from cicada\n\nhttps://video.nostr.build/66373d8edea1fadaacd10f4c5590729fb6e80f3f2279254d4c6b16cdbd80797f.mp4","created_at":1725798022,"id":"75b0fbee92009f70f57770aaf6ca993619af032b38d4b9abfa9bfe8d79c0f933","kind":1,"pubkey":"0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd","sig":"cf8bdc8e24e46c41aede59ca5a8766ead2c347b8a064b26e2ec30895a5f38ed6194c2bb8be11f6d17d5757de165364075c166ea1203777d299329f91ab245728","tags":[["imeta","url https://video.nostr.build/66373d8edea1fadaacd10f4c5590729fb6e80f3f2279254d4c6b16cdbd80797f.mp4","m video/mp4","x 6c0fe5d3347139a488f5f7ffcf179b5a61b28c65057eac951241d46c299db34d","ox 66373d8edea1fadaacd10f4c5590729fb6e80f3f2279254d4c6b16cdbd80797f","size 664508"]]}`, + } + + DecodedEvent *event.Event + EncodedEvent []byte +) + +func TestDecode(t *testing.T) { + for _, e := range validRawEvents { + _, err := event.Decode([]byte(e)) + assert.NoError(t, err, "valid event must be decoded with no erros JSON") + } +} + +func BenchmarkDecode(b *testing.B) { + var decodedEvent *event.Event + for i := 0; i < b.N; i++ { + for _, e := range validRawEvents { + decodedEvent, _ = event.Decode([]byte(e)) + } + } + DecodedEvent = decodedEvent +} + +func TestEncode(t *testing.T) { + events := make([]*event.Event, len(validRawEvents)) + for _, e := range validRawEvents { + decodedEvent, err := event.Decode([]byte(e)) + assert.NoError(t, err) + + events = append(events, decodedEvent) + } + + for _, e := range events { + _, err := e.Encode() + assert.NoError(t, err) + } +} + +func BenchmarkEncode(b *testing.B) { + events := make([]*event.Event, len(validRawEvents)) + for _, e := range validRawEvents { + decodedEvent, _ := event.Decode([]byte(e)) + events = append(events, decodedEvent) + } + + b.ResetTimer() + + var encodedEvent []byte + for i := 0; i < b.N; i++ { + for _, e := range events { + encodedEvent, _ = e.Encode() + } + } + EncodedEvent = encodedEvent +} + +// TODO::: add more test cases + benchmark. +func TestMatch(t *testing.T) { + f, err := filter.Decode([]byte(`{"kinds":[1, 2, 4],"authors":["3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","1d80e5588de010d137a67c42b03717595f5f510e73e42cfc48f31bae91844d59","ed4ca520e9929dfe9efdadf4011b53d30afd0678a09aa026927e60e7a45d9244"],"since":1677033299}`)) + assert.NoError(t, err) + + e, err := event.Decode([]byte(`{"kind":1,"id":"dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1644271588,"tags":[],"content":"now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?","sig":"230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524"}`)) + assert.NoError(t, err) + + assert.True(t, e.Match(*f)) +} diff --git a/types/filter.go b/types/filter.go deleted file mode 100644 index 88b64a7..0000000 --- a/types/filter.go +++ /dev/null @@ -1,15 +0,0 @@ -package types - -// Filter defined the filter structure based on NIP-01 and NIP-50. -type Filter struct { - IDs []string `json:"ids"` - Authors []string `json:"authors"` - Kinds []uint16 `json:"kinds"` - Tags map[string]Tag `json:"tags"` - Since int64 `json:"since"` - Until int64 `json:"until"` - Limit int16 `json:"limit"` - - // Sould we proxy Searchs to index server and elastic search? - Search string `json:"search"` // Check NIP-50 -} diff --git a/types/filter/filter.go b/types/filter/filter.go new file mode 100644 index 0000000..8a34dd0 --- /dev/null +++ b/types/filter/filter.go @@ -0,0 +1,41 @@ +package filter + +import ( + "github.com/dezh-tech/immortal/types" + "github.com/mailru/easyjson" +) + +// Filter defined the filter structure based on NIP-01 and NIP-50. +type Filter struct { + IDs []string `json:"ids"` + Authors []string `json:"authors"` + Kinds []types.Kind `json:"kinds"` + Tags map[string]types.Tag `json:"tags"` + Since int64 `json:"since"` + Until int64 `json:"until"` + Limit uint16 `json:"limit"` + + // Sould we proxy Searchs to index server and elastic search? + Search string `json:"search"` // Check NIP-50 +} + +// Decode decodes a byte array into event structure. +func Decode(b []byte) (*Filter, error) { + e := new(Filter) + + if err := easyjson.Unmarshal(b, e); err != nil { + return nil, err + } + + return e, nil +} + +// Encode encodes an event to a byte array. +func (e *Filter) Encode() ([]byte, error) { + ee, err := easyjson.Marshal(e) + if err != nil { + return nil, err + } + + return ee, nil +} diff --git a/types/filter/filter_easyjson.go b/types/filter/filter_easyjson.go new file mode 100644 index 0000000..8478254 --- /dev/null +++ b/types/filter/filter_easyjson.go @@ -0,0 +1,293 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package filter + +import ( + json "encoding/json" + types "github.com/dezh-tech/immortal/types" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson4d398eaaDecodeGithubComDezhTechImmortalTypesFilter(in *jlexer.Lexer, out *Filter) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "ids": + if in.IsNull() { + in.Skip() + out.IDs = nil + } else { + in.Delim('[') + if out.IDs == nil { + if !in.IsDelim(']') { + out.IDs = make([]string, 0, 4) + } else { + out.IDs = []string{} + } + } else { + out.IDs = (out.IDs)[:0] + } + for !in.IsDelim(']') { + var v1 string + v1 = string(in.String()) + out.IDs = append(out.IDs, v1) + in.WantComma() + } + in.Delim(']') + } + case "authors": + if in.IsNull() { + in.Skip() + out.Authors = nil + } else { + in.Delim('[') + if out.Authors == nil { + if !in.IsDelim(']') { + out.Authors = make([]string, 0, 4) + } else { + out.Authors = []string{} + } + } else { + out.Authors = (out.Authors)[:0] + } + for !in.IsDelim(']') { + var v2 string + v2 = string(in.String()) + out.Authors = append(out.Authors, v2) + in.WantComma() + } + in.Delim(']') + } + case "kinds": + if in.IsNull() { + in.Skip() + out.Kinds = nil + } else { + in.Delim('[') + if out.Kinds == nil { + if !in.IsDelim(']') { + out.Kinds = make([]types.Kind, 0, 32) + } else { + out.Kinds = []types.Kind{} + } + } else { + out.Kinds = (out.Kinds)[:0] + } + for !in.IsDelim(']') { + var v3 types.Kind + v3 = types.Kind(in.Uint16()) + out.Kinds = append(out.Kinds, v3) + in.WantComma() + } + in.Delim(']') + } + case "tags": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + out.Tags = make(map[string]types.Tag) + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v4 types.Tag + if in.IsNull() { + in.Skip() + v4 = nil + } else { + in.Delim('[') + if v4 == nil { + if !in.IsDelim(']') { + v4 = make(types.Tag, 0, 4) + } else { + v4 = types.Tag{} + } + } else { + v4 = (v4)[:0] + } + for !in.IsDelim(']') { + var v5 string + v5 = string(in.String()) + v4 = append(v4, v5) + in.WantComma() + } + in.Delim(']') + } + (out.Tags)[key] = v4 + in.WantComma() + } + in.Delim('}') + } + case "since": + out.Since = int64(in.Int64()) + case "until": + out.Until = int64(in.Int64()) + case "limit": + out.Limit = uint16(in.Uint16()) + case "search": + out.Search = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4d398eaaEncodeGithubComDezhTechImmortalTypesFilter(out *jwriter.Writer, in Filter) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"ids\":" + out.RawString(prefix[1:]) + if in.IDs == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v6, v7 := range in.IDs { + if v6 > 0 { + out.RawByte(',') + } + out.String(string(v7)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"authors\":" + out.RawString(prefix) + if in.Authors == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v8, v9 := range in.Authors { + if v8 > 0 { + out.RawByte(',') + } + out.String(string(v9)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"kinds\":" + out.RawString(prefix) + if in.Kinds == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v10, v11 := range in.Kinds { + if v10 > 0 { + out.RawByte(',') + } + out.Uint16(uint16(v11)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"tags\":" + out.RawString(prefix) + if in.Tags == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { + out.RawString(`null`) + } else { + out.RawByte('{') + v12First := true + for v12Name, v12Value := range in.Tags { + if v12First { + v12First = false + } else { + out.RawByte(',') + } + out.String(string(v12Name)) + out.RawByte(':') + if v12Value == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v13, v14 := range v12Value { + if v13 > 0 { + out.RawByte(',') + } + out.String(string(v14)) + } + out.RawByte(']') + } + } + out.RawByte('}') + } + } + { + const prefix string = ",\"since\":" + out.RawString(prefix) + out.Int64(int64(in.Since)) + } + { + const prefix string = ",\"until\":" + out.RawString(prefix) + out.Int64(int64(in.Until)) + } + { + const prefix string = ",\"limit\":" + out.RawString(prefix) + out.Uint16(uint16(in.Limit)) + } + { + const prefix string = ",\"search\":" + out.RawString(prefix) + out.String(string(in.Search)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Filter) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4d398eaaEncodeGithubComDezhTechImmortalTypesFilter(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Filter) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4d398eaaEncodeGithubComDezhTechImmortalTypesFilter(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Filter) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4d398eaaDecodeGithubComDezhTechImmortalTypesFilter(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Filter) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4d398eaaDecodeGithubComDezhTechImmortalTypesFilter(l, v) +} diff --git a/types/filter/filter_test.go b/types/filter/filter_test.go new file mode 100644 index 0000000..7c4420b --- /dev/null +++ b/types/filter/filter_test.go @@ -0,0 +1,76 @@ +package filter_test + +import ( + "testing" + + "github.com/dezh-tech/immortal/types/filter" + "github.com/stretchr/testify/assert" +) + +// TODO::: Use table test. +// TODO::: Add error cases. + +var ( + rawValidFilters = []string{ + `{"ids": ["abc"],"#e":["zzz"],"#something":["nothing","bab"],"since":1644254609,"search":"test"}`, + `{"ids": ["abc"],"#e":["zzz"],"limit":0,"#something":["nothing","bab"],"since":1644254609,"search":"test"}`, + `{"kinds":[1],"authors":["a8171781fd9e90ede3ea44ddca5d3abf828fe8eedeb0f3abb0dd3e563562e1fc","1d80e5588de010d137a67c42b03717595f5f510e73e42cfc48f31bae91844d59","ed4ca520e9929dfe9efdadf4011b53d30afd0678a09aa026927e60e7a45d9244"],"since":1677033299}`, + `{"kinds":[1,2,4],"until":12345678,"limit":0,"#fruit":["banana","mango"]}`, + `{"kinds":[1,2,4],"until":12345678,"#fruit":["banana","mango"]}`, + } + + EncodedFilter []byte + DecodedFilter *filter.Filter +) + +func TestDencode(t *testing.T) { + for _, f := range rawValidFilters { + _, err := filter.Decode([]byte(f)) + assert.NoError(t, err) + } +} + +func BenchmarkDecode(b *testing.B) { + var decodedFilter *filter.Filter + for i := 0; i < b.N; i++ { + for _, f := range rawValidFilters { + decodedFilter, _ = filter.Decode([]byte(f)) + } + } + DecodedFilter = decodedFilter +} + +func TestEncode(t *testing.T) { + filters := make([]*filter.Filter, len(rawValidFilters)) + + for _, f := range rawValidFilters { + decodedFilter, err := filter.Decode([]byte(f)) + assert.NoError(t, err) + + filters = append(filters, decodedFilter) + } + + for _, f := range filters { + _, err := f.Encode() + assert.NoError(t, err) + } +} + +func BenchmarkEncode(b *testing.B) { + filters := make([]*filter.Filter, len(rawValidFilters)) + + for _, f := range rawValidFilters { + decodedFilter, _ := filter.Decode([]byte(f)) + filters = append(filters, decodedFilter) + } + + b.ResetTimer() + + var encodedFilter []byte + for i := 0; i < b.N; i++ { + for _, f := range filters { + encodedFilter, _ = f.Encode() + } + } + EncodedFilter = encodedFilter +} diff --git a/types/kind.go b/types/kind.go new file mode 100644 index 0000000..27972cb --- /dev/null +++ b/types/kind.go @@ -0,0 +1,101 @@ +package types + +type ( + Kind uint16 + Range uint8 +) + +const ( + // Ranges + Regular Range = iota + Replaceable + Ephemeral + ParameterizedReplaceable + + // Kinds + KindProfileMetadata Kind = 0 + KindTextNote Kind = 1 + KindRecommendServer Kind = 2 + KindContactList Kind = 3 + KindEncryptedDirectMessage Kind = 4 + KindDeletion Kind = 5 + KindRepost Kind = 6 + KindReaction Kind = 7 + KindSimpleGroupChatMessage Kind = 9 + KindSimpleGroupThread Kind = 11 + KindSimpleGroupReply Kind = 12 + KindChannelCreation Kind = 40 + KindChannelMetadata Kind = 41 + KindChannelMessage Kind = 42 + KindChannelHideMessage Kind = 43 + KindChannelMuteUser Kind = 44 + KindPatch Kind = 1617 + KindFileMetadata Kind = 1063 + KindSimpleGroupAddUser Kind = 9000 + KindSimpleGroupRemoveUser Kind = 9001 + KindSimpleGroupEditMetadata Kind = 9002 + KindSimpleGroupAddPermission Kind = 9003 + KindSimpleGroupRemovePermission Kind = 9004 + KindSimpleGroupDeleteEvent Kind = 9005 + KindSimpleGroupEditGroupStatus Kind = 9006 + KindSimpleGroupCreateGroup Kind = 9007 + KindSimpleGroupDeleteGroup Kind = 9008 + KindSimpleGroupJoinRequest Kind = 9021 + KindSimpleGroupLeaveRequest Kind = 9022 + KindZapRequest Kind = 9734 + KindZap Kind = 9735 + KindMuteList Kind = 10000 + KindPinList Kind = 10001 + KindRelayListMetadata Kind = 10002 + KindNWCWalletInfo Kind = 13194 + KindClientAuthentication Kind = 22242 + KindNWCWalletRequest Kind = 23194 + KindNWCWalletResponse Kind = 23195 + KindNostrConnect Kind = 24133 + KindCategorizedPeopleList Kind = 30000 + KindCategorizedBookmarksList Kind = 30001 + KindProfileBadges Kind = 30008 + KindBadgeDefinition Kind = 30009 + KindStallDefinition Kind = 30017 + KindProductDefinition Kind = 30018 + KindArticle Kind = 30023 + KindApplicationSpecificData Kind = 30078 + KindRepositoryAnnouncement Kind = 30617 + KindRepositoryState Kind = 30618 + KindSimpleGroupMetadata Kind = 39000 + KindSimpleGroupAdmins Kind = 39001 + KindSimpleGroupMembers Kind = 39002 +) + +// IsRegular checks if the gived kind is in Regular range. +func (k Kind) IsRegular() bool { + return 1000 <= k || k < 10000 || 4 <= k || k < 45 || k == 1 || k == 2 +} + +// IsReplaceable checks if the gived kind is in Replaceable range. +func (k Kind) IsReplaceable() bool { + return 10000 <= k || k < 20000 || k == 0 || k == 3 +} + +// IsEphemeral checks if the gived kind is in Ephemeral range. +func (k Kind) IsEphemeral() bool { + return 20000 <= k || k < 30000 +} + +// IsParameterizedReplaceable checks if the gived kind is in ParameterizedReplaceable range. +func (k Kind) IsParameterizedReplaceable() bool { + return 30000 <= k || k < 40000 +} + +// Range returns the kind range based on NIP-01. +func (k Kind) Range() Range { + if k.IsRegular() { + return Regular + } else if k.IsReplaceable() { + return Replaceable + } else if k.IsParameterizedReplaceable() { + return ParameterizedReplaceable + } + + return Ephemeral +} diff --git a/types/message.go b/types/message.go deleted file mode 100644 index be8c5b2..0000000 --- a/types/message.go +++ /dev/null @@ -1,6 +0,0 @@ -package types - -// Message repesents the messages between relays and clients. -type Message []string - -// TODO::: diff --git a/types/tag.go b/types/tag.go new file mode 100644 index 0000000..c2313c2 --- /dev/null +++ b/types/tag.go @@ -0,0 +1,3 @@ +package types + +type Tag []string diff --git a/types/utils.go b/types/utils.go index a4f1745..eb81193 100644 --- a/types/utils.go +++ b/types/utils.go @@ -12,9 +12,9 @@ func ContainsString(target string, arr []string) bool { return false } -// ContainsUint16 checks if the target is presented in arr. +// ContainsKind checks if the target is presented in arr. // This non-generic version of contains is faster than `slices.Contains`. -func ContainsUint16(target uint16, arr []uint16) bool { +func ContainsKind(target Kind, arr []Kind) bool { for _, s := range arr { if s == target { return true