fix and improve envelope stuff again, deal with messages as strings on all envelope parsing steps.

This commit is contained in:
fiatjaf 2025-03-12 00:17:01 -03:00
parent 42a2243b72
commit cecc71cd81
11 changed files with 151 additions and 140 deletions

View File

@ -1,81 +1,75 @@
package nostr package nostr
import ( import (
"bytes"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"unsafe"
"github.com/mailru/easyjson" "github.com/mailru/easyjson"
jwriter "github.com/mailru/easyjson/jwriter" jwriter "github.com/mailru/easyjson/jwriter"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
var ( var UnknownLabel = errors.New("unknown envelope label")
labelEvent = []byte("EVENT")
labelReq = []byte("REQ")
labelCount = []byte("COUNT")
labelNotice = []byte("NOTICE")
labelEose = []byte("EOSE")
labelOk = []byte("OK")
labelAuth = []byte("AUTH")
labelClosed = []byte("CLOSED")
labelClose = []byte("CLOSE")
UnknownLabel = errors.New("unknown envelope label")
)
type MessageParser interface { type MessageParser interface {
// ParseMessage parses a message into an Envelope. // ParseMessage parses a message into an Envelope.
ParseMessage([]byte) (Envelope, error) ParseMessage(string) (Envelope, error)
} }
// Deprecated: use NewMessageParser instead // Deprecated: use NewMessageParser instead
func ParseMessage(message []byte) Envelope { func ParseMessage(message string) Envelope {
firstComma := bytes.Index(message, []byte{','}) firstQuote := strings.IndexRune(message, '"')
if firstComma == -1 { if firstQuote == -1 {
return nil return nil
} }
label := message[0:firstComma] secondQuote := strings.IndexRune(message[firstQuote+1:], '"')
if secondQuote == -1 {
return nil
}
label := message[firstQuote+1 : firstQuote+1+secondQuote]
var v Envelope var v Envelope
switch { switch label {
case bytes.Contains(label, labelEvent): case "EVENT":
v = &EventEnvelope{} v = &EventEnvelope{}
case bytes.Contains(label, labelReq): case "REQ":
v = &ReqEnvelope{} v = &ReqEnvelope{}
case bytes.Contains(label, labelCount): case "COUNT":
v = &CountEnvelope{} v = &CountEnvelope{}
case bytes.Contains(label, labelNotice): case "NOTICE":
x := NoticeEnvelope("") x := NoticeEnvelope("")
v = &x v = &x
case bytes.Contains(label, labelEose): case "EOSE":
x := EOSEEnvelope("") x := EOSEEnvelope("")
v = &x v = &x
case bytes.Contains(label, labelOk): case "OK":
v = &OKEnvelope{} v = &OKEnvelope{}
case bytes.Contains(label, labelAuth): case "AUTH":
v = &AuthEnvelope{} v = &AuthEnvelope{}
case bytes.Contains(label, labelClosed): case "CLOSED":
v = &ClosedEnvelope{} v = &ClosedEnvelope{}
case bytes.Contains(label, labelClose): case "CLOSE":
x := CloseEnvelope("") x := CloseEnvelope("")
v = &x v = &x
default: default:
return nil return nil
} }
if err := v.UnmarshalJSON(message); err != nil { if err := v.FromJSON(message); err != nil {
return nil return nil
} }
return v return v
} }
// Envelope is the interface for all nostr message envelopes. // Envelope is the interface for all nostr message envelopes.
type Envelope interface { type Envelope interface {
Label() string Label() string
UnmarshalJSON([]byte) error FromJSON(string) error
MarshalJSON() ([]byte, error) MarshalJSON() ([]byte, error)
String() string String() string
} }
@ -99,15 +93,15 @@ type EventEnvelope struct {
func (_ EventEnvelope) Label() string { return "EVENT" } func (_ EventEnvelope) Label() string { return "EVENT" }
func (v *EventEnvelope) UnmarshalJSON(data []byte) error { func (v *EventEnvelope) FromJSON(data string) error {
r := gjson.ParseBytes(data) r := gjson.Parse(data)
arr := r.Array() arr := r.Array()
switch len(arr) { switch len(arr) {
case 2: case 2:
return easyjson.Unmarshal([]byte(arr[1].Raw), &v.Event) return easyjson.Unmarshal(unsafe.Slice(unsafe.StringData(arr[1].Raw), len(arr[1].Raw)), &v.Event)
case 3: case 3:
v.SubscriptionID = &arr[1].Str v.SubscriptionID = &arr[1].Str
return easyjson.Unmarshal([]byte(arr[2].Raw), &v.Event) return easyjson.Unmarshal(unsafe.Slice(unsafe.StringData(arr[2].Raw), len(arr[2].Raw)), &v.Event)
default: default:
return fmt.Errorf("failed to decode EVENT envelope") return fmt.Errorf("failed to decode EVENT envelope")
} }
@ -134,8 +128,8 @@ type ReqEnvelope struct {
func (_ ReqEnvelope) Label() string { return "REQ" } func (_ ReqEnvelope) Label() string { return "REQ" }
func (v *ReqEnvelope) UnmarshalJSON(data []byte) error { func (v *ReqEnvelope) FromJSON(data string) error {
r := gjson.ParseBytes(data) r := gjson.Parse(data)
arr := r.Array() arr := r.Array()
if len(arr) < 3 { if len(arr) < 3 {
return fmt.Errorf("failed to decode REQ envelope: missing filters") return fmt.Errorf("failed to decode REQ envelope: missing filters")
@ -144,7 +138,7 @@ func (v *ReqEnvelope) UnmarshalJSON(data []byte) error {
v.Filters = make(Filters, len(arr)-2) v.Filters = make(Filters, len(arr)-2)
f := 0 f := 0
for i := 2; i < len(arr); i++ { for i := 2; i < len(arr); i++ {
if err := easyjson.Unmarshal([]byte(arr[i].Raw), &v.Filters[f]); err != nil { if err := easyjson.Unmarshal(unsafe.Slice(unsafe.StringData(arr[i].Raw), len(arr[i].Raw)), &v.Filters[f]); err != nil {
return fmt.Errorf("%w -- on filter %d", err, f) return fmt.Errorf("%w -- on filter %d", err, f)
} }
f++ f++
@ -180,8 +174,8 @@ func (c CountEnvelope) String() string {
return string(v) return string(v)
} }
func (v *CountEnvelope) UnmarshalJSON(data []byte) error { func (v *CountEnvelope) FromJSON(data string) error {
r := gjson.ParseBytes(data) r := gjson.Parse(data)
arr := r.Array() arr := r.Array()
if len(arr) < 3 { if len(arr) < 3 {
return fmt.Errorf("failed to decode COUNT envelope: missing filters") return fmt.Errorf("failed to decode COUNT envelope: missing filters")
@ -192,7 +186,7 @@ func (v *CountEnvelope) UnmarshalJSON(data []byte) error {
Count *int64 `json:"count"` Count *int64 `json:"count"`
HLL string `json:"hll"` HLL string `json:"hll"`
} }
if err := json.Unmarshal([]byte(arr[2].Raw), &countResult); err == nil && countResult.Count != nil { if err := json.Unmarshal(unsafe.Slice(unsafe.StringData(arr[2].Raw), len(arr[2].Raw)), &countResult); err == nil && countResult.Count != nil {
v.Count = countResult.Count v.Count = countResult.Count
if len(countResult.HLL) == 512 { if len(countResult.HLL) == 512 {
v.HyperLogLog, err = hex.DecodeString(countResult.HLL) v.HyperLogLog, err = hex.DecodeString(countResult.HLL)
@ -205,7 +199,7 @@ func (v *CountEnvelope) UnmarshalJSON(data []byte) error {
f := 0 f := 0
for i := 2; i < len(arr); i++ { for i := 2; i < len(arr); i++ {
item := []byte(arr[i].Raw) item := unsafe.Slice(unsafe.StringData(arr[i].Raw), len(arr[i].Raw))
if err := easyjson.Unmarshal(item, &v.Filter); err != nil { if err := easyjson.Unmarshal(item, &v.Filter); err != nil {
return fmt.Errorf("%w -- on filter %d", err, f) return fmt.Errorf("%w -- on filter %d", err, f)
@ -249,8 +243,8 @@ func (n NoticeEnvelope) String() string {
return string(v) return string(v)
} }
func (v *NoticeEnvelope) UnmarshalJSON(data []byte) error { func (v *NoticeEnvelope) FromJSON(data string) error {
r := gjson.ParseBytes(data) r := gjson.Parse(data)
arr := r.Array() arr := r.Array()
if len(arr) < 2 { if len(arr) < 2 {
return fmt.Errorf("failed to decode NOTICE envelope") return fmt.Errorf("failed to decode NOTICE envelope")
@ -276,8 +270,8 @@ func (e EOSEEnvelope) String() string {
return string(v) return string(v)
} }
func (v *EOSEEnvelope) UnmarshalJSON(data []byte) error { func (v *EOSEEnvelope) FromJSON(data string) error {
r := gjson.ParseBytes(data) r := gjson.Parse(data)
arr := r.Array() arr := r.Array()
if len(arr) < 2 { if len(arr) < 2 {
return fmt.Errorf("failed to decode EOSE envelope") return fmt.Errorf("failed to decode EOSE envelope")
@ -303,8 +297,8 @@ func (c CloseEnvelope) String() string {
return string(v) return string(v)
} }
func (v *CloseEnvelope) UnmarshalJSON(data []byte) error { func (v *CloseEnvelope) FromJSON(data string) error {
r := gjson.ParseBytes(data) r := gjson.Parse(data)
arr := r.Array() arr := r.Array()
switch len(arr) { switch len(arr) {
case 2: case 2:
@ -335,8 +329,8 @@ func (c ClosedEnvelope) String() string {
return string(v) return string(v)
} }
func (v *ClosedEnvelope) UnmarshalJSON(data []byte) error { func (v *ClosedEnvelope) FromJSON(data string) error {
r := gjson.ParseBytes(data) r := gjson.Parse(data)
arr := r.Array() arr := r.Array()
switch len(arr) { switch len(arr) {
case 3: case 3:
@ -370,8 +364,8 @@ func (o OKEnvelope) String() string {
return string(v) return string(v)
} }
func (v *OKEnvelope) UnmarshalJSON(data []byte) error { func (v *OKEnvelope) FromJSON(data string) error {
r := gjson.ParseBytes(data) r := gjson.Parse(data)
arr := r.Array() arr := r.Array()
if len(arr) < 4 { if len(arr) < 4 {
return fmt.Errorf("failed to decode OK envelope: missing fields") return fmt.Errorf("failed to decode OK envelope: missing fields")
@ -411,14 +405,14 @@ func (a AuthEnvelope) String() string {
return string(v) return string(v)
} }
func (v *AuthEnvelope) UnmarshalJSON(data []byte) error { func (v *AuthEnvelope) FromJSON(data string) error {
r := gjson.ParseBytes(data) r := gjson.Parse(data)
arr := r.Array() arr := r.Array()
if len(arr) < 2 { if len(arr) < 2 {
return fmt.Errorf("failed to decode Auth envelope: missing fields") return fmt.Errorf("failed to decode Auth envelope: missing fields")
} }
if arr[1].IsObject() { if arr[1].IsObject() {
return easyjson.Unmarshal([]byte(arr[1].Raw), &v.Event) return easyjson.Unmarshal(unsafe.Slice(unsafe.StringData(arr[1].Raw), len(arr[1].Raw)), &v.Event)
} else { } else {
v.Challenge = &arr[1].Str v.Challenge = &arr[1].Str
} }

View File

@ -1,3 +1,5 @@
//go:build sonic
package nostr package nostr
import ( import (
@ -6,6 +8,7 @@ import (
"math/rand/v2" "math/rand/v2"
"testing" "testing"
"time" "time"
"unsafe"
) )
func BenchmarkParseMessage(b *testing.B) { func BenchmarkParseMessage(b *testing.B) {
@ -17,7 +20,7 @@ func BenchmarkParseMessage(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
for _, msg := range messages { for _, msg := range messages {
var v any var v any
stdlibjson.Unmarshal(msg, &v) stdlibjson.Unmarshal(unsafe.Slice(unsafe.StringData(msg), len(msg)), &v)
} }
} }
}) })
@ -42,8 +45,8 @@ func BenchmarkParseMessage(b *testing.B) {
} }
} }
func generateTestMessages(typ string) [][]byte { func generateTestMessages(typ string) []string {
messages := make([][]byte, 0, 600) messages := make([]string, 0, 600)
setup := map[string]map[int]func() []byte{ setup := map[string]map[int]func() []byte{
"client": { "client": {
@ -62,7 +65,7 @@ func generateTestMessages(typ string) [][]byte {
for count, generator := range setup { for count, generator := range setup {
for range count { for range count {
messages = append(messages, generator()) messages = append(messages, string(generator()))
} }
} }

View File

@ -3,8 +3,8 @@
package nostr package nostr
import ( import (
"bytes"
"errors" "errors"
"strings"
) )
func NewMessageParser() MessageParser { func NewMessageParser() MessageParser {
@ -13,41 +13,45 @@ func NewMessageParser() MessageParser {
type messageParser struct{} type messageParser struct{}
func (messageParser) ParseMessage(message []byte) (Envelope, error) { func (messageParser) ParseMessage(message string) (Envelope, error) {
firstComma := bytes.Index(message, []byte{','}) firstQuote := strings.IndexRune(message, '"')
if firstComma == -1 { if firstQuote == -1 {
return nil, errors.New("malformed json") return nil, errors.New("malformed json")
} }
label := message[0:firstComma] secondQuote := strings.IndexRune(message[firstQuote+1:], '"')
if secondQuote == -1 {
return nil, errors.New("malformed json")
}
label := message[firstQuote+1 : firstQuote+1+secondQuote]
var v Envelope var v Envelope
switch { switch label {
case bytes.Contains(label, labelEvent): case "EVENT":
v = &EventEnvelope{} v = &EventEnvelope{}
case bytes.Contains(label, labelReq): case "REQ":
v = &ReqEnvelope{} v = &ReqEnvelope{}
case bytes.Contains(label, labelCount): case "COUNT":
v = &CountEnvelope{} v = &CountEnvelope{}
case bytes.Contains(label, labelNotice): case "NOTICE":
x := NoticeEnvelope("") x := NoticeEnvelope("")
v = &x v = &x
case bytes.Contains(label, labelEose): case "EOSE":
x := EOSEEnvelope("") x := EOSEEnvelope("")
v = &x v = &x
case bytes.Contains(label, labelOk): case "OK":
v = &OKEnvelope{} v = &OKEnvelope{}
case bytes.Contains(label, labelAuth): case "AUTH":
v = &AuthEnvelope{} v = &AuthEnvelope{}
case bytes.Contains(label, labelClosed): case "CLOSED":
v = &ClosedEnvelope{} v = &ClosedEnvelope{}
case bytes.Contains(label, labelClose): case "CLOSE":
x := CloseEnvelope("") x := CloseEnvelope("")
v = &x v = &x
default: default:
return nil, UnknownLabel return nil, UnknownLabel
} }
if err := v.UnmarshalJSON(message); err != nil { if err := v.FromJSON(message); err != nil {
return nil, err return nil, err
} }
return v, nil return v, nil

View File

@ -551,11 +551,11 @@ func (smp *sonicMessageParser) doneWithIntSlice(slice []int) {
// When an unexpected message (like ["NEG-OPEN", ...]) is found, the error UnknownLabel will be // When an unexpected message (like ["NEG-OPEN", ...]) is found, the error UnknownLabel will be
// returned. Other errors will be returned if the JSON is malformed or the objects are not exactly // returned. Other errors will be returned if the JSON is malformed or the objects are not exactly
// as they should. // as they should.
func (smp sonicMessageParser) ParseMessage(message []byte) (Envelope, error) { func (smp sonicMessageParser) ParseMessage(message string) (Envelope, error) {
sv := &sonicVisitor{smp: &smp} sv := &sonicVisitor{smp: &smp}
sv.whereWeAre = inEnvelope sv.whereWeAre = inEnvelope
err := ast.Preorder(unsafe.String(unsafe.SliceData(message), len(message)), sv, nil) err := ast.Preorder(message, sv, nil)
return sv.mainEnvelope, err return sv.mainEnvelope, err
} }

View File

@ -1,3 +1,5 @@
//go:build sonic
package nostr package nostr
import ( import (
@ -11,97 +13,97 @@ import (
func TestParseMessage(t *testing.T) { func TestParseMessage(t *testing.T) {
testCases := []struct { testCases := []struct {
Name string Name string
Message []byte Message string
ExpectedEnvelope Envelope ExpectedEnvelope Envelope
}{ }{
{ {
Name: "nil", Name: "nil",
Message: nil, Message: "",
ExpectedEnvelope: nil, ExpectedEnvelope: nil,
}, },
{ {
Name: "invalid string", Name: "invalid string",
Message: []byte("invalid input"), Message: "invalid input",
ExpectedEnvelope: nil, ExpectedEnvelope: nil,
}, },
{ {
Name: "invalid string with a comma", Name: "invalid string with a comma",
Message: []byte("invalid, input"), Message: "invalid, input",
ExpectedEnvelope: nil, ExpectedEnvelope: nil,
}, },
{ {
Name: "EVENT envelope with subscription id", Name: "EVENT envelope with subscription id",
Message: []byte(`["EVENT","_",{"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"}]`), Message: `["EVENT","_",{"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"}]`,
ExpectedEnvelope: &EventEnvelope{SubscriptionID: ptr("_"), Event: Event{Kind: 1, ID: "dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962", PubKey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", CreatedAt: 1644271588, Tags: Tags{}, Content: "now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?", Sig: "230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524"}}, ExpectedEnvelope: &EventEnvelope{SubscriptionID: ptr("_"), Event: Event{Kind: 1, ID: "dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962", PubKey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", CreatedAt: 1644271588, Tags: Tags{}, Content: "now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?", Sig: "230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524"}},
}, },
{ {
Name: "EVENT envelope without subscription id", Name: "EVENT envelope without subscription id",
Message: []byte(`["EVENT",{"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"}]`), Message: `["EVENT",{"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"}]`,
ExpectedEnvelope: &EventEnvelope{Event: Event{Kind: 1, ID: "dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962", PubKey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", CreatedAt: 1644271588, Tags: Tags{}, Content: "now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?", Sig: "230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524"}}, ExpectedEnvelope: &EventEnvelope{Event: Event{Kind: 1, ID: "dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962", PubKey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", CreatedAt: 1644271588, Tags: Tags{}, Content: "now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?", Sig: "230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524"}},
}, },
{ {
Name: "EVENT envelope with tags", Name: "EVENT envelope with tags",
Message: []byte(`["EVENT",{"kind":3,"id":"9e662bdd7d8abc40b5b15ee1ff5e9320efc87e9274d8d440c58e6eed2dddfbe2","pubkey":"373ebe3d45ec91977296a178d9f19f326c70631d2a1b0bbba5c5ecc2eb53b9e7","created_at":1644844224,"tags":[["p","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"],["e","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"}]`), Message: `["EVENT",{"kind":3,"id":"9e662bdd7d8abc40b5b15ee1ff5e9320efc87e9274d8d440c58e6eed2dddfbe2","pubkey":"373ebe3d45ec91977296a178d9f19f326c70631d2a1b0bbba5c5ecc2eb53b9e7","created_at":1644844224,"tags":[["p","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"],["e","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"}]`,
ExpectedEnvelope: &EventEnvelope{Event: Event{Kind: 3, ID: "9e662bdd7d8abc40b5b15ee1ff5e9320efc87e9274d8d440c58e6eed2dddfbe2", PubKey: "373ebe3d45ec91977296a178d9f19f326c70631d2a1b0bbba5c5ecc2eb53b9e7", CreatedAt: 1644844224, Tags: Tags{Tag{"p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"}, Tag{"e", "75fc5ac2487363293bd27fb0d14fb966477d0f1dbc6361d37806a6a740eda91e"}, Tag{"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"}}, ExpectedEnvelope: &EventEnvelope{Event: Event{Kind: 3, ID: "9e662bdd7d8abc40b5b15ee1ff5e9320efc87e9274d8d440c58e6eed2dddfbe2", PubKey: "373ebe3d45ec91977296a178d9f19f326c70631d2a1b0bbba5c5ecc2eb53b9e7", CreatedAt: 1644844224, Tags: Tags{Tag{"p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"}, Tag{"e", "75fc5ac2487363293bd27fb0d14fb966477d0f1dbc6361d37806a6a740eda91e"}, Tag{"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"}},
}, },
{ {
Name: "NOTICE envelope", Name: "NOTICE envelope",
Message: []byte(`["NOTICE","kjasbdlasvdluiasvd\"kjasbdksab\\d"]`), Message: `["NOTICE","kjasbdlasvdluiasvd\"kjasbdksab\\d"]`,
ExpectedEnvelope: ptr(NoticeEnvelope("kjasbdlasvdluiasvd\"kjasbdksab\\d")), ExpectedEnvelope: ptr(NoticeEnvelope("kjasbdlasvdluiasvd\"kjasbdksab\\d")),
}, },
{ {
Name: "EOSE envelope", Name: "EOSE envelope",
Message: []byte(`["EOSE","kjasbdlasvdluiasvd\"kjasbdksab\\d"]`), Message: `["EOSE","kjasbdlasvdluiasvd\"kjasbdksab\\d"]`,
ExpectedEnvelope: ptr(EOSEEnvelope("kjasbdlasvdluiasvd\"kjasbdksab\\d")), ExpectedEnvelope: ptr(EOSEEnvelope("kjasbdlasvdluiasvd\"kjasbdksab\\d")),
}, },
{ {
Name: "COUNT envelope", Name: "COUNT envelope",
Message: []byte(`["COUNT","z",{"count":12}]`), Message: `["COUNT","z",{"count":12}]`,
ExpectedEnvelope: &CountEnvelope{SubscriptionID: "z", Count: ptr(int64(12))}, ExpectedEnvelope: &CountEnvelope{SubscriptionID: "z", Count: ptr(int64(12))},
}, },
{ {
Name: "COUNT envelope with HLL", Name: "COUNT envelope with HLL",
Message: []byte(`["COUNT","sub1",{"count":42, "hll": "0100000101000000000000040000000001020000000002000000000200000003000002040000000101020001010000000000000007000004010000000200040000020400000000000102000002000004010000010000000301000102030002000301000300010000070000000001000004000102010000000400010002000000000103000100010001000001040100020001000000000000010000020000000000030100000001000400010000000000000901010100000000040000000b030000010100010000010000010000000003000000000000010003000100020000000000010000010100000100000104000200030001000300000001000101000102"}]`), Message: `["COUNT","sub1",{"count":42, "hll": "0100000101000000000000040000000001020000000002000000000200000003000002040000000101020001010000000000000007000004010000000200040000020400000000000102000002000004010000010000000301000102030002000301000300010000070000000001000004000102010000000400010002000000000103000100010001000001040100020001000000000000010000020000000000030100000001000400010000000000000901010100000000040000000b030000010100010000010000010000000003000000000000010003000100020000000000010000010100000100000104000200030001000300000001000101000102"}]`,
ExpectedEnvelope: &CountEnvelope{SubscriptionID: "sub1", Count: ptr(int64(42)), HyperLogLog: []byte{1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 2, 4, 0, 0, 0, 1, 1, 2, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 4, 1, 0, 0, 0, 2, 0, 4, 0, 0, 2, 4, 0, 0, 0, 0, 0, 1, 2, 0, 0, 2, 0, 0, 4, 1, 0, 0, 1, 0, 0, 0, 3, 1, 0, 1, 2, 3, 0, 2, 0, 3, 1, 0, 3, 0, 1, 0, 0, 7, 0, 0, 0, 0, 1, 0, 0, 4, 0, 1, 2, 1, 0, 0, 0, 4, 0, 1, 0, 2, 0, 0, 0, 0, 1, 3, 0, 1, 0, 1, 0, 1, 0, 0, 1, 4, 1, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 3, 1, 0, 0, 0, 1, 0, 4, 0, 1, 0, 0, 0, 0, 0, 0, 9, 1, 1, 1, 0, 0, 0, 0, 4, 0, 0, 0, 11, 3, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 1, 0, 3, 0, 1, 0, 2, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 4, 0, 2, 0, 3, 0, 1, 0, 3, 0, 0, 0, 1, 0, 1, 1, 0, 1, 2}}, ExpectedEnvelope: &CountEnvelope{SubscriptionID: "sub1", Count: ptr(int64(42)), HyperLogLog: []byte{1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 2, 4, 0, 0, 0, 1, 1, 2, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 4, 1, 0, 0, 0, 2, 0, 4, 0, 0, 2, 4, 0, 0, 0, 0, 0, 1, 2, 0, 0, 2, 0, 0, 4, 1, 0, 0, 1, 0, 0, 0, 3, 1, 0, 1, 2, 3, 0, 2, 0, 3, 1, 0, 3, 0, 1, 0, 0, 7, 0, 0, 0, 0, 1, 0, 0, 4, 0, 1, 2, 1, 0, 0, 0, 4, 0, 1, 0, 2, 0, 0, 0, 0, 1, 3, 0, 1, 0, 1, 0, 1, 0, 0, 1, 4, 1, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 3, 1, 0, 0, 0, 1, 0, 4, 0, 1, 0, 0, 0, 0, 0, 0, 9, 1, 1, 1, 0, 0, 0, 0, 4, 0, 0, 0, 11, 3, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 1, 0, 3, 0, 1, 0, 2, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 4, 0, 2, 0, 3, 0, 1, 0, 3, 0, 0, 0, 1, 0, 1, 1, 0, 1, 2}},
}, },
{ {
Name: "OK envelope success", Name: "OK envelope success",
Message: []byte(`["OK","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa",true,""]`), Message: `["OK","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa",true,""]`,
ExpectedEnvelope: &OKEnvelope{EventID: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa", OK: true, Reason: ""}, ExpectedEnvelope: &OKEnvelope{EventID: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa", OK: true, Reason: ""},
}, },
{ {
Name: "OK envelope failure", Name: "OK envelope failure",
Message: []byte(`["OK","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa",false,"error: could not connect to the database"]`), Message: `["OK","3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa",false,"error: could not connect to the database"]`,
ExpectedEnvelope: &OKEnvelope{EventID: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa", OK: false, Reason: "error: could not connect to the database"}, ExpectedEnvelope: &OKEnvelope{EventID: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefaaaaa", OK: false, Reason: "error: could not connect to the database"},
}, },
{ {
Name: "CLOSED envelope with underscore", Name: "CLOSED envelope with underscore",
Message: []byte(`["CLOSED","_","error: something went wrong"]`), Message: `["CLOSED","_","error: something went wrong"]`,
ExpectedEnvelope: &ClosedEnvelope{SubscriptionID: "_", Reason: "error: something went wrong"}, ExpectedEnvelope: &ClosedEnvelope{SubscriptionID: "_", Reason: "error: something went wrong"},
}, },
{ {
Name: "CLOSED envelope with colon", Name: "CLOSED envelope with colon",
Message: []byte(`["CLOSED",":1","auth-required: take a selfie and send it to the CIA"]`), Message: `["CLOSED",":1","auth-required: take a selfie and send it to the CIA"]`,
ExpectedEnvelope: &ClosedEnvelope{SubscriptionID: ":1", Reason: "auth-required: take a selfie and send it to the CIA"}, ExpectedEnvelope: &ClosedEnvelope{SubscriptionID: ":1", Reason: "auth-required: take a selfie and send it to the CIA"},
}, },
{ {
Name: "AUTH envelope with challenge", Name: "AUTH envelope with challenge",
Message: []byte(`["AUTH","kjsabdlasb aslkd kasndkad \"as.kdnbskadb"]`), Message: `["AUTH","kjsabdlasb aslkd kasndkad \"as.kdnbskadb"]`,
ExpectedEnvelope: &AuthEnvelope{Challenge: ptr("kjsabdlasb aslkd kasndkad \"as.kdnbskadb")}, ExpectedEnvelope: &AuthEnvelope{Challenge: ptr("kjsabdlasb aslkd kasndkad \"as.kdnbskadb")},
}, },
{ {
Name: "AUTH envelope with event", Name: "AUTH envelope with event",
Message: []byte(`["AUTH",{"kind":1,"id":"ae1fc7154296569d87ca4663f6bdf448c217d1590d28c85d158557b8b43b4d69","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1683660344,"tags":[],"content":"hello world","sig":"94e10947814b1ebe38af42300ecd90c7642763896c4f69506ae97bfdf54eec3c0c21df96b7d95daa74ff3d414b1d758ee95fc258125deebc31df0c6ba9396a51"}]`), Message: `["AUTH",{"kind":1,"id":"ae1fc7154296569d87ca4663f6bdf448c217d1590d28c85d158557b8b43b4d69","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1683660344,"tags":[],"content":"hello world","sig":"94e10947814b1ebe38af42300ecd90c7642763896c4f69506ae97bfdf54eec3c0c21df96b7d95daa74ff3d414b1d758ee95fc258125deebc31df0c6ba9396a51"}]`,
ExpectedEnvelope: &AuthEnvelope{Event: Event{Kind: 1, ID: "ae1fc7154296569d87ca4663f6bdf448c217d1590d28c85d158557b8b43b4d69", PubKey: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", CreatedAt: 1683660344, Tags: Tags{}, Content: "hello world", Sig: "94e10947814b1ebe38af42300ecd90c7642763896c4f69506ae97bfdf54eec3c0c21df96b7d95daa74ff3d414b1d758ee95fc258125deebc31df0c6ba9396a51"}}, ExpectedEnvelope: &AuthEnvelope{Event: Event{Kind: 1, ID: "ae1fc7154296569d87ca4663f6bdf448c217d1590d28c85d158557b8b43b4d69", PubKey: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", CreatedAt: 1683660344, Tags: Tags{}, Content: "hello world", Sig: "94e10947814b1ebe38af42300ecd90c7642763896c4f69506ae97bfdf54eec3c0c21df96b7d95daa74ff3d414b1d758ee95fc258125deebc31df0c6ba9396a51"}},
}, },
{ {
Name: "REQ envelope", Name: "REQ envelope",
Message: []byte(`["REQ","million", {"kinds": [1]}, {"kinds": [30023 ], "#d": ["buteko", "batuke"]}]`), Message: `["REQ","million", {"kinds": [1]}, {"kinds": [30023 ], "#d": ["buteko", "batuke"]}]`,
ExpectedEnvelope: &ReqEnvelope{SubscriptionID: "million", Filters: Filters{{Kinds: []int{1}}, {Kinds: []int{30023}, Tags: TagMap{"d": []string{"buteko", "batuke"}}}}}, ExpectedEnvelope: &ReqEnvelope{SubscriptionID: "million", Filters: Filters{{Kinds: []int{1}}, {Kinds: []int{30023}, Tags: TagMap{"d": []string{"buteko", "batuke"}}}}},
}, },
{ {
Name: "CLOSE envelope", Name: "CLOSE envelope",
Message: []byte(`["CLOSE","subscription123"]`), Message: `["CLOSE","subscription123"]`,
ExpectedEnvelope: ptr(CloseEnvelope("subscription123")), ExpectedEnvelope: ptr(CloseEnvelope("subscription123")),
}, },
} }
@ -168,8 +170,8 @@ func TestParseMessagesFromFile(t *testing.T) {
continue continue
} }
standardEnvelope := ParseMessage(line) standardEnvelope := ParseMessage(string(line))
sonicEnvelope, err := smp.ParseMessage(line) sonicEnvelope, err := smp.ParseMessage(string(line))
if standardEnvelope == nil { if standardEnvelope == nil {
require.Nil(t, sonicEnvelope, "line %d: standard parser returned nil but sonic parser didn't", lineNum) require.Nil(t, sonicEnvelope, "line %d: standard parser returned nil but sonic parser didn't", lineNum)

View File

@ -1,7 +1,6 @@
package nostr package nostr
import ( import (
"bytes"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -119,35 +118,39 @@ func isLowerHex(thing string) bool {
return true return true
} }
func extractSubID(jsonStr []byte) string { func extractSubID(jsonStr string) string {
// look for "EVENT" pattern // look for "EVENT" pattern
start := bytes.Index(jsonStr, []byte(`"EVENT"`)) start := strings.Index(jsonStr, `"EVENT"`)
if start == -1 { if start == -1 {
return "" return ""
} }
// move to the next quote // move to the next quote
offset := bytes.Index(jsonStr[start+7:], []byte{'"'}) offset := strings.Index(jsonStr[start+7:], `"`)
if offset == -1 {
return ""
}
start += 7 + offset + 1 start += 7 + offset + 1
// find the ending quote // find the ending quote
end := bytes.Index(jsonStr[start:], []byte{'"'}) end := strings.Index(jsonStr[start:], `"`)
// get the contents // get the contents
return unsafe.String(unsafe.SliceData(jsonStr[start:start+end]), end) return jsonStr[start : start+end]
} }
func extractEventID(jsonStr []byte) string { func extractEventID(jsonStr string) string {
// look for "id": pattern // look for "id" pattern
start := bytes.Index(jsonStr, []byte(`"id":`)) start := strings.Index(jsonStr, `"id"`)
if start == -1 { if start == -1 {
return "" return ""
} }
// move to the next quote // move to the next quote
offset := bytes.Index(jsonStr[start+4:], []byte{'"'}) offset := strings.IndexRune(jsonStr[start+4:], '"')
start += 4 + offset + 1 start += 4 + offset + 1
// get 64 characters of the id // get 64 characters of the id
return unsafe.String(unsafe.SliceData(jsonStr[start:start+64]), 64) return jsonStr[start : start+64]
} }

View File

@ -27,18 +27,18 @@ func TestIsLower(t *testing.T) {
func TestIDExtract(t *testing.T) { func TestIDExtract(t *testing.T) {
{ {
data := []byte(`{"kind":1,"id":"6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16","pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at":1736909072,"tags":[["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638"}`) data := `{"kind":1,"id":"6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16","pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at":1736909072,"tags":[["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638"}`
require.Equal(t, "6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16", extractEventID(data)) require.Equal(t, "6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16", extractEventID(data))
} }
{ {
data := []byte(`{"kind":1,"pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at":1736909072,"tags":[["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638","id": "6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16" }`) data := `{"kind":1,"pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at":1736909072,"tags":[["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638","id": "6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16" }`
require.Equal(t, "6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16", extractEventID(data)) require.Equal(t, "6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16", extractEventID(data))
} }
} }
func TestSubIdExtract(t *testing.T) { func TestSubIdExtract(t *testing.T) {
{ {
data := []byte(`["EVENT", "xxz" ,{"kind":1,"id":"6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16","pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at":1736909072,"tags":[["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638"}]`) data := `["EVENT", "xxz" ,{"kind":1,"id":"6b5988e9471fa340880a40df815befc69c901420facfb670acd8308012088f16","pubkey":"67ada8e344532cbf82f0e702472e24c7896e0e1c96235eacbaaa4b8616052171","created_at":1736909072,"tags":[["e","cfdf18b78527455097515545be4ccbe17e9b88f64539a566c632e405e2c0d08a","","root"],["e","f1ec9c301383be082f1860f7e24e49164d855bfab67f8e5c3ed17f6f3f867cca","","reply"],["p","1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d"],["p","8aa642e26e65072139e10db59646a89aa7538a59965aab3ed89191d71967d6c3"],["p","f4d89779148ccd245c8d50914a284fd62d97cb0fb68b797a70f24a172b522db9"],["p","18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08"],["p","9a0e2043afaa056a12b8bbe77ac4c3185c0e2bc46b12aac158689144323c0e3c"],["p","45f195cffcb8c9724efc248f0507a2fb65b579dfabe7cd35398598163cab7627"]],"content":"🫡","sig":"d21aaf43963b07a3cb5f85ac8809c2b2e4dd3269195f4d810e1b7650895178fe01cf685ab3ee93f193cdde1f8d17419ff05332c6e3fc7429bbbe3d70016b8638"}]`
require.Equal(t, "xxz", extractSubID(data)) require.Equal(t, "xxz", extractSubID(data))
} }
} }

View File

@ -3,6 +3,8 @@ package nip77
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"strings"
"unsafe"
"github.com/mailru/easyjson" "github.com/mailru/easyjson"
jwriter "github.com/mailru/easyjson/jwriter" jwriter "github.com/mailru/easyjson/jwriter"
@ -10,28 +12,28 @@ import (
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
func ParseNegMessage(message []byte) nostr.Envelope { func ParseNegMessage(message string) nostr.Envelope {
firstComma := bytes.Index(message, []byte{','}) firstComma := strings.Index(message, ",")
if firstComma == -1 { if firstComma == -1 {
return nil return nil
} }
label := message[0:firstComma] label := message[0:firstComma]
var v nostr.Envelope var v nostr.Envelope
switch { switch label {
case bytes.Contains(label, []byte("NEG-MSG")): case "NEG-MSG":
v = &MessageEnvelope{} v = &MessageEnvelope{}
case bytes.Contains(label, []byte("NEG-OPEN")): case "NEG-OPEN":
v = &OpenEnvelope{} v = &OpenEnvelope{}
case bytes.Contains(label, []byte("NEG-ERR")): case "NEG-ERR":
v = &ErrorEnvelope{} v = &ErrorEnvelope{}
case bytes.Contains(label, []byte("NEG-CLOSE")): case "NEG-CLOSE":
v = &CloseEnvelope{} v = &CloseEnvelope{}
default: default:
return nil return nil
} }
if err := v.UnmarshalJSON(message); err != nil { if err := v.FromJSON(message); err != nil {
return nil return nil
} }
return v return v
@ -56,8 +58,8 @@ func (v OpenEnvelope) String() string {
return string(b) return string(b)
} }
func (v *OpenEnvelope) UnmarshalJSON(data []byte) error { func (v *OpenEnvelope) FromJSON(data string) error {
r := gjson.ParseBytes(data) r := gjson.Parse(data)
arr := r.Array() arr := r.Array()
if len(arr) != 4 { if len(arr) != 4 {
return fmt.Errorf("failed to decode NEG-OPEN envelope") return fmt.Errorf("failed to decode NEG-OPEN envelope")
@ -65,7 +67,7 @@ func (v *OpenEnvelope) UnmarshalJSON(data []byte) error {
v.SubscriptionID = arr[1].Str v.SubscriptionID = arr[1].Str
v.Message = arr[3].Str v.Message = arr[3].Str
return easyjson.Unmarshal([]byte(arr[2].Raw), &v.Filter) return easyjson.Unmarshal(unsafe.Slice(unsafe.StringData(arr[2].Raw), len(arr[2].Raw)), &v.Filter)
} }
func (v OpenEnvelope) MarshalJSON() ([]byte, error) { func (v OpenEnvelope) MarshalJSON() ([]byte, error) {
@ -97,8 +99,8 @@ func (v MessageEnvelope) String() string {
return string(b) return string(b)
} }
func (v *MessageEnvelope) UnmarshalJSON(data []byte) error { func (v *MessageEnvelope) FromJSON(data string) error {
r := gjson.ParseBytes(data) r := gjson.Parse(data)
arr := r.Array() arr := r.Array()
if len(arr) < 3 { if len(arr) < 3 {
return fmt.Errorf("failed to decode NEG-MSG envelope") return fmt.Errorf("failed to decode NEG-MSG envelope")
@ -130,8 +132,8 @@ func (v CloseEnvelope) String() string {
return string(b) return string(b)
} }
func (v *CloseEnvelope) UnmarshalJSON(data []byte) error { func (v *CloseEnvelope) FromJSON(data string) error {
r := gjson.ParseBytes(data) r := gjson.Parse(data)
arr := r.Array() arr := r.Array()
if len(arr) < 2 { if len(arr) < 2 {
return fmt.Errorf("failed to decode NEG-CLOSE envelope") return fmt.Errorf("failed to decode NEG-CLOSE envelope")
@ -159,8 +161,8 @@ func (v ErrorEnvelope) String() string {
return string(b) return string(b)
} }
func (v *ErrorEnvelope) UnmarshalJSON(data []byte) error { func (v *ErrorEnvelope) FromJSON(data string) error {
r := gjson.ParseBytes(data) r := gjson.Parse(data)
arr := r.Array() arr := r.Array()
if len(arr) < 3 { if len(arr) < 3 {
return fmt.Errorf("failed to decode NEG-ERROR envelope") return fmt.Errorf("failed to decode NEG-ERROR envelope")

View File

@ -20,7 +20,7 @@ func FetchIDsOnly(
result := make(chan error) result := make(chan error)
var r *nostr.Relay var r *nostr.Relay
r, err := nostr.RelayConnect(ctx, url, nostr.WithCustomHandler(func(data []byte) { r, err := nostr.RelayConnect(ctx, url, nostr.WithCustomHandler(func(data string) {
envelope := ParseNegMessage(data) envelope := ParseNegMessage(data)
if envelope == nil { if envelope == nil {
return return

View File

@ -49,7 +49,7 @@ func NegentropySync(
result := make(chan error) result := make(chan error)
var r *nostr.Relay var r *nostr.Relay
r, err = nostr.RelayConnect(ctx, url, nostr.WithCustomHandler(func(data []byte) { r, err = nostr.RelayConnect(ctx, url, nostr.WithCustomHandler(func(data string) {
envelope := ParseNegMessage(data) envelope := ParseNegMessage(data)
if envelope == nil { if envelope == nil {
return return

View File

@ -13,6 +13,7 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"unsafe"
"github.com/puzpuzpuz/xsync/v3" "github.com/puzpuzpuz/xsync/v3"
) )
@ -35,7 +36,7 @@ type Relay struct {
challenge string // NIP-42 challenge, we only keep the last challenge string // NIP-42 challenge, we only keep the last
noticeHandler func(string) // NIP-01 NOTICEs noticeHandler func(string) // NIP-01 NOTICEs
customHandler func([]byte) // nonstandard unparseable messages customHandler func(string) // nonstandard unparseable messages
okCallbacks *xsync.MapOf[string, func(bool, string)] okCallbacks *xsync.MapOf[string, func(bool, string)]
writeQueue chan writeRequest writeQueue chan writeRequest
subscriptionChannelCloseQueue chan *Subscription subscriptionChannelCloseQueue chan *Subscription
@ -104,7 +105,7 @@ func (nh WithNoticeHandler) ApplyRelayOption(r *Relay) {
// WithCustomHandler must be a function that handles any relay message that couldn't be // WithCustomHandler must be a function that handles any relay message that couldn't be
// parsed as a standard envelope. // parsed as a standard envelope.
type WithCustomHandler func(data []byte) type WithCustomHandler func(data string)
func (ch WithCustomHandler) ApplyRelayOption(r *Relay) { func (ch WithCustomHandler) ApplyRelayOption(r *Relay) {
r.customHandler = ch r.customHandler = ch
@ -212,6 +213,7 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
// general message reader loop // general message reader loop
go func() { go func() {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
mp := NewMessageParser()
for { for {
buf.Reset() buf.Reset()
@ -222,7 +224,8 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
break break
} }
message := buf.Bytes() msgb := buf.Bytes()
message := unsafe.String(unsafe.SliceData(msgb), len(msgb))
debugLogf("{%s} received %v\n", r.URL, message) debugLogf("{%s} received %v\n", r.URL, message)
// if this is an "EVENT" we will have this preparser logic that should speed things up a little // if this is an "EVENT" we will have this preparser logic that should speed things up a little
@ -235,9 +238,9 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
} }
} }
envelope := ParseMessage(message) envelope, err := mp.ParseMessage(message)
if envelope == nil { if envelope == nil {
if r.customHandler != nil { if r.customHandler != nil && err == UnknownLabel {
r.customHandler(message) r.customHandler(message)
} }
continue continue
@ -258,13 +261,13 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
r.challenge = *env.Challenge r.challenge = *env.Challenge
case *EventEnvelope: case *EventEnvelope:
// we already have the subscription from the pre-check above, so we can just reuse it // we already have the subscription from the pre-check above, so we can just reuse it
if subscription == nil { if sub == nil {
// InfoLogger.Printf("{%s} no subscription with id '%s'\n", r.URL, *env.SubscriptionID) // InfoLogger.Printf("{%s} no subscription with id '%s'\n", r.URL, *env.SubscriptionID)
continue continue
} else { } else {
// check if the event matches the desired filter, ignore otherwise // check if the event matches the desired filter, ignore otherwise
if !subscription.match(&env.Event) { if !sub.match(&env.Event) {
InfoLogger.Printf("{%s} filter does not match: %v ~ %v\n", r.URL, subscription.Filters, env.Event) InfoLogger.Printf("{%s} filter does not match: %v ~ %v\n", r.URL, sub.Filters, env.Event)
continue continue
} }
@ -277,7 +280,7 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
} }
// dispatch this to the internal .events channel of the subscription // dispatch this to the internal .events channel of the subscription
subscription.dispatchEvent(&env.Event) sub.dispatchEvent(&env.Event)
} }
case *EOSEEnvelope: case *EOSEEnvelope:
if subscription, ok := r.Subscriptions.Load(subIdToSerial(string(*env))); ok { if subscription, ok := r.Subscriptions.Load(subIdToSerial(string(*env))); ok {