Files
Nip-05-api/internal/nostr/profile.go
2026-04-29 02:35:00 +00:00

82 lines
2.0 KiB
Go

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)
}