Files
next.orly.dev/pkg/sync/nip11.go
2025-11-23 08:15:06 +00:00

125 lines
3.1 KiB
Go

// Package sync provides NIP-11 relay information document fetching and caching
package sync
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
"time"
"git.mleku.dev/mleku/nostr/relayinfo"
)
// NIP11Cache caches relay information documents with TTL
type NIP11Cache struct {
cache map[string]*cachedRelayInfo
mutex sync.RWMutex
ttl time.Duration
}
// cachedRelayInfo holds cached relay info with expiration
type cachedRelayInfo struct {
info *relayinfo.T
expiresAt time.Time
}
// NewNIP11Cache creates a new NIP-11 cache with the specified TTL
func NewNIP11Cache(ttl time.Duration) *NIP11Cache {
return &NIP11Cache{
cache: make(map[string]*cachedRelayInfo),
ttl: ttl,
}
}
// Get fetches relay information for a given URL, using cache if available
func (c *NIP11Cache) Get(ctx context.Context, relayURL string) (*relayinfo.T, error) {
// Normalize URL - remove protocol and trailing slash
normalizedURL := strings.TrimPrefix(relayURL, "https://")
normalizedURL = strings.TrimPrefix(normalizedURL, "http://")
normalizedURL = strings.TrimSuffix(normalizedURL, "/")
// Check cache first
c.mutex.RLock()
if cached, exists := c.cache[normalizedURL]; exists && time.Now().Before(cached.expiresAt) {
c.mutex.RUnlock()
return cached.info, nil
}
c.mutex.RUnlock()
// Fetch fresh data
info, err := c.fetchNIP11(ctx, relayURL)
if err != nil {
return nil, err
}
// Cache the result
c.mutex.Lock()
c.cache[normalizedURL] = &cachedRelayInfo{
info: info,
expiresAt: time.Now().Add(c.ttl),
}
c.mutex.Unlock()
return info, nil
}
// fetchNIP11 fetches relay information document from a given URL
func (c *NIP11Cache) fetchNIP11(ctx context.Context, relayURL string) (*relayinfo.T, error) {
// Construct NIP-11 URL
nip11URL := relayURL
if !strings.HasSuffix(nip11URL, "/") {
nip11URL += "/"
}
nip11URL += ".well-known/nostr.json"
// Create HTTP client with timeout
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
},
}
req, err := http.NewRequestWithContext(ctx, "GET", nip11URL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Accept", "application/nostr+json")
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch NIP-11 document from %s: %w", nip11URL, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("NIP-11 request failed with status %d", resp.StatusCode)
}
var info relayinfo.T
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return nil, fmt.Errorf("failed to decode NIP-11 document: %w", err)
}
return &info, nil
}
// GetPubkey fetches the relay's identity pubkey from its NIP-11 document
func (c *NIP11Cache) GetPubkey(ctx context.Context, relayURL string) (string, error) {
info, err := c.Get(ctx, relayURL)
if err != nil {
return "", err
}
if info.PubKey == "" {
return "", fmt.Errorf("relay %s does not provide pubkey in NIP-11 document", relayURL)
}
return info.PubKey, nil
}