diff --git a/event.go b/event.go index 5d687e9..60386a0 100644 --- a/event.go +++ b/event.go @@ -4,7 +4,6 @@ import ( "crypto/sha256" "encoding/hex" "fmt" - "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" @@ -24,12 +23,6 @@ type Event struct { extra map[string]any } -type Timestamp int64 - -func (t Timestamp) Time() time.Time { - return time.Unix(int64(t), 0) -} - const ( KindSetMetadata int = 0 KindTextNote int = 1 diff --git a/event_easyjson.go b/event_easyjson.go index 93b00cf..fc972b6 100644 --- a/event_easyjson.go +++ b/event_easyjson.go @@ -27,6 +27,7 @@ func easyjsonF642ad3eDecodeGithubComNbdWtfGoNostr(in *jlexer.Lexer, out *Event) in.Skip() return } + out.extra = make(map[string]any) in.Delim('{') for !in.IsDelim('}') { key := in.UnsafeFieldName(true) diff --git a/event_test.go b/event_test.go index 529234d..d2527f2 100644 --- a/event_test.go +++ b/event_test.go @@ -3,7 +3,6 @@ package nostr import ( "encoding/json" "testing" - "time" ) func TestEventParsingAndVerifying(t *testing.T) { @@ -47,7 +46,7 @@ func TestEventSerialization(t *testing.T) { ID: "92570b321da503eac8014b23447301eb3d0bbdfbace0d11a4e4072e72bb7205d", PubKey: "e9142f724955c5854de36324dab0434f97b15ec6b33464d56ebe491e3f559d1b", Kind: 4, - CreatedAt: time.Unix(1671028682, 0), + CreatedAt: Timestamp(1671028682), Tags: Tags{Tag{"p", "f8340b2bde651576b75af61aa26c80e13c65029f00f7f64004eece679bf7059f"}}, Content: "you say yes, I say no", Sig: "ed08d2dd5b0f7b6a3cdc74643d4adee3158ddede9cc848e8cd97630c097001acc2d052d2d3ec2b7ac4708b2314b797106d1b3c107322e61b5e5cc2116e099b79", @@ -68,7 +67,7 @@ func TestEventSerialization(t *testing.T) { } if evt.ID != re.ID || evt.PubKey != re.PubKey || evt.Content != re.Content || - !evt.CreatedAt.Equal(re.CreatedAt) || evt.Sig != re.Sig || + evt.CreatedAt != re.CreatedAt || evt.Sig != re.Sig || len(evt.Tags) != len(evt.Tags) { t.Error("reparsed event differs from original") } @@ -93,7 +92,7 @@ func TestEventSerializationWithExtraFields(t *testing.T) { ID: "92570b321da503eac8014b23447301eb3d0bbdfbace0d11a4e4072e72bb7205d", PubKey: "e9142f724955c5854de36324dab0434f97b15ec6b33464d56ebe491e3f559d1b", Kind: 7, - CreatedAt: time.Unix(1671028682, 0), + CreatedAt: Timestamp(1671028682), Content: "there is an extra field here", Sig: "ed08d2dd5b0f7b6a3cdc74643d4adee3158ddede9cc848e8cd97630c097001acc2d052d2d3ec2b7ac4708b2314b797106d1b3c107322e61b5e5cc2116e099b79", } @@ -115,7 +114,7 @@ func TestEventSerializationWithExtraFields(t *testing.T) { } if evt.ID != re.ID || evt.PubKey != re.PubKey || evt.Content != re.Content || - !evt.CreatedAt.Equal(re.CreatedAt) || evt.Sig != re.Sig || + evt.CreatedAt != re.CreatedAt || evt.Sig != re.Sig || len(evt.Tags) != len(evt.Tags) { t.Error("reparsed event differs from original") } diff --git a/filter.go b/filter.go index ca2ff90..fbd604a 100644 --- a/filter.go +++ b/filter.go @@ -9,14 +9,14 @@ import ( type Filters []Filter type Filter struct { - IDs []string - Kinds []int - Authors []string - Tags TagMap - Since Timestamp - Until Timestamp - Limit int - Search string + IDs []string `json:"ids"` + Kinds []int `json:"kinds"` + Authors []string `json:"authors"` + Tags TagMap `json:"-"` + Since *Timestamp `json:"since"` + Until *Timestamp `json:"until"` + Limit int `json:"limit"` + Search string `json:"search"` } type TagMap map[string][]string @@ -63,11 +63,11 @@ func (ef Filter) Matches(event *Event) bool { } } - if event.CreatedAt < ef.Since { + if ef.Since != nil && event.CreatedAt < *ef.Since { return false } - if ef.Until != 0 && event.CreatedAt > ef.Until { + if ef.Until != nil && event.CreatedAt > *ef.Until { return false } diff --git a/filter_easyjson.go b/filter_easyjson.go index fd80947..a8b6bb2 100644 --- a/filter_easyjson.go +++ b/filter_easyjson.go @@ -4,6 +4,7 @@ package nostr import ( json "encoding/json" + easyjson "github.com/mailru/easyjson" jlexer "github.com/mailru/easyjson/jlexer" jwriter "github.com/mailru/easyjson/jwriter" @@ -26,6 +27,7 @@ func easyjson4d398eaaDecodeGithubComNbdWtfGoNostr(in *jlexer.Lexer, out *Filter) in.Skip() return } + out.Tags = make(TagMap) in.Delim('{') for !in.IsDelim('}') { key := in.UnsafeFieldName(false) @@ -36,7 +38,7 @@ func easyjson4d398eaaDecodeGithubComNbdWtfGoNostr(in *jlexer.Lexer, out *Filter) continue } switch key { - case "IDs": + case "ids": if in.IsNull() { in.Skip() out.IDs = nil @@ -44,7 +46,7 @@ func easyjson4d398eaaDecodeGithubComNbdWtfGoNostr(in *jlexer.Lexer, out *Filter) in.Delim('[') if out.IDs == nil { if !in.IsDelim(']') { - out.IDs = make([]string, 0, 4) + out.IDs = make([]string, 0, 20) } else { out.IDs = []string{} } @@ -59,7 +61,7 @@ func easyjson4d398eaaDecodeGithubComNbdWtfGoNostr(in *jlexer.Lexer, out *Filter) } in.Delim(']') } - case "Kinds": + case "kinds": if in.IsNull() { in.Skip() out.Kinds = nil @@ -82,7 +84,7 @@ func easyjson4d398eaaDecodeGithubComNbdWtfGoNostr(in *jlexer.Lexer, out *Filter) } in.Delim(']') } - case "Authors": + case "authors": if in.IsNull() { in.Skip() out.Authors = nil @@ -90,7 +92,7 @@ func easyjson4d398eaaDecodeGithubComNbdWtfGoNostr(in *jlexer.Lexer, out *Filter) in.Delim('[') if out.Authors == nil { if !in.IsDelim(']') { - out.Authors = make([]string, 0, 4) + out.Authors = make([]string, 0, 40) } else { out.Authors = []string{} } @@ -105,53 +107,56 @@ func easyjson4d398eaaDecodeGithubComNbdWtfGoNostr(in *jlexer.Lexer, out *Filter) } in.Delim(']') } - case "Tags": + case "since": if in.IsNull() { in.Skip() + out.Since = nil } else { - in.Delim('{') - out.Tags = make(TagMap) - for !in.IsDelim('}') { - key := string(in.String()) - in.WantColon() - var v4 []string - if in.IsNull() { - in.Skip() - v4 = nil - } else { - in.Delim('[') - if v4 == nil { - if !in.IsDelim(']') { - v4 = make([]string, 0, 4) - } else { - v4 = []string{} - } - } 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() + if out.Since == nil { + out.Since = new(Timestamp) } - in.Delim('}') + *out.Since = Timestamp(in.Int64()) } - case "Since": - out.Since = Timestamp(in.Int64()) - case "Until": - out.Until = Timestamp(in.Int64()) - case "Limit": + case "until": + if in.IsNull() { + in.Skip() + out.Until = nil + } else { + if out.Until == nil { + out.Until = new(Timestamp) + } + *out.Until = Timestamp(in.Int64()) + } + case "limit": out.Limit = int(in.Int()) - case "Search": + case "search": out.Search = string(in.String()) default: - in.SkipRecursive() + if len(key) > 1 && key[0] == '#' { + tagValues := make([]string, 0, 40) + if !in.IsNull() { + in.Delim('[') + if out.Authors == nil { + if !in.IsDelim(']') { + tagValues = make([]string, 0, 4) + } else { + tagValues = []string{} + } + } else { + tagValues = (tagValues)[:0] + } + for !in.IsDelim(']') { + var v3 string + v3 = string(in.String()) + tagValues = append(tagValues, v3) + in.WantComma() + } + in.Delim(']') + } + out.Tags[key[1:]] = tagValues + } else { + in.SkipRecursive() + } } in.WantComma() } @@ -160,107 +165,84 @@ func easyjson4d398eaaDecodeGithubComNbdWtfGoNostr(in *jlexer.Lexer, out *Filter) in.Consumed() } } + func easyjson4d398eaaEncodeGithubComNbdWtfGoNostr(out *jwriter.Writer, in Filter) { out.RawByte('{') first := true _ = first { - const prefix string = ",\"IDs\":" + 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 { + for v4, v5 := range in.IDs { + if v4 > 0 { out.RawByte(',') } - out.String(string(v7)) + out.String(string(v5)) } out.RawByte(']') } } { - const prefix string = ",\"Kinds\":" + const prefix string = ",\"kinds\":" out.RawString(prefix) if in.Kinds == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { out.RawString("null") } else { out.RawByte('[') - for v8, v9 := range in.Kinds { - if v8 > 0 { + for v6, v7 := range in.Kinds { + if v6 > 0 { out.RawByte(',') } - out.Int(int(v9)) + out.Int(int(v7)) } out.RawByte(']') } } { - const prefix string = ",\"Authors\":" + const prefix string = ",\"authors\":" out.RawString(prefix) if in.Authors == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { out.RawString("null") } else { out.RawByte('[') - for v10, v11 := range in.Authors { - if v10 > 0 { + for v8, v9 := range in.Authors { + if v8 > 0 { out.RawByte(',') } - out.String(string(v11)) + out.String(string(v9)) } out.RawByte(']') } } { - const prefix string = ",\"Tags\":" + const prefix string = ",\"since\":" out.RawString(prefix) - if in.Tags == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { - out.RawString(`null`) + if in.Since == nil { + 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('}') + out.Int64(int64(*in.Since)) } } { - const prefix string = ",\"Since\":" + const prefix string = ",\"until\":" out.RawString(prefix) - out.Int64(int64(in.Since)) + if in.Until == nil { + out.RawString("null") + } else { + out.Int64(int64(*in.Until)) + } } { - const prefix string = ",\"Until\":" - out.RawString(prefix) - out.Int64(int64(in.Until)) - } - { - const prefix string = ",\"Limit\":" + const prefix string = ",\"limit\":" out.RawString(prefix) out.Int(int(in.Limit)) } { - const prefix string = ",\"Search\":" + const prefix string = ",\"search\":" out.RawString(prefix) out.String(string(in.Search)) } diff --git a/filter_test.go b/filter_test.go index 2832bfc..5559d98 100644 --- a/filter_test.go +++ b/filter_test.go @@ -3,7 +3,6 @@ package nostr import ( "encoding/json" "testing" - "time" "golang.org/x/exp/slices" ) @@ -16,7 +15,7 @@ func TestFilterUnmarshal(t *testing.T) { t.Errorf("failed to parse filter json: %v", err) } - if f.Since == nil || f.Since.UTC().Format("2006-01-02") != "2022-02-07" || + if f.Since == nil || f.Since.Time().UTC().Format("2006-01-02") != "2022-02-07" || f.Until != nil || f.Tags == nil || len(f.Tags) != 2 || !slices.Contains(f.Tags["something"], "bab") || f.Search != "test" { @@ -25,12 +24,11 @@ func TestFilterUnmarshal(t *testing.T) { } func TestFilterMarshal(t *testing.T) { - tm := time.Unix(12345678, 0) - + until := Timestamp(12345678) filterj, err := json.Marshal(Filter{ Kinds: []int{1, 2, 4}, Tags: TagMap{"fruit": {"banana", "mango"}}, - Until: &tm, + Until: &until, }) if err != nil { t.Errorf("failed to marshal filter json: %v", err) @@ -93,7 +91,7 @@ func TestFilterEquality(t *testing.T) { t.Error("kind+tags filters should be equal") } - tm := time.Now() + tm := Now() if !FilterEqual( Filter{ Kinds: []int{4, 5}, diff --git a/nip13/nip13.go b/nip13/nip13.go index 35a7d1f..ba63cf1 100644 --- a/nip13/nip13.go +++ b/nip13/nip13.go @@ -64,7 +64,7 @@ func Generate(event *nostr.Event, targetDifficulty int, timeout time.Duration) ( for { nonce++ tag[1] = strconv.FormatUint(nonce, 10) - event.CreatedAt = time.Now() + event.CreatedAt = nostr.Now() if Difficulty(event.GetID()) >= targetDifficulty { return event, nil } diff --git a/nip26/nip26.go b/nip26/nip26.go index f5da8d7..ca08933 100644 --- a/nip26/nip26.go +++ b/nip26/nip26.go @@ -135,10 +135,10 @@ jump1: } jump2: - if d.since != nil && ev.CreatedAt.Before(*d.since) { + if d.since != nil && ev.CreatedAt.Time().Before(*d.since) { return false, fmt.Errorf("Event is created before delegation conditions allow.") } - if d.until != nil && ev.CreatedAt.After(*d.until) { + if d.until != nil && ev.CreatedAt.Time().After(*d.until) { return false, fmt.Errorf("Event is created after delegation conditions allow.") } @@ -166,7 +166,7 @@ func DelegatedSign(ev *nostr.Event, d *DelegationToken, delegatee_sk string) err return fmt.Errorf("event already has delegation token") } } - if d.since != nil && ev.CreatedAt.Before(*d.since) || d.until != nil && ev.CreatedAt.After(*d.until) { + if d.since != nil && ev.CreatedAt.Time().Before(*d.since) || d.until != nil && ev.CreatedAt.Time().After(*d.until) { return fmt.Errorf("event created_at field is not compatible with delegation token time conditions.") } if len(d.kinds) > 0 { diff --git a/nip26/nip26_test.go b/nip26/nip26_test.go index cb01a32..d5e59a9 100644 --- a/nip26/nip26_test.go +++ b/nip26/nip26_test.go @@ -1,9 +1,10 @@ package nip26 import ( - "github.com/nbd-wtf/go-nostr" "testing" "time" + + "github.com/nbd-wtf/go-nostr" ) func TestDelegateSign(t *testing.T) { @@ -16,7 +17,7 @@ func TestDelegateSign(t *testing.T) { t.Error(err) } ev := &nostr.Event{} - ev.CreatedAt = time.Unix(1600000050, 0) + ev.CreatedAt = nostr.Timestamp(1600000050) ev.Content = "hello world" ev.Kind = 1 if err != nil { diff --git a/nip42/nip42.go b/nip42/nip42.go index a00b390..9fd8d3e 100644 --- a/nip42/nip42.go +++ b/nip42/nip42.go @@ -13,7 +13,7 @@ import ( func CreateUnsignedAuthEvent(challenge, pubkey, relayURL string) nostr.Event { return nostr.Event{ PubKey: pubkey, - CreatedAt: time.Now(), + CreatedAt: nostr.Now(), Kind: 22242, Tags: nostr.Tags{ nostr.Tag{"relay", relayURL}, @@ -60,7 +60,7 @@ func ValidateAuthEvent(event *nostr.Event, challenge string, relayURL string) (p } now := time.Now() - if event.CreatedAt.After(now.Add(10*time.Minute)) || event.CreatedAt.Before(now.Add(-10*time.Minute)) { + if event.CreatedAt.Time().After(now.Add(10*time.Minute)) || event.CreatedAt.Time().Before(now.Add(-10*time.Minute)) { return "", false } diff --git a/relay_test.go b/relay_test.go index 832bc7a..48b4e18 100644 --- a/relay_test.go +++ b/relay_test.go @@ -21,7 +21,7 @@ func TestPublish(t *testing.T) { textNote := Event{ Kind: 1, Content: "hello", - CreatedAt: time.Unix(1672068534, 0), // random fixed timestamp + CreatedAt: Timestamp(1672068534), // random fixed timestamp Tags: Tags{[]string{"foo", "bar"}}, PubKey: pub, } diff --git a/timestamp.go b/timestamp.go new file mode 100644 index 0000000..43ecb98 --- /dev/null +++ b/timestamp.go @@ -0,0 +1,13 @@ +package nostr + +import "time" + +type Timestamp int64 + +func Now() Timestamp { + return Timestamp(time.Now().Unix()) +} + +func (t Timestamp) Time() time.Time { + return time.Unix(int64(t), 0) +}