diff --git a/nip27/references.go b/nip27/references.go new file mode 100644 index 0000000..d48b781 --- /dev/null +++ b/nip27/references.go @@ -0,0 +1,78 @@ +package nip27 + +import ( + "iter" + "regexp" + + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" +) + +type Reference struct { + Text string + Start int + End int + Profile *nostr.ProfilePointer + Event *nostr.EventPointer + Entity *nostr.EntityPointer +} + +var mentionRegex = regexp.MustCompile(`\bnostr:((note|npub|naddr|nevent|nprofile)1\w+)\b`) + +func ParseReferences(evt nostr.Event) iter.Seq[Reference] { + return func(yield func(Reference) bool) { + for _, ref := range mentionRegex.FindAllStringSubmatchIndex(evt.Content, -1) { + reference := Reference{ + Text: evt.Content[ref[0]:ref[1]], + Start: ref[0], + End: ref[1], + } + + nip19code := evt.Content[ref[2]:ref[3]] + + if prefix, data, err := nip19.Decode(nip19code); err == nil { + switch prefix { + case "npub": + reference.Profile = &nostr.ProfilePointer{ + PublicKey: data.(string), Relays: []string{}, + } + tag := evt.Tags.GetFirst([]string{"p", reference.Profile.PublicKey}) + if tag != nil && len(*tag) >= 3 { + reference.Profile.Relays = []string{(*tag)[2]} + } + case "nprofile": + pp := data.(nostr.ProfilePointer) + reference.Profile = &pp + tag := evt.Tags.GetFirst([]string{"p", reference.Profile.PublicKey}) + if tag != nil && len(*tag) >= 3 { + reference.Profile.Relays = append(reference.Profile.Relays, (*tag)[2]) + } + case "note": + // we don't even bother here because people using note1 codes aren't including relay hints anyway + reference.Event = &nostr.EventPointer{ID: data.(string), Relays: []string{}} + case "nevent": + evp := data.(nostr.EventPointer) + reference.Event = &evp + tag := evt.Tags.GetFirst([]string{"e", reference.Event.ID}) + if tag != nil && len(*tag) >= 3 { + reference.Event.Relays = append(reference.Event.Relays, (*tag)[2]) + if reference.Event.Author == "" && len(*tag) >= 5 { + reference.Event.Author = (*tag)[4] + } + } + case "naddr": + addr := data.(nostr.EntityPointer) + reference.Entity = &addr + tag := evt.Tags.GetFirst([]string{"a", reference.Entity.AsTagReference()}) + if tag != nil && len(*tag) >= 3 { + reference.Entity.Relays = append(reference.Entity.Relays, (*tag)[2]) + } + } + } + + if !yield(reference) { + return + } + } + } +} diff --git a/nip27/references_test.go b/nip27/references_test.go new file mode 100644 index 0000000..8e70c1f --- /dev/null +++ b/nip27/references_test.go @@ -0,0 +1,46 @@ +package nip27 + +import ( + "slices" + "testing" + + "github.com/nbd-wtf/go-nostr" + "github.com/stretchr/testify/require" +) + +func TestParseReferences(t *testing.T) { + evt := nostr.Event{ + Tags: nostr.Tags{ + {"p", "cc6b9fea033f59c3c39a0407c5f1bfee439b077508d918cfdc0d6fd431d39393", "wss://xawr.com"}, + {"e", "a84c5de86efc2ec2cff7bad077c4171e09146b633b7ad117fffe088d9579ac33", "wss://other.com", "reply"}, + {"e", "cc6b9fea033f59c3c39a0407c5f1bfee439b077508d918cfdc0d6fd431d39393", "wss://nasdj.com"}, + }, + Content: "hello, nostr:nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg wrote nostr:nevent1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8ychxp5v4!", + } + + expected := []Reference{ + { + Text: "nostr:nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg", + Start: 7, + End: 83, + Profile: &nostr.ProfilePointer{ + PublicKey: "cc6b9fea033f59c3c39a0407c5f1bfee439b077508d918cfdc0d6fd431d39393", + Relays: []string{"wss://xawr.com"}, + }, + }, + { + Text: "nostr:nevent1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8ychxp5v4", + Start: 90, + End: 164, + Event: &nostr.EventPointer{ + ID: "cc6b9fea033f59c3c39a0407c5f1bfee439b077508d918cfdc0d6fd431d39393", + Relays: []string{"wss://nasdj.com"}, + Author: "", + }, + }, + } + + got := slices.Collect(ParseReferences(evt)) + + require.EqualValues(t, expected, got) +} diff --git a/sdk/references.go b/sdk/references.go deleted file mode 100644 index f16b6b8..0000000 --- a/sdk/references.go +++ /dev/null @@ -1,108 +0,0 @@ -package sdk - -import ( - "iter" - "regexp" - "strconv" - "strings" - - "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip19" -) - -type Reference struct { - Text string - Start int - End int - 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) iter.Seq[Reference] { - return func(yield func(Reference) bool) { - for _, ref := range mentionRegex.FindAllStringSubmatchIndex(evt.Content, -1) { - reference := Reference{ - Text: evt.Content[ref[0]:ref[1]], - Start: ref[0], - End: ref[1], - } - - if ref[6] == -1 { - // didn't find a NIP-10 #[0] reference, so it's a NIP-27 mention - nip19code := evt.Content[ref[2]:ref[3]] - - if prefix, data, err := nip19.Decode(nip19code); err == nil { - switch prefix { - case "npub": - reference.Profile = &nostr.ProfilePointer{ - PublicKey: data.(string), Relays: []string{}, - } - case "nprofile": - pp := data.(nostr.ProfilePointer) - reference.Profile = &pp - case "note": - reference.Event = &nostr.EventPointer{ID: data.(string), Relays: []string{}} - case "nevent": - evp := data.(nostr.EventPointer) - reference.Event = &evp - case "naddr": - addr := data.(nostr.EntityPointer) - reference.Entity = &addr - } - } - } else { - // it's a NIP-10 mention. - // parse the number, get data from event tags. - n := evt.Content[ref[6]:ref[7]] - idx, err := strconv.Atoi(n) - if err != nil || len(evt.Tags) <= idx { - continue - } - if tag := evt.Tags[idx]; tag != nil && len(tag) >= 2 { - switch tag[0] { - case "p": - relays := make([]string, 0, 1) - if len(tag) > 2 && tag[2] != "" { - relays = append(relays, tag[2]) - } - reference.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]) - } - reference.Event = &nostr.EventPointer{ - ID: tag[1], - Relays: relays, - } - case "a": - if parts := strings.Split(tag[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]) - } - reference.Entity = &nostr.EntityPointer{ - Identifier: parts[2], - PublicKey: parts[1], - Kind: kind, - Relays: relays, - } - } - } - } - } - - if !yield(reference) { - return - } - } - } -} diff --git a/sdk/references_test.go b/sdk/references_test.go deleted file mode 100644 index ec98293..0000000 --- a/sdk/references_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package sdk - -import ( - "fmt" - "slices" - "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]", - Start: 6, - End: 10, - Profile: &nostr.ProfilePointer{ - PublicKey: "c9d556c6d3978d112d30616d0d20aaa81410e3653911dd67787b5aaf9b36ade8", - Relays: []string{"wss://nostr.com"}, - }, - }, - { - Text: "#[2]", - Start: 26, - End: 30, - Event: &nostr.EventPointer{ - ID: "31d7c2875b5fc8e6f9c8f9dc1f84de1b6b91d1947ea4c59225e55c325d330fa8", - Relays: []string{}, - }, - }, - { - Text: "nostr:nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg", - Start: 47, - End: 123, - Profile: &nostr.ProfilePointer{ - PublicKey: "cc6b9fea033f59c3c39a0407c5f1bfee439b077508d918cfdc0d6fd431d39393", - Relays: []string{}, - }, - }, - { - Text: "nostr:nevent1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8ychxp5v4", - Start: 127, - End: 201, - Event: &nostr.EventPointer{ - ID: "cc6b9fea033f59c3c39a0407c5f1bfee439b077508d918cfdc0d6fd431d39393", - Relays: []string{}, - Author: "", - }, - }, - } - - got := slices.Collect(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.Start != e.Start { - t.Errorf("%d: got start %d, expected %d", i, g.Start, e.Start) - } - - if g.End != e.End { - t.Errorf("%d: got end %d, expected %d", i, g.End, e.End) - } - - 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) - } - } -} diff --git a/sdk/tracker.go b/sdk/tracker.go index a4969f9..a6da6b6 100644 --- a/sdk/tracker.go +++ b/sdk/tracker.go @@ -4,6 +4,7 @@ import ( "net/url" "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip27" "github.com/nbd-wtf/go-nostr/sdk/hints" ) @@ -104,7 +105,7 @@ func (sys *System) trackEventHints(ie nostr.RelayEvent) { } } - for ref := range ParseReferences(*ie.Event) { + for ref := range nip27.ParseReferences(*ie.Event) { if ref.Profile != nil { for _, relay := range ref.Profile.Relays { if IsVirtualRelay(relay) {