nip11.go raw

   1  // Package common provides shared utilities for sync services
   2  package common
   3  
   4  import (
   5  	"context"
   6  	"crypto/tls"
   7  	"encoding/json"
   8  	"fmt"
   9  	"net/http"
  10  	"strings"
  11  	"sync"
  12  	"time"
  13  
  14  	"next.orly.dev/pkg/nostr/relayinfo"
  15  )
  16  
  17  // NIP11Cache caches relay information documents with TTL
  18  type NIP11Cache struct {
  19  	cache map[string]*cachedRelayInfo
  20  	mutex sync.RWMutex
  21  	ttl   time.Duration
  22  }
  23  
  24  // cachedRelayInfo holds cached relay info with expiration
  25  type cachedRelayInfo struct {
  26  	info      *relayinfo.T
  27  	expiresAt time.Time
  28  }
  29  
  30  // NewNIP11Cache creates a new NIP-11 cache with the specified TTL
  31  func NewNIP11Cache(ttl time.Duration) *NIP11Cache {
  32  	return &NIP11Cache{
  33  		cache: make(map[string]*cachedRelayInfo),
  34  		ttl:   ttl,
  35  	}
  36  }
  37  
  38  // Get fetches relay information for a given URL, using cache if available
  39  func (c *NIP11Cache) Get(ctx context.Context, relayURL string) (*relayinfo.T, error) {
  40  	// Normalize URL - remove protocol and trailing slash
  41  	normalizedURL := strings.TrimPrefix(relayURL, "https://")
  42  	normalizedURL = strings.TrimPrefix(normalizedURL, "http://")
  43  	normalizedURL = strings.TrimSuffix(normalizedURL, "/")
  44  
  45  	// Check cache first
  46  	c.mutex.RLock()
  47  	if cached, exists := c.cache[normalizedURL]; exists && time.Now().Before(cached.expiresAt) {
  48  		c.mutex.RUnlock()
  49  		return cached.info, nil
  50  	}
  51  	c.mutex.RUnlock()
  52  
  53  	// Fetch fresh data
  54  	info, err := c.fetchNIP11(ctx, relayURL)
  55  	if err != nil {
  56  		return nil, err
  57  	}
  58  
  59  	// Cache the result
  60  	c.mutex.Lock()
  61  	c.cache[normalizedURL] = &cachedRelayInfo{
  62  		info:      info,
  63  		expiresAt: time.Now().Add(c.ttl),
  64  	}
  65  	c.mutex.Unlock()
  66  
  67  	return info, nil
  68  }
  69  
  70  // fetchNIP11 fetches relay information document from a given URL
  71  func (c *NIP11Cache) fetchNIP11(ctx context.Context, relayURL string) (*relayinfo.T, error) {
  72  	// Convert WebSocket URL to HTTP URL for NIP-11 fetch
  73  	// wss:// -> https://, ws:// -> http://
  74  	nip11URL := relayURL
  75  	nip11URL = strings.Replace(nip11URL, "wss://", "https://", 1)
  76  	nip11URL = strings.Replace(nip11URL, "ws://", "http://", 1)
  77  	if !strings.HasSuffix(nip11URL, "/") {
  78  		nip11URL += "/"
  79  	}
  80  	nip11URL += ".well-known/nostr.json"
  81  
  82  	// Create HTTP client with timeout
  83  	client := &http.Client{
  84  		Timeout: 10 * time.Second,
  85  		Transport: &http.Transport{
  86  			TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
  87  		},
  88  	}
  89  
  90  	req, err := http.NewRequestWithContext(ctx, "GET", nip11URL, nil)
  91  	if err != nil {
  92  		return nil, fmt.Errorf("failed to create request: %w", err)
  93  	}
  94  
  95  	req.Header.Set("Accept", "application/nostr+json")
  96  
  97  	resp, err := client.Do(req)
  98  	if err != nil {
  99  		return nil, fmt.Errorf("failed to fetch NIP-11 document from %s: %w", nip11URL, err)
 100  	}
 101  	defer resp.Body.Close()
 102  
 103  	if resp.StatusCode != http.StatusOK {
 104  		return nil, fmt.Errorf("NIP-11 request failed with status %d", resp.StatusCode)
 105  	}
 106  
 107  	var info relayinfo.T
 108  	if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
 109  		return nil, fmt.Errorf("failed to decode NIP-11 document: %w", err)
 110  	}
 111  
 112  	return &info, nil
 113  }
 114  
 115  // GetPubkey fetches the relay's identity pubkey from its NIP-11 document
 116  func (c *NIP11Cache) GetPubkey(ctx context.Context, relayURL string) (string, error) {
 117  	info, err := c.Get(ctx, relayURL)
 118  	if err != nil {
 119  		return "", err
 120  	}
 121  
 122  	if info.PubKey == "" {
 123  		return "", fmt.Errorf("relay %s does not provide pubkey in NIP-11 document", relayURL)
 124  	}
 125  
 126  	return info.PubKey, nil
 127  }
 128