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