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