package nostr import ( "context" "encoding/json" "time" gn "github.com/nbd-wtf/go-nostr" ) // Metadata mirrors the kind:0 profile JSON. We accept both snake_case // (`display_name`, NIP-24) and the deprecated camelCase `displayName` since // some clients still publish only the latter. `Username` is also deprecated // but appears in older profiles; per NIP-24 it should be ignored in favor of // `Name`, but we surface it as a last-resort fallback. type Metadata struct { Name string `json:"name"` DisplayName string `json:"display_name"` DisplayNameAlt string `json:"displayName"` Username string `json:"username"` NIP05 string `json:"nip05"` About string `json:"about"` Picture string `json:"picture"` } // ParseMetadata decodes a kind:0 content payload. func ParseMetadata(content string) (*Metadata, error) { var md Metadata if err := json.Unmarshal([]byte(content), &md); err != nil { return nil, err } return &md, nil } // FetchMetadata pulls the most recent kind:0 event for a hex pubkey across the pool. // Each relay may return multiple replacement events; we keep the one with the // highest CreatedAt across every relay reached before timeout. func FetchMetadata(ctx context.Context, p *Pool, hexpk string) (*Metadata, error) { filter := gn.Filter{ Kinds: []int{0}, Authors: []string{hexpk}, Limit: 100, } var newest *gn.Event for _, url := range p.URLs() { r, err := p.Connect(ctx, url) if err != nil { continue } subCtx, cancel := context.WithTimeout(ctx, 8*time.Second) sub, err := r.Subscribe(subCtx, gn.Filters{filter}) if err != nil { cancel() continue } loop: for { select { case ev, ok := <-sub.Events: if !ok { break loop } if newest == nil || ev.CreatedAt > newest.CreatedAt { newest = ev } case <-sub.EndOfStoredEvents: break loop case <-subCtx.Done(): break loop } } sub.Unsub() cancel() } if newest == nil { return nil, nil } return ParseMetadata(newest.Content) }