125 lines
3.1 KiB
Go
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
|
|
}
|