Files
nostrlib/pointers.go
2025-11-23 06:57:54 -03:00

186 lines
5.0 KiB
Go

package nostr
import (
"fmt"
"strconv"
"strings"
)
// Pointer is an interface for different types of Nostr pointers.
//
// In this context, a "pointer" is a reference to an event or profile potentially including
// relays and other metadata that might help find it.
type Pointer interface {
// AsTagReference returns the pointer as a string as it would be seen in the value of a tag (i.e. the tag's second item).
AsTagReference() string
// AsTag converts the pointer with all the information available to a tag that can be included in events.
AsTag() Tag
// AsFilter converts the pointer to a Filter that can be used to query for it on relays.
AsFilter() Filter
MatchesEvent(Event) bool
}
var (
_ Pointer = (*ProfilePointer)(nil)
_ Pointer = (*EventPointer)(nil)
_ Pointer = (*EntityPointer)(nil)
)
// ProfilePointer represents a pointer to a Nostr profile.
type ProfilePointer struct {
PublicKey PubKey
Relays []string
}
// ProfilePointerFromTag creates a ProfilePointer from a "p" tag (but it doesn't have to be necessarily a "p" tag, could be something else).
func ProfilePointerFromTag(refTag Tag) (ProfilePointer, error) {
pk, err := PubKeyFromHex(refTag[1])
if err != nil {
return ProfilePointer{}, err
}
pointer := ProfilePointer{
PublicKey: pk,
}
if len(refTag) > 2 {
if relay := (refTag)[2]; IsValidRelayURL(relay) {
pointer.Relays = []string{relay}
}
}
return pointer, nil
}
// MatchesEvent checks if the pointer matches an event.
func (ep ProfilePointer) MatchesEvent(_ Event) bool { return false }
func (ep ProfilePointer) AsFilter() Filter { return Filter{Authors: []PubKey{ep.PublicKey}} }
func (ep ProfilePointer) AsTagReference() string { return HexEncodeToString(ep.PublicKey[:]) }
func (ep ProfilePointer) AsTag() Tag {
if len(ep.Relays) > 0 {
return Tag{"p", HexEncodeToString(ep.PublicKey[:]), ep.Relays[0]}
}
return Tag{"p", HexEncodeToString(ep.PublicKey[:])}
}
// EventPointer represents a pointer to a nostr event.
type EventPointer struct {
ID ID
Relays []string
Author PubKey
Kind Kind
}
// EventPointerFromTag creates an EventPointer from an "e" tag (but it could be other tag name, it isn't checked).
func EventPointerFromTag(refTag Tag) (EventPointer, error) {
id, err := IDFromHex(refTag[1])
if err != nil {
return EventPointer{}, err
}
pointer := EventPointer{
ID: id,
}
if len(refTag) > 2 {
if relay := (refTag)[2]; IsValidRelayURL(relay) {
pointer.Relays = []string{relay}
}
if len(refTag) > 3 {
if pk, err := PubKeyFromHex(refTag[3]); err == nil {
pointer.Author = pk
}
}
}
return pointer, nil
}
func (ep EventPointer) MatchesEvent(evt Event) bool { return evt.ID == ep.ID }
func (ep EventPointer) AsFilter() Filter { return Filter{IDs: []ID{ep.ID}} }
func (ep EventPointer) AsTagReference() string { return HexEncodeToString(ep.ID[:]) }
// AsTag converts the pointer to a Tag.
func (ep EventPointer) AsTag() Tag {
if len(ep.Relays) > 0 {
if ep.Author != [32]byte{} {
return Tag{"e", HexEncodeToString(ep.ID[:]), ep.Relays[0], HexEncodeToString(ep.Author[:])}
} else {
return Tag{"e", HexEncodeToString(ep.ID[:]), ep.Relays[0]}
}
}
return Tag{"e", HexEncodeToString(ep.ID[:])}
}
// EntityPointer represents a pointer to a nostr entity (addressable event).
type EntityPointer struct {
PublicKey PubKey
Kind Kind
Identifier string
Relays []string
}
// EntityPointerFromTag creates an EntityPointer from an "a" tag (but it doesn't check if the tag is really "a", it could be anything).
func EntityPointerFromTag(refTag Tag) (EntityPointer, error) {
pointer, err := ParseAddrString(refTag[1])
if err != nil {
return EntityPointer{}, fmt.Errorf("invalid addr '%s': %w", refTag[1], err)
}
if len(refTag) > 2 {
if relay := (refTag)[2]; IsValidRelayURL(relay) {
pointer.Relays = []string{relay}
}
}
return pointer, nil
}
func ParseAddrString(addr string) (EntityPointer, error) {
spl := strings.SplitN(addr, ":", 3)
if len(spl) != 3 {
return EntityPointer{}, fmt.Errorf("invalid splits")
}
pk, err := PubKeyFromHex(spl[1])
if err != nil {
return EntityPointer{}, err
}
kind, err := strconv.Atoi(spl[0])
if err != nil || kind > (1<<16) {
return EntityPointer{}, fmt.Errorf("invalid kind")
}
return EntityPointer{
Kind: Kind(kind),
PublicKey: pk,
Identifier: spl[2],
}, nil
}
// MatchesEvent checks if the pointer matches an event.
func (ep EntityPointer) MatchesEvent(evt Event) bool {
return ep.PublicKey == evt.PubKey &&
ep.Kind == evt.Kind &&
evt.Tags.GetD() == ep.Identifier
}
func (ep EntityPointer) AsTagReference() string {
return fmt.Sprintf("%d:%s:%s", ep.Kind, ep.PublicKey, ep.Identifier)
}
func (ep EntityPointer) AsFilter() Filter {
return Filter{
Kinds: []Kind{ep.Kind},
Authors: []PubKey{ep.PublicKey},
Tags: TagMap{"d": []string{ep.Identifier}},
}
}
func (ep EntityPointer) AsTag() Tag {
if len(ep.Relays) > 0 {
return Tag{"a", ep.AsTagReference(), ep.Relays[0]}
}
return Tag{"a", ep.AsTagReference()}
}