diff --git a/aux.go b/aux.go new file mode 100644 index 0000000..2637fdd --- /dev/null +++ b/aux.go @@ -0,0 +1,77 @@ +package nostr + +import ( + "strings" +) + +type StringList []string +type IntList []int + +func (as StringList) Equals(bs StringList) bool { + if len(as) != len(bs) { + return false + } + + for _, a := range as { + for _, b := range bs { + if b == a { + goto next + } + } + // didn't find a B that corresponded to the current A + return false + + next: + continue + } + + return true +} + +func (as IntList) Equals(bs IntList) bool { + if len(as) != len(bs) { + return false + } + + for _, a := range as { + for _, b := range bs { + if b == a { + goto next + } + } + // didn't find a B that corresponded to the current A + return false + + next: + continue + } + + return true +} + +func (haystack StringList) Contains(needle string) bool { + for _, hay := range haystack { + if hay == needle { + return true + } + } + return false +} + +func (haystack StringList) ContainsPrefixOf(needle string) bool { + for _, hay := range haystack { + if strings.HasPrefix(needle, hay) { + return true + } + } + return false +} + +func (haystack IntList) Contains(needle int) bool { + for _, hay := range haystack { + if hay == needle { + return true + } + } + return false +} diff --git a/event.go b/event.go index 690aa0f..e1916f3 100644 --- a/event.go +++ b/event.go @@ -1,18 +1,26 @@ package nostr import ( - "bytes" "crypto/rand" "crypto/sha256" "encoding/hex" - "encoding/json" "fmt" - "strconv" "time" "github.com/fiatjaf/bip340" + "github.com/valyala/fastjson" ) +type Event struct { + ID string + PubKey string + CreatedAt time.Time + Kind int + Tags Tags + Content string + Sig string +} + const ( KindSetMetadata int = 0 KindTextNote int = 1 @@ -22,36 +30,6 @@ const ( KindDeletion int = 5 ) -type Event struct { - ID string `json:"id"` // it's the hash of the serialized event - - PubKey string `json:"pubkey"` - CreatedAt Time `json:"created_at"` - - Kind int `json:"kind"` - - Tags Tags `json:"tags"` - Content string `json:"content"` - Sig string `json:"sig"` -} - -type Time time.Time - -func (tm *Time) UnmarshalJSON(payload []byte) error { - unix, err := strconv.ParseInt(string(payload), 10, 64) - if err != nil { - return fmt.Errorf("time must be a unix timestamp as an integer, not '%s': %w", - string(payload), err) - } - t := Time(time.Unix(unix, 0)) - *tm = t - return nil -} - -func (t Time) MarshalJSON() ([]byte, error) { - return []byte(strconv.FormatInt(time.Time(t).Unix(), 10)), nil -} - // GetID serializes and returns the event ID as a string func (evt *Event) GetID() string { h := sha256.Sum256(evt.Serialize()) @@ -62,36 +40,29 @@ func (evt *Event) GetID() string { func (evt *Event) Serialize() []byte { // the serialization process is just putting everything into a JSON array // so the order is kept - arr := make([]interface{}, 6) + var arena fastjson.Arena + + arr := arena.NewArray() // version: 0 - arr[0] = 0 + arr.SetArrayItem(0, arena.NewNumberInt(0)) // pubkey - arr[1] = evt.PubKey + arr.SetArrayItem(1, arena.NewString(evt.PubKey)) // created_at - arr[2] = int64(time.Time(evt.CreatedAt).Unix()) + arr.SetArrayItem(2, arena.NewNumberInt(int(evt.CreatedAt.Unix()))) // kind - arr[3] = int64(evt.Kind) + arr.SetArrayItem(3, arena.NewNumberInt(evt.Kind)) // tags - if evt.Tags != nil { - arr[4] = evt.Tags - } else { - arr[4] = make([]bool, 0) - } + arr.SetArrayItem(4, tagsToFastjsonArray(&arena, evt.Tags)) // content - arr[5] = evt.Content + arr.SetArrayItem(5, arena.NewString(evt.Content)) - serialized := new(bytes.Buffer) - - enc := json.NewEncoder(serialized) - enc.SetEscapeHTML(false) - _ = enc.Encode(arr) - return serialized.Bytes()[:serialized.Len()-1] // Encode add new line char + return arr.MarshalTo(nil) } // CheckSignature checks if the signature is valid for the id @@ -104,19 +75,6 @@ func (evt Event) CheckSignature() (bool, error) { return false, fmt.Errorf("Event has invalid pubkey '%s': %w", evt.PubKey, err) } - // check tags - for _, tag := range evt.Tags { - for _, item := range tag { - switch item.(type) { - case string, int64, float64, int, bool: - // fine - default: - // not fine - return false, fmt.Errorf("tag contains an invalid value %v", item) - } - } - } - s, err := hex.DecodeString(evt.Sig) if err != nil { return false, fmt.Errorf("signature is invalid hex: %w", err) diff --git a/event_aux.go b/event_aux.go new file mode 100644 index 0000000..87e6843 --- /dev/null +++ b/event_aux.go @@ -0,0 +1,166 @@ +package nostr + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/valyala/fastjson" +) + +type Tags []StringList + +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 + } + + if tag[0] != tagName { + continue + } + + if values.Contains(tag[1]) { + return true + } + } + + return false +} + +func (evt *Event) UnmarshalJSON(payload []byte) error { + var fastjsonParser fastjson.Parser + parsed, err := fastjsonParser.ParseBytes(payload) + if err != nil { + return fmt.Errorf("failed to parse event: %w", err) + } + + obj, err := parsed.Object() + if err != nil { + return fmt.Errorf("event is not an object") + } + + var visiterr error + obj.Visit(func(k []byte, v *fastjson.Value) { + key := string(k) + switch key { + case "id": + id, err := v.StringBytes() + if err != nil { + visiterr = fmt.Errorf("invalid 'id' field: %w", err) + } + evt.ID = string(id) + case "pubkey": + id, err := v.StringBytes() + if err != nil { + visiterr = fmt.Errorf("invalid 'pubkey' field: %w", err) + } + evt.PubKey = string(id) + case "created_at": + val, err := v.Int64() + if err != nil { + visiterr = fmt.Errorf("invalid 'created_at' field: %w", err) + } + evt.CreatedAt = time.Unix(val, 0) + case "kind": + kind, err := v.Int64() + if err != nil { + visiterr = fmt.Errorf("invalid 'kind' field: %w", err) + } + evt.Kind = int(kind) + case "tags": + evt.Tags, err = fastjsonArrayToTags(v) + if err != nil { + visiterr = fmt.Errorf("invalid '%s' field: %w", key, err) + } + case "content": + id, err := v.StringBytes() + if err != nil { + visiterr = fmt.Errorf("invalid 'content' field: %w", err) + } + evt.Content = string(id) + case "sig": + id, err := v.StringBytes() + if err != nil { + visiterr = fmt.Errorf("invalid 'sig' field: %w", err) + } + evt.Sig = string(id) + } + }) + if visiterr != nil { + return visiterr + } + + return nil +} + +func (evt Event) MarshalJSON() ([]byte, error) { + var arena fastjson.Arena + + o := arena.NewObject() + o.Set("id", arena.NewString(evt.ID)) + o.Set("pubkey", arena.NewString(evt.PubKey)) + o.Set("created_at", arena.NewNumberInt(int(evt.CreatedAt.Unix()))) + o.Set("kind", arena.NewNumberInt(evt.Kind)) + o.Set("tags", tagsToFastjsonArray(&arena, evt.Tags)) + o.Set("content", arena.NewString(evt.Content)) + o.Set("sig", arena.NewString(evt.Sig)) + + return o.MarshalTo(nil), nil +} + +func fastjsonArrayToTags(v *fastjson.Value) (Tags, error) { + arr, err := v.Array() + if err != nil { + return nil, err + } + + sll := make([]StringList, len(arr)) + for i, v := range arr { + subarr, err := v.Array() + if err != nil { + return nil, err + } + + sl := make(StringList, len(arr)) + for j, subv := range subarr { + sb, err := subv.StringBytes() + if err != nil { + return nil, err + } + sl[j] = string(sb) + } + sll[i] = sl + } + + return Tags(sll), nil +} + +func tagsToFastjsonArray(arena *fastjson.Arena, tags Tags) *fastjson.Value { + jtags := arena.NewArray() + for i, v := range tags { + arr := arena.NewArray() + for j, subv := range v { + arr.SetArrayItem(j, arena.NewString(subv)) + } + jtags.SetArrayItem(i, arr) + } + return jtags +} diff --git a/filter_aux.go b/filter_aux.go index 362158e..1bf29b1 100644 --- a/filter_aux.go +++ b/filter_aux.go @@ -8,78 +8,6 @@ import ( "github.com/valyala/fastjson" ) -type StringList []string -type IntList []int - -func (as StringList) Equals(bs StringList) bool { - if len(as) != len(bs) { - return false - } - - for _, a := range as { - for _, b := range bs { - if b == a { - goto next - } - } - // didn't find a B that corresponded to the current A - return false - - next: - continue - } - - return true -} - -func (as IntList) Equals(bs IntList) bool { - if len(as) != len(bs) { - return false - } - - for _, a := range as { - for _, b := range bs { - if b == a { - goto next - } - } - // didn't find a B that corresponded to the current A - return false - - next: - continue - } - - return true -} - -func (haystack StringList) Contains(needle string) bool { - for _, hay := range haystack { - if hay == needle { - return true - } - } - return false -} - -func (haystack StringList) ContainsPrefixOf(needle string) bool { - for _, hay := range haystack { - if strings.HasPrefix(needle, hay) { - return true - } - } - return false -} - -func (haystack IntList) Contains(needle int) bool { - for _, hay := range haystack { - if hay == needle { - return true - } - } - return false -} - func (f *Filter) UnmarshalJSON(payload []byte) error { var fastjsonParser fastjson.Parser parsed, err := fastjsonParser.ParseBytes(payload) diff --git a/tags.go b/tags.go deleted file mode 100644 index 593a762..0000000 --- a/tags.go +++ /dev/null @@ -1,49 +0,0 @@ -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 -}