relays.go raw
1 package main
2
3 import (
4 "log"
5 "strings"
6 "sync"
7
8 "git.mleku.dev/mleku/dendrite/pkg/nostr"
9 )
10
11 // RelayPool discovers and maintains a set of known relay URLs
12 // by harvesting kind-10002 (NIP-65 relay list) events.
13 type RelayPool struct {
14 mu sync.RWMutex
15 relays map[string]struct{}
16 max int
17 }
18
19 // NewRelayPool creates a pool seeded with initial relay URLs.
20 func NewRelayPool(seeds []string, max int) *RelayPool {
21 if max < 1 {
22 max = 1000
23 }
24 p := &RelayPool{
25 relays: make(map[string]struct{}, max),
26 max: max,
27 }
28 for _, u := range seeds {
29 u = normalizeURL(u)
30 if u != "" {
31 p.relays[u] = struct{}{}
32 }
33 }
34 return p
35 }
36
37 // Ingest processes a kind-10002 relay list event, extracting relay URLs.
38 // NIP-65 format: tags are ["r", "<url>"] or ["r", "<url>", "read"|"write"].
39 // We want write-capable relays (no marker = both, "write" = write-only).
40 func (p *RelayPool) Ingest(ev *nostr.Event) {
41 if ev.Kind != 10002 {
42 return
43 }
44 p.mu.Lock()
45 defer p.mu.Unlock()
46
47 for _, tag := range ev.Tags {
48 if len(tag) < 2 || tag[0] != "r" {
49 continue
50 }
51 url := normalizeURL(tag[1])
52 if url == "" {
53 continue
54 }
55 // Skip read-only relays — we need to write to them.
56 if len(tag) >= 3 && tag[2] == "read" {
57 continue
58 }
59 if len(p.relays) >= p.max {
60 return // cap reached
61 }
62 p.relays[url] = struct{}{}
63 }
64 }
65
66 // URLs returns all known relay URLs.
67 func (p *RelayPool) URLs() []string {
68 p.mu.RLock()
69 defer p.mu.RUnlock()
70 urls := make([]string, 0, len(p.relays))
71 for u := range p.relays {
72 urls = append(urls, u)
73 }
74 return urls
75 }
76
77 // Size returns the number of known relays.
78 func (p *RelayPool) Size() int {
79 p.mu.RLock()
80 defer p.mu.RUnlock()
81 return len(p.relays)
82 }
83
84 // normalizeURL cleans up a relay WebSocket URL.
85 func normalizeURL(u string) string {
86 u = strings.TrimSpace(u)
87 if u == "" {
88 return ""
89 }
90 // Accept wss:// and ws:// only.
91 if !strings.HasPrefix(u, "wss://") && !strings.HasPrefix(u, "ws://") {
92 return ""
93 }
94 // Strip trailing slash.
95 u = strings.TrimRight(u, "/")
96 // Skip localhost/private relays.
97 lower := strings.ToLower(u)
98 if strings.Contains(lower, "localhost") || strings.Contains(lower, "127.0.0.1") {
99 return ""
100 }
101 return u
102 }
103
104 // Well-known relays to seed the pool.
105 var seedRelays = []string{
106 "wss://relay.orly.dev",
107 "wss://relay.damus.io",
108 "wss://nos.lol",
109 "wss://relay.nostr.band",
110 "wss://relay.snort.social",
111 "wss://nostr.wine",
112 "wss://relay.primal.net",
113 "wss://nostr-pub.wellorder.net",
114 "wss://nostr.mutinywallet.com",
115 "wss://purplepag.es",
116 "wss://relay.nostr.bg",
117 }
118
119 func init() {
120 log.SetFlags(log.Ldate | log.Ltime)
121 }
122