From b0031bfd8600401fa5335e84b60eaeea7adc570d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 27 Mar 2023 08:47:59 -0300 Subject: [PATCH] sdk.ParseReferences() --- sdk/references.go | 116 +++++++++++++++++++++++++++++++++++++++++ sdk/references_test.go | 92 ++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 sdk/references.go create mode 100644 sdk/references_test.go diff --git a/sdk/references.go b/sdk/references.go new file mode 100644 index 0000000..80b730f --- /dev/null +++ b/sdk/references.go @@ -0,0 +1,116 @@ +package sdk + +import ( + "regexp" + "strconv" + "strings" + + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" +) + +type Reference struct { + Text string + Profile *nostr.ProfilePointer + Event *nostr.EventPointer + Entity *nostr.EntityPointer +} + +var mentionRegex = regexp.MustCompile(`\bnostr:((note|npub|naddr|nevent|nprofile)1\w+)\b|#\[(\d+)\]`) + +// ParseReferences parses both NIP-08 and NIP-27 references in a single unifying interface. +func ParseReferences(evt *nostr.Event) []*Reference { + var references []*Reference + for _, ref := range mentionRegex.FindAllStringSubmatch(evt.Content, -1) { + if ref[2] != "" { + // it's a NIP-27 mention + if prefix, data, err := nip19.Decode(ref[1]); err == nil { + switch prefix { + case "npub": + references = append(references, &Reference{ + Text: ref[0], + Profile: &nostr.ProfilePointer{ + PublicKey: data.(string), Relays: []string{}, + }, + }) + case "nprofile": + pp := data.(nostr.ProfilePointer) + references = append(references, &Reference{ + Text: ref[0], + Profile: &pp, + }) + case "note": + references = append(references, &Reference{ + Text: ref[0], + Event: &nostr.EventPointer{ID: data.(string), Relays: []string{}}, + }) + case "nevent": + evp := data.(nostr.EventPointer) + references = append(references, &Reference{ + Text: ref[0], + Event: &evp, + }) + case "naddr": + addr := data.(nostr.EntityPointer) + references = append(references, &Reference{ + Text: ref[0], + Entity: &addr, + }) + } + } + } else if ref[3] != "" { + // it's a NIP-10 mention + idx, err := strconv.Atoi(ref[3]) + if err != nil || len(evt.Tags) <= idx { + continue + } + if tag := evt.Tags[idx]; tag != nil { + switch tag[0] { + case "p": + relays := make([]string, 0, 1) + if len(tag) > 2 && tag[2] != "" { + relays = append(relays, tag[2]) + } + references = append(references, &Reference{ + Text: ref[0], + Profile: &nostr.ProfilePointer{ + PublicKey: tag[1], + Relays: relays, + }, + }) + case "e": + relays := make([]string, 0, 1) + if len(tag) > 2 && tag[2] != "" { + relays = append(relays, tag[2]) + } + references = append(references, &Reference{ + Text: ref[0], + Event: &nostr.EventPointer{ + ID: tag[1], + Relays: relays, + }, + }) + case "a": + if parts := strings.Split(ref[1], ":"); len(parts) == 3 { + kind, _ := strconv.Atoi(parts[0]) + relays := make([]string, 0, 1) + if len(tag) > 2 && tag[2] != "" { + relays = append(relays, tag[2]) + } + references = append(references, &Reference{ + Text: ref[0], + Entity: &nostr.EntityPointer{ + Identifier: parts[2], + PublicKey: parts[1], + Kind: kind, + Relays: relays, + }, + }) + } + } + } + } + } + + return references +} diff --git a/sdk/references_test.go b/sdk/references_test.go new file mode 100644 index 0000000..9f3b0a8 --- /dev/null +++ b/sdk/references_test.go @@ -0,0 +1,92 @@ +package sdk + +import ( + "fmt" + "testing" + + "github.com/nbd-wtf/go-nostr" +) + +func TestParseReferences(t *testing.T) { + evt := nostr.Event{ + Tags: nostr.Tags{ + {"p", "c9d556c6d3978d112d30616d0d20aaa81410e3653911dd67787b5aaf9b36ade8", "wss://nostr.com"}, + {"e", "a84c5de86efc2ec2cff7bad077c4171e09146b633b7ad117fffe088d9579ac33", "wss://other.com", "reply"}, + {"e", "31d7c2875b5fc8e6f9c8f9dc1f84de1b6b91d1947ea4c59225e55c325d330fa8", ""}, + }, + Content: "hello #[0], have you seen #[2]? it was made by nostr:nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg on nostr:nevent1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8ychxp5v4! broken #[3]", + } + + expected := []Reference{ + { + Text: "#[0]", + Profile: &nostr.ProfilePointer{ + PublicKey: "c9d556c6d3978d112d30616d0d20aaa81410e3653911dd67787b5aaf9b36ade8", + Relays: []string{"wss://nostr.com"}, + }, + }, + { + Text: "#[2]", + Event: &nostr.EventPointer{ + ID: "31d7c2875b5fc8e6f9c8f9dc1f84de1b6b91d1947ea4c59225e55c325d330fa8", + Relays: []string{}, + }, + }, + { + Text: "nostr:nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg", + Profile: &nostr.ProfilePointer{ + PublicKey: "cc6b9fea033f59c3c39a0407c5f1bfee439b077508d918cfdc0d6fd431d39393", + Relays: []string{}, + }, + }, + { + Text: "nostr:nevent1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8ychxp5v4", + Event: &nostr.EventPointer{ + ID: "cc6b9fea033f59c3c39a0407c5f1bfee439b077508d918cfdc0d6fd431d39393", + Relays: []string{}, + Author: "", + }, + }, + } + + got := ParseReferences(&evt) + + if len(got) != len(expected) { + t.Errorf("got %d references, expected %d", len(got), len(expected)) + } + + for i, g := range got { + e := expected[i] + if g.Text != e.Text { + t.Errorf("%d: got text %s, expected %s", i, g.Text, e.Text) + } + + if (g.Entity == nil && e.Entity != nil) || + (g.Event == nil && e.Event != nil) || + (g.Profile == nil && e.Profile != nil) { + t.Errorf("%d: got some unexpected nil", i) + } + + if g.Profile != nil && (g.Profile.PublicKey != e.Profile.PublicKey || + len(g.Profile.Relays) != len(e.Profile.Relays) || + (len(g.Profile.Relays) > 0 && g.Profile.Relays[0] != e.Profile.Relays[0])) { + t.Errorf("%d: profile value is wrong", i) + } + + if g.Event != nil && (g.Event.ID != e.Event.ID || + g.Event.Author != e.Event.Author || + len(g.Event.Relays) != len(e.Event.Relays) || + (len(g.Event.Relays) > 0 && g.Event.Relays[0] != e.Event.Relays[0])) { + fmt.Println(g.Event.ID, g.Event.Relays, len(g.Event.Relays), g.Event.Relays[0] == "") + fmt.Println(e.Event.Relays, len(e.Event.Relays)) + t.Errorf("%d: event value is wrong", i) + } + + if g.Entity != nil && (g.Entity.PublicKey != e.Entity.PublicKey || + g.Entity.Identifier != e.Entity.Identifier || + g.Entity.Kind != e.Entity.Kind || + len(g.Entity.Relays) != len(g.Entity.Relays)) { + t.Errorf("%d: entity value is wrong", i) + } + } +}