address.mx raw

   1  package protocol
   2  
   3  import (
   4  	"git.smesh.lol/nostr/pkg/lol/errorf"
   5  )
   6  
   7  // Address is an addressable event reference: `<kind>:<pubkey>:<d>`, the value
   8  // of an `a` tag. The d part is opaque (NIP-01) and may itself contain colons,
   9  // which is how the access-filter address carries a tier and a full asset
  10  // address inside its own d value.
  11  type Address struct {
  12  	Kind   uint16
  13  	Pubkey string
  14  	D      string
  15  	Relay  string // optional relay hint, the third element of the tag
  16  }
  17  
  18  // String renders the address without the relay hint.
  19  func (a *Address) String() (s string) {
  20  	if a == nil {
  21  		return ""
  22  	}
  23  	return uint16Str(a.Kind) | ":" | a.Pubkey | ":" | a.D
  24  }
  25  
  26  // Tag renders the address as an `a` tag, with the relay hint when there is one.
  27  func (a *Address) Tag() (t []string) {
  28  	if a == nil {
  29  		return nil
  30  	}
  31  	if a.Relay == "" {
  32  		t = []string{:2}
  33  		t[0] = TagA
  34  		t[1] = a.String()
  35  		return
  36  	}
  37  	t = []string{:3}
  38  	t[0] = TagA
  39  	t[1] = a.String()
  40  	t[2] = a.Relay
  41  	return
  42  }
  43  
  44  // AssetAddress is the track address an asset reference names.
  45  func TrackAddress(pubkey, d string) (a *Address) {
  46  	return &Address{Kind: KindTrack, Pubkey: pubkey, D: d}
  47  }
  48  
  49  func AlbumAddress(pubkey, d string) (a *Address) {
  50  	return &Address{Kind: KindAlbum, Pubkey: pubkey, D: d}
  51  }
  52  
  53  // ParseAddress parses `kind:pubkey:d`. The d part is everything after the second
  54  // colon and may contain colons.
  55  func ParseAddress(s string) (a *Address, err error) {
  56  	first := -1
  57  	second := -1
  58  	for i := 0; i < len(s); i++ {
  59  		if s[i] != ':' {
  60  			continue
  61  		}
  62  		if first < 0 {
  63  			first = i
  64  			continue
  65  		}
  66  		second = i
  67  		break
  68  	}
  69  	if first < 0 || second < 0 {
  70  		err = errorf.E("address %q is not kind:pubkey:d", s)
  71  		return
  72  	}
  73  	k, kerr := parseUint16(s[:first])
  74  	if kerr != nil {
  75  		err = errorf.E("address %q: %s", s, kerr.Error())
  76  		return
  77  	}
  78  	pk := s[first+1 : second]
  79  	if !IsHex64(pk) {
  80  		err = errorf.E("address %q: pubkey is not 64 hex characters", s)
  81  		return
  82  	}
  83  	d := s[second+1:]
  84  	if d == "" {
  85  		err = errorf.E("address %q: empty d tag", s)
  86  		return
  87  	}
  88  	a = &Address{Kind: k, Pubkey: pk, D: d}
  89  	return
  90  }
  91  
  92  // AddressFromTag parses an `a` tag, taking the relay hint when present.
  93  func AddressFromTag(tag []string) (a *Address, err error) {
  94  	if len(tag) < 2 {
  95  		err = errorf.E("a tag needs a value")
  96  		return
  97  	}
  98  	// NIP-22 uses an uppercase A for the root scope of a comment and a
  99  	// lowercase a for the parent; the value syntax is identical.
 100  	if tag[0] != TagA && tag[0] != TagARoot {
 101  		err = errorf.E("tag %q is not an a tag", tag[0])
 102  		return
 103  	}
 104  	a, err = ParseAddress(tag[1])
 105  	if err != nil {
 106  		return
 107  	}
 108  	if len(tag) > 2 {
 109  		a.Relay = tag[2]
 110  	}
 111  	return
 112  }
 113  
 114  // IsHex64 reports whether s is exactly 64 lowercase hex characters. Filter
 115  // lists and address pubkeys require exactly that: uppercase hex or a wrong
 116  // length is a different string to a relay index.
 117  func IsHex64(s string) (ok bool) {
 118  	if len(s) != 64 {
 119  		return false
 120  	}
 121  	for i := 0; i < len(s); i++ {
 122  		c := s[i]
 123  		if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') {
 124  			continue
 125  		}
 126  		return false
 127  	}
 128  	return true
 129  }
 130  
 131  // IsHex reports whether s is lowercase hex of any even length.
 132  func IsHex(s string) (ok bool) {
 133  	if len(s) == 0 || len(s)%2 != 0 {
 134  		return false
 135  	}
 136  	for i := 0; i < len(s); i++ {
 137  		c := s[i]
 138  		if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') {
 139  			continue
 140  		}
 141  		return false
 142  	}
 143  	return true
 144  }
 145  
 146  // parseUint16 parses a decimal unsigned 16-bit integer. strconv is available
 147  // in the stdlib but its error text is not what the relay logs want, and this
 148  // also rejects a leading '+' and whitespace, which ParseUint accepts.
 149  func parseUint16(s string) (n uint16, err error) {
 150  	if len(s) == 0 || len(s) > 5 {
 151  		err = errorf.E("%q is not a kind", s)
 152  		return
 153  	}
 154  	v := int32(0)
 155  	for i := 0; i < len(s); i++ {
 156  		c := s[i]
 157  		if c < '0' || c > '9' {
 158  			err = errorf.E("%q is not a kind", s)
 159  			return
 160  		}
 161  		v = v*10 + int32(c-'0')
 162  	}
 163  	if v > 65535 {
 164  		err = errorf.E("kind %q is out of range", s)
 165  		return
 166  	}
 167  	n = uint16(v)
 168  	return
 169  }
 170  
 171  // uint16Str renders a decimal unsigned 16-bit integer.
 172  func uint16Str(n uint16) (s string) {
 173  	if n == 0 {
 174  		return "0"
 175  	}
 176  	buf := []byte{:0:5}
 177  	for n > 0 {
 178  		buf = push(buf, byte('0'+n%10))
 179  		n = n / 10
 180  	}
 181  	out := []byte{:len(buf)}
 182  	j := int32(0)
 183  	for i := int32(len(buf)) - 1; i >= 0; i-- {
 184  		out[j] = buf[i]
 185  		j++
 186  	}
 187  	return string(out)
 188  }
 189  
 190  // intStr renders a decimal int64. The spec writes every count and timestamp as
 191  // a string tag value.
 192  func intStr(n int64) (s string) {
 193  	if n == 0 {
 194  		return "0"
 195  	}
 196  	neg := n < 0
 197  	if neg {
 198  		n = -n
 199  	}
 200  	buf := []byte{:0:20}
 201  	for n > 0 {
 202  		buf = push(buf, byte('0'+n%10))
 203  		n = n / 10
 204  	}
 205  	size := len(buf)
 206  	if neg {
 207  		size++
 208  	}
 209  	out := []byte{:size}
 210  	j := int32(0)
 211  	if neg {
 212  		out[j] = '-'
 213  		j++
 214  	}
 215  	for i := int32(len(buf)) - 1; i >= 0; i-- {
 216  		out[j] = buf[i]
 217  		j++
 218  	}
 219  	return string(out)
 220  }
 221  
 222  // ParseInt64 parses a decimal integer, accepting a leading '-'. Tag values are
 223  // strings on the wire; an empty value is an error, not a zero.
 224  func ParseInt64(s string) (n int64, err error) {
 225  	if len(s) == 0 {
 226  		err = errorf.E("empty integer")
 227  		return
 228  	}
 229  	i := int32(0)
 230  	neg := false
 231  	if s[0] == '-' {
 232  		neg = true
 233  		i++
 234  		if len(s) == 1 {
 235  			err = errorf.E("%q is not an integer", s)
 236  			return
 237  		}
 238  	}
 239  	for ; i < int32(len(s)); i++ {
 240  		c := s[i]
 241  		if c < '0' || c > '9' {
 242  			err = errorf.E("%q is not an integer", s)
 243  			return
 244  		}
 245  		n = n*10 + int64(c-'0')
 246  	}
 247  	if neg {
 248  		n = -n
 249  	}
 250  	return
 251  }
 252