diff --git a/nson/nson.go b/nson/nson.go index 8a140d0..700dbc1 100644 --- a/nson/nson.go +++ b/nson/nson.go @@ -25,6 +25,25 @@ import ( "nson":"xxkkccccttnn111122223333nn11112222" */ +const ( + ID_START = 7 + ID_END = 7 + 64 + PUBKEY_START = 83 + PUBKEY_END = 83 + 64 + SIG_START = 156 + SIG_END = 156 + 128 + CREATED_AT_START = 299 + CREATED_AT_END = 299 + 10 + + NSON_STRING_START = 318 // the actual json string for the "nson" field + NSON_VALUES_START = 318 + 2 // skipping the first byte which delimits the nson size + + NSON_MARKER_START = 309 // this is used just to determine if an event is nson or not + NSON_MARKER_END = 317 // it's just the `,"nson":` (including ,": garbage to reduce false positives) part +) + +var NotNSON = fmt.Errorf("not nson") + // Unmarshal turns a NSON string into a nostr.Event struct func Unmarshal(data string) (evt *nostr.Event, err error) { defer func() { @@ -34,28 +53,26 @@ func Unmarshal(data string) (evt *nostr.Event, err error) { }() // check if it's nson - if data[311:315] != "nson" { - return nil, fmt.Errorf("not nson") + if data[NSON_MARKER_START:NSON_MARKER_END] != ",\"nson\":" { + return nil, NotNSON } // nson values - nsonSizeBytes, _ := hex.DecodeString(data[318 : 318+2]) - nsonSize := int(nsonSizeBytes[0]) * 2 // number of bytes is given, we x2 because the string is in hex - nsonDescriptors, _ := hex.DecodeString(data[320 : 320+nsonSize]) + nsonSize, nsonDescriptors := parseDescriptors(data) evt = &nostr.Event{} // static fields - evt.ID = data[7 : 7+64] - evt.PubKey = data[83 : 83+64] - evt.Sig = data[156 : 156+128] - ts, _ := strconv.ParseInt(data[299:299+10], 10, 64) + evt.ID = data[ID_START:ID_END] + evt.PubKey = data[PUBKEY_START:PUBKEY_END] + evt.Sig = data[SIG_START:SIG_END] + ts, _ := strconv.ParseUint(data[CREATED_AT_START:CREATED_AT_END], 10, 64) evt.CreatedAt = nostr.Timestamp(ts) // dynamic fields // kind kindChars := int(nsonDescriptors[0]) - kindStart := 320 + nsonSize + 9 // len(`","kind":`) + kindStart := NSON_VALUES_START + nsonSize + 9 // len(`","kind":`) evt.Kind, _ = strconv.Atoi(data[kindStart : kindStart+kindChars]) // content @@ -90,7 +107,7 @@ func Unmarshal(data string) (evt *nostr.Event, err error) { return evt, err } -func Marshal(evt *nostr.Event) (string, error) { +func Marshal(evt nostr.Event) (string, error) { // start building the nson descriptors (without the first byte that represents the nson size) nsonBuf := make([]byte, 256) @@ -136,7 +153,7 @@ func Marshal(evt *nostr.Event) (string, error) { // actually build the json base := strings.Builder{} - base.Grow(320 + // everything up to "nson": + base.Grow(NSON_VALUES_START + // everything up to "nson": 2 + len(nsonBuf)*2 + // nson 9 + kindChars + // kind and its label 12 + contentChars + // content and its label @@ -158,6 +175,13 @@ func Marshal(evt *nostr.Event) (string, error) { return base.String(), nil } +func parseDescriptors(data string) (int, []byte) { + nsonSizeBytes, _ := hex.DecodeString(data[NSON_STRING_START:NSON_VALUES_START]) + size := int(nsonSizeBytes[0]) * 2 // number of bytes is given, we x2 because the string is in hex + values, _ := hex.DecodeString(data[NSON_VALUES_START : NSON_VALUES_START+size]) + return size, values +} + // A nson.Event is basically a wrapper over the string that makes it easy to get each event property (except tags). type Event struct { data string @@ -172,25 +196,15 @@ func New(nsonText string) Event { func (ne *Event) parseDescriptors() { if ne.descriptors == nil { - nsonSizeBytes, _ := hex.DecodeString(ne.data[318 : 318+2]) - ne.descriptorsSize = int(nsonSizeBytes[0]) - ne.descriptors, _ = hex.DecodeString(ne.data[320 : 320+ne.descriptorsSize]) + ne.descriptorsSize, ne.descriptors = parseDescriptors(ne.data) } } -func (ne *Event) parseKind() { - if ne.descriptors == nil { - nsonSizeBytes, _ := hex.DecodeString(ne.data[318 : 318+2]) - ne.descriptorsSize = int(nsonSizeBytes[0]) - ne.descriptors, _ = hex.DecodeString(ne.data[320 : 320+ne.descriptorsSize]) - } -} - -func (ne Event) GetID() string { return ne.data[7 : 7+64] } -func (ne Event) GetPubkey() string { return ne.data[83 : 83+64] } -func (ne Event) GetSig() string { return ne.data[156 : 156+128] } +func (ne Event) GetID() string { return ne.data[ID_START:ID_END] } +func (ne Event) GetPubkey() string { return ne.data[PUBKEY_START:PUBKEY_END] } +func (ne Event) GetSig() string { return ne.data[SIG_START:SIG_END] } func (ne Event) GetCreatedAt() nostr.Timestamp { - ts, _ := strconv.ParseInt(ne.data[299:299+10], 10, 64) + ts, _ := strconv.ParseUint(ne.data[CREATED_AT_START:CREATED_AT_END], 10, 64) return nostr.Timestamp(ts) } @@ -198,7 +212,7 @@ func (ne *Event) GetKind() int { ne.parseDescriptors() kindChars := int(ne.descriptors[0]) - kindStart := 320 + ne.descriptorsSize + 9 // len(`","kind":`) + kindStart := NSON_VALUES_START + ne.descriptorsSize + 9 // len(`","kind":`) kind, _ := strconv.Atoi(ne.data[kindStart : kindStart+kindChars]) return kind @@ -208,7 +222,7 @@ func (ne *Event) GetContent() string { ne.parseDescriptors() kindChars := int(ne.descriptors[0]) - kindStart := 320 + ne.descriptorsSize + 9 // len(`","kind":`) + kindStart := NSON_VALUES_START + ne.descriptorsSize + 9 // len(`","kind":`) contentChars := int(binary.BigEndian.Uint16(ne.descriptors[1:3])) contentStart := kindStart + kindChars + 12 // len(`,"content":"`) diff --git a/nson/nson_test.go b/nson/nson_test.go index fc3ce2f..7c6dc37 100644 --- a/nson/nson_test.go +++ b/nson/nson_test.go @@ -9,9 +9,7 @@ import ( ) var nsonTestEvents = []string{ - `{"id":"ae1fc7154296569d87ca4663f6bdf448c217d1590d28c85d158557b8b43b4d69","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","sig":"94e10947814b1ebe38af42300ecd90c7642763896c4f69506ae97bfdf54eec3c0c21df96b7d95daa74ff3d414b1d758ee95fc258125deebc31df0c6ba9396a51","created_at":1683660344,"nson":"1405000b0203000100400005040001004000000014","kind":30023,"content":"hello hello","tags":[["e","b6de44a9dd47d1c000f795ea0453046914f44ba7d5e369608b04867a575ea83e","reply"],["p","c26f7b252cea77a5b94f42b1a4771021be07d4df766407e47738605f7e3ab774","","wss://relay.damus.io"]]}`, - `{"id":"ae1fc7154296569d87ca4663f6bdf448c217d1590d28c85d158557b8b43b4d69","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","sig":"94e10947814b1ebe38af42300ecd90c7642763896c4f69506ae97bfdf54eec3c0c21df96b7d95daa74ff3d414b1d758ee95fc258125deebc31df0c6ba9396a51","created_at":1683660344,"nson":"140500100203000100400005040001004000000014","kind":30023,"content":"hello\n\"hello\"","tags":[["e","b6de44a9dd47d1c000f795ea0453046914f44ba7d5e369608b04867a575ea83e","reply"],["p","c26f7b252cea77a5b94f42b1a4771021be07d4df766407e47738605f7e3ab774","","wss://relay.damus.io"]]}`, - `{"id":"a235323ad6ae7032667330c4d52def2d6be67a973d71f2f1784b2b5b01d57026","pubkey":"69aeace80672c08ef7729a03e597ed4e9dd5ddaa7c457349d55d12c043e8a7ab","sig":"7e5fedc3c1c16abb95d207b73a689b5f17ab039cffd0b6bea62dcbfb607a27d38c830542df5ce762685a0da4e8edd28beab0c9a8f47e3037ff6e676ea6297bfa","created_at":1680277541,"nson":"0401049600","kind":1,"content":"Hello #Plebstrs šŸ¤™\n\nExcited to share with you the latest improvements we've designed to enhance your user experience when creating new posts on #Plebstr.\n\nMain UX improvements include:\n— The ability to mention anyone directly in your new post šŸ”\n— Real-time previews of all attachments while creating your post \U0001fa7b\n— Minimizing the new post window to enable #multitasking and easy access to your draft šŸ“\n\nThis is the first design mockup and we can't wait to bring it to you in an upcoming updates. Our amazing developers are currently working on it, together with other small improvements. Stay tuned! šŸš€šŸ‘Ø\u200dšŸ’»\n\n*Some details may change so keep your fingers crossed šŸ˜„šŸ¤ž\n\n#comingsoon #maybe #insomeshapeorform\n\nhttps://nostr.build/i/nostr.build_2b337d24f0cd19eff0678893ac93d58ee374ca8c3b9215516aa76a05856ec9c0.png\nhttps://nostr.build/i/nostr.build_f79b14687a1f6e7275192554722a85be815219573d824e381c4913715644e10d.png\nhttps://nostr.build/i/nostr.build_009fa068a32383f88e19763fa22e16142cf44166cb097293f7082e7bf4a38eed.png\nhttps://nostr.build/i/nostr.build_47ecdd4867808b3f3af2620ac4bf40aefcc9022c4ba26762567012c22d636487.png","tags":[]}}`, + `{"id":"192eaf31bd20476bbe9265a3667cfef6410dfd563c02a64cb15d6fa8efec0ed6","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","sig":"5b9051596a5ba0619fd5fd7d2766b8aeb0cc398f1d1a0804f4b4ed884482025b3d4888e4c892f2fc437415bfc121482a990fad30f5cd9e333e55364052f99bbc","created_at":1688505641,"nson":"0401000500","kind":1,"content":"hello","tags":[]}`, } func TestBasicNsonParse(t *testing.T) { @@ -24,60 +22,29 @@ func TestBasicNsonParse(t *testing.T) { func TestNsonPartialGet(t *testing.T) { for _, jevt := range nsonTestEvents { evt, _ := Unmarshal(jevt) - wrapper := New(jevt) if id := wrapper.GetID(); id != evt.ID { - t.Error("partial id wrong") + t.Errorf("partial id wrong. got %v, expected %v", id, evt.ID) } if pubkey := wrapper.GetPubkey(); pubkey != evt.PubKey { - t.Error("partial pubkey wrong") + t.Errorf("partial pubkey wrong. got %v, expected %v", pubkey, evt.PubKey) } if sig := wrapper.GetSig(); sig != evt.Sig { - t.Error("partial sig wrong") + t.Errorf("partial sig wrong. got %v, expected %v", sig, evt.Sig) } if createdAt := wrapper.GetCreatedAt(); createdAt != evt.CreatedAt { - t.Error("partial created_at wrong") + t.Errorf("partial created_at wrong. got %v, expected %v", createdAt, evt.CreatedAt) } if kind := wrapper.GetKind(); kind != evt.Kind { - t.Error("partial kind wrong") + t.Errorf("partial kind wrong. got %v, expected %v", kind, evt.Kind) } if content := wrapper.GetContent(); content != evt.Content { - t.Error("partial content wrong") + t.Errorf("partial content wrong. got %v, expected %v", content, evt.Content) } } } -func TestEncodeNson(t *testing.T) { - jevt := `{ - "content": "hello world", - "created_at": 1683762317, - "id": "57ff66490a6a2af3992accc26ae95f3f60c6e5f84ed0ddf6f59c534d3920d3d2", - "kind": 1, - "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - "sig": "504d142aed7fa7e0f6dab5bcd7eed63963b0277a8e11bbcb03b94531beb4b95a12f1438668b02746bd5362161bc782068e6b71494060975414e793f9e19f57ea", - "tags": [ - [ - "e", - "b6de44a9dd47d1c000f795ea0453046914f44ba7d5e369608b04867a575ea83e", - "reply" - ], - [ - "p", - "c26f7b252cea77a5b94f42b1a4771021be07d4df766407e47738605f7e3ab774", - "", - "wss://relay.damus.io" - ] - ] -}` - - evt := &nostr.Event{} - json.Unmarshal([]byte(jevt), evt) - - nevt, _ := Marshal(evt) - fmt.Println(nevt) -} - func checkParsedCorrectly(t *testing.T, evt *nostr.Event, jevt string) (isBad bool) { var canonical nostr.Event err := json.Unmarshal([]byte(jevt), &canonical)