nip5A: nsites.

This commit is contained in:
fiatjaf
2026-04-22 15:08:01 -03:00
parent 5b28d08e47
commit a21ea55eaa
3 changed files with 189 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
package nip5a
import (
"fmt"
"math/big"
"strings"
"fiatjaf.com/nostr"
)
func NormalizePath(p string) string {
if !strings.HasSuffix(p, ".html") && !strings.HasSuffix(p, "/") {
return p
}
if strings.HasSuffix(p, "/") {
return p + "index.html"
}
return p
}
func PubKeyFromBase36(value string) (nostr.PubKey, error) {
bi, ok := new(big.Int).SetString(value, 36)
if !ok {
return nostr.ZeroPK, fmt.Errorf("invalid base36 pubkey")
}
buf := bi.Bytes()
if len(buf) > 32 {
return nostr.ZeroPK, fmt.Errorf("base36 pubkey too long")
}
var pk nostr.PubKey
copy(pk[32-len(buf):], buf)
return pk, nil
}
func PubKeyToBase36(pubkey nostr.PubKey) string {
value := new(big.Int).SetBytes(pubkey[:]).Text(36)
return strings.Repeat("0", 50-len(value)) + value
}
+145
View File
@@ -0,0 +1,145 @@
package nip5a
import (
"encoding/hex"
"fmt"
"regexp"
"strings"
"unsafe"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
)
type SiteManifest struct {
Event *nostr.Event
Pubkey nostr.PubKey
Root bool
Identifier string
Paths map[string][32]byte
Servers []string
Title string
Description string
Source string
}
func ParseSiteManifest(event *nostr.Event) (*SiteManifest, error) {
sm := &SiteManifest{Event: event}
switch event.Kind {
case nostr.KindNsiteRoot:
sm.Root = true
case nostr.KindNsiteNamed:
sm.Root = false
for _, tag := range event.Tags {
if len(tag) >= 2 && tag[0] == "d" {
sm.Identifier = tag[1]
break
}
}
if sm.Identifier == "" {
return nil, fmt.Errorf("named site manifest missing d tag")
}
default:
return nil, fmt.Errorf("invalid site manifest kind: %d", event.Kind)
}
sm.Pubkey = event.PubKey
sm.Paths = make(map[string][32]byte, len(event.Tags))
for _, tag := range event.Tags {
if len(tag) < 2 {
continue
}
switch tag[0] {
case "path":
var hash [32]byte
if len(tag[2]) != 64 {
return nil, fmt.Errorf("invalid hash '%s' for path '%s'", tag[2], tag[1])
}
if _, err := hex.Decode(hash[:], unsafe.Slice(unsafe.StringData(tag[2]), 64)); err != nil {
return nil, fmt.Errorf("invalid hash '%s' for path '%s'", tag[2], tag[1])
}
sm.Paths[NormalizePath(tag[1])] = hash
case "server":
sm.Servers = append(sm.Servers, tag[1])
case "title":
sm.Title = tag[1]
case "description":
sm.Description = tag[1]
case "source":
sm.Source = tag[1]
}
}
return sm, nil
}
func (sm *SiteManifest) ToEvent(pubkey nostr.PubKey) *nostr.Event {
event := &nostr.Event{
PubKey: pubkey,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{},
}
if sm.Root {
event.Kind = nostr.KindNsiteRoot
} else {
event.Kind = nostr.KindNsiteNamed
event.Tags = append(event.Tags, nostr.Tag{"d", sm.Identifier})
}
for path, hash := range sm.Paths {
event.Tags = append(event.Tags, nostr.Tag{"path", NormalizePath(path), hex.EncodeToString(hash[:])})
}
for _, s := range sm.Servers {
event.Tags = append(event.Tags, nostr.Tag{"server", s})
}
if sm.Title != "" {
event.Tags = append(event.Tags, nostr.Tag{"title", sm.Title})
}
if sm.Description != "" {
event.Tags = append(event.Tags, nostr.Tag{"description", sm.Description})
}
if sm.Source != "" {
event.Tags = append(event.Tags, nostr.Tag{"source", sm.Source})
}
return event
}
//go:inline
func (sm *SiteManifest) GetHashForPath(path string) ([32]byte, bool) {
path = NormalizePath(path)
hash, ok := sm.Paths[path]
return hash, ok
}
func DecodeSiteURL(label string) (pubkey nostr.PubKey, identifier string, isRoot bool, err error) {
label, _, _ = strings.Cut(label, ".")
if strings.HasPrefix(label, "npub1") {
_, value, err := nip19.Decode(label)
if err != nil {
return nostr.ZeroPK, "", false, err
}
return value.(nostr.PubKey), "", true, nil
}
if len(label) < 51 || len(label) > 63 || strings.HasSuffix(label, "-") {
return nostr.ZeroPK, "", false, fmt.Errorf("invalid site label format")
}
pubkeyB36 := label[:50]
dTag := label[50:]
if !regexp.MustCompile(`^[a-z0-9-]{1,13}$`).MatchString(dTag) {
return nostr.ZeroPK, "", false, fmt.Errorf("invalid dtag format")
}
pk, err := PubKeyFromBase36(pubkeyB36)
if err != nil {
return nostr.ZeroPK, "", false, err
}
return pk, dTag, false, nil
}