package protocol import ( "git.smesh.lol/nostr/pkg/lol/errorf" ) // Address is an addressable event reference: `::`, the value // of an `a` tag. The d part is opaque (NIP-01) and may itself contain colons, // which is how the access-filter address carries a tier and a full asset // address inside its own d value. type Address struct { Kind uint16 Pubkey string D string Relay string // optional relay hint, the third element of the tag } // String renders the address without the relay hint. func (a *Address) String() (s string) { if a == nil { return "" } return uint16Str(a.Kind) | ":" | a.Pubkey | ":" | a.D } // Tag renders the address as an `a` tag, with the relay hint when there is one. func (a *Address) Tag() (t []string) { if a == nil { return nil } if a.Relay == "" { t = []string{:2} t[0] = TagA t[1] = a.String() return } t = []string{:3} t[0] = TagA t[1] = a.String() t[2] = a.Relay return } // AssetAddress is the track address an asset reference names. func TrackAddress(pubkey, d string) (a *Address) { return &Address{Kind: KindTrack, Pubkey: pubkey, D: d} } func AlbumAddress(pubkey, d string) (a *Address) { return &Address{Kind: KindAlbum, Pubkey: pubkey, D: d} } // ParseAddress parses `kind:pubkey:d`. The d part is everything after the second // colon and may contain colons. func ParseAddress(s string) (a *Address, err error) { first := -1 second := -1 for i := 0; i < len(s); i++ { if s[i] != ':' { continue } if first < 0 { first = i continue } second = i break } if first < 0 || second < 0 { err = errorf.E("address %q is not kind:pubkey:d", s) return } k, kerr := parseUint16(s[:first]) if kerr != nil { err = errorf.E("address %q: %s", s, kerr.Error()) return } pk := s[first+1 : second] if !IsHex64(pk) { err = errorf.E("address %q: pubkey is not 64 hex characters", s) return } d := s[second+1:] if d == "" { err = errorf.E("address %q: empty d tag", s) return } a = &Address{Kind: k, Pubkey: pk, D: d} return } // AddressFromTag parses an `a` tag, taking the relay hint when present. func AddressFromTag(tag []string) (a *Address, err error) { if len(tag) < 2 { err = errorf.E("a tag needs a value") return } // NIP-22 uses an uppercase A for the root scope of a comment and a // lowercase a for the parent; the value syntax is identical. if tag[0] != TagA && tag[0] != TagARoot { err = errorf.E("tag %q is not an a tag", tag[0]) return } a, err = ParseAddress(tag[1]) if err != nil { return } if len(tag) > 2 { a.Relay = tag[2] } return } // IsHex64 reports whether s is exactly 64 lowercase hex characters. Filter // lists and address pubkeys require exactly that: uppercase hex or a wrong // length is a different string to a relay index. func IsHex64(s string) (ok bool) { if len(s) != 64 { return false } for i := 0; i < len(s); i++ { c := s[i] if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') { continue } return false } return true } // IsHex reports whether s is lowercase hex of any even length. func IsHex(s string) (ok bool) { if len(s) == 0 || len(s)%2 != 0 { return false } for i := 0; i < len(s); i++ { c := s[i] if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') { continue } return false } return true } // parseUint16 parses a decimal unsigned 16-bit integer. strconv is available // in the stdlib but its error text is not what the relay logs want, and this // also rejects a leading '+' and whitespace, which ParseUint accepts. func parseUint16(s string) (n uint16, err error) { if len(s) == 0 || len(s) > 5 { err = errorf.E("%q is not a kind", s) return } v := int32(0) for i := 0; i < len(s); i++ { c := s[i] if c < '0' || c > '9' { err = errorf.E("%q is not a kind", s) return } v = v*10 + int32(c-'0') } if v > 65535 { err = errorf.E("kind %q is out of range", s) return } n = uint16(v) return } // uint16Str renders a decimal unsigned 16-bit integer. func uint16Str(n uint16) (s string) { if n == 0 { return "0" } buf := []byte{:0:5} for n > 0 { buf = push(buf, byte('0'+n%10)) n = n / 10 } out := []byte{:len(buf)} j := int32(0) for i := int32(len(buf)) - 1; i >= 0; i-- { out[j] = buf[i] j++ } return string(out) } // intStr renders a decimal int64. The spec writes every count and timestamp as // a string tag value. func intStr(n int64) (s string) { if n == 0 { return "0" } neg := n < 0 if neg { n = -n } buf := []byte{:0:20} for n > 0 { buf = push(buf, byte('0'+n%10)) n = n / 10 } size := len(buf) if neg { size++ } out := []byte{:size} j := int32(0) if neg { out[j] = '-' j++ } for i := int32(len(buf)) - 1; i >= 0; i-- { out[j] = buf[i] j++ } return string(out) } // ParseInt64 parses a decimal integer, accepting a leading '-'. Tag values are // strings on the wire; an empty value is an error, not a zero. func ParseInt64(s string) (n int64, err error) { if len(s) == 0 { err = errorf.E("empty integer") return } i := int32(0) neg := false if s[0] == '-' { neg = true i++ if len(s) == 1 { err = errorf.E("%q is not an integer", s) return } } for ; i < int32(len(s)); i++ { c := s[i] if c < '0' || c > '9' { err = errorf.E("%q is not an integer", s) return } n = n*10 + int64(c-'0') } if neg { n = -n } return }