manager.go raw
1 // Package relaygroup provides relay group configuration management
2 package relaygroup
3
4 import (
5 "context"
6 "encoding/hex"
7 "encoding/json"
8 "sort"
9 "strings"
10
11 "github.com/minio/sha256-simd"
12 "next.orly.dev/pkg/nostr/encoders/bech32encoding"
13 "next.orly.dev/pkg/nostr/encoders/event"
14 "next.orly.dev/pkg/nostr/encoders/filter"
15 "next.orly.dev/pkg/nostr/encoders/kind"
16 "next.orly.dev/pkg/nostr/encoders/tag"
17 "next.orly.dev/pkg/lol/log"
18 "next.orly.dev/pkg/database"
19 )
20
21 // PeerUpdater is an interface for updating peer lists
22 type PeerUpdater interface {
23 UpdatePeers(peers []string)
24 }
25
26 // Config represents a relay group configuration
27 type Config struct {
28 Relays []string `json:"relays"`
29 }
30
31 // Manager handles relay group configuration
32 type Manager struct {
33 db *database.D
34 authorizedPubkeys [][]byte
35 }
36
37 // ManagerConfig holds configuration for the relay group manager
38 type ManagerConfig struct {
39 AdminNpubs []string
40 }
41
42 // NewManager creates a new relay group manager
43 func NewManager(db *database.D, cfg *ManagerConfig) *Manager {
44 var pubkeys [][]byte
45 if cfg != nil {
46 for _, npub := range cfg.AdminNpubs {
47 if pk, err := bech32encoding.NpubOrHexToPublicKeyBinary(npub); err == nil {
48 pubkeys = append(pubkeys, pk)
49 }
50 }
51 }
52
53 return &Manager{
54 db: db,
55 authorizedPubkeys: pubkeys,
56 }
57 }
58
59 // FindAuthoritativeConfig finds the authoritative relay group configuration
60 // by selecting the latest event by timestamp, with hash tie-breaking
61 func (rgm *Manager) FindAuthoritativeConfig(ctx context.Context) (*Config, error) {
62 if len(rgm.authorizedPubkeys) == 0 {
63 return nil, nil
64 }
65
66 // Query for all relay group config events from authorized pubkeys
67 f := &filter.F{
68 Kinds: kind.NewS(kind.RelayGroupConfig),
69 Authors: tag.NewFromBytesSlice(rgm.authorizedPubkeys...),
70 }
71
72 events, err := rgm.db.QueryEvents(ctx, f)
73 if err != nil {
74 return nil, err
75 }
76
77 if len(events) == 0 {
78 return nil, nil
79 }
80
81 // Find the authoritative event
82 authEvent := rgm.selectAuthoritativeEvent(events)
83 if authEvent == nil {
84 return nil, nil
85 }
86
87 // Parse the configuration from the event content
88 var config Config
89 if err := json.Unmarshal([]byte(authEvent.Content), &config); err != nil {
90 return nil, err
91 }
92
93 return &config, nil
94 }
95
96 // FindAuthoritativeRelays returns just the relay URLs from the authoritative config
97 func (rgm *Manager) FindAuthoritativeRelays(ctx context.Context) ([]string, error) {
98 config, err := rgm.FindAuthoritativeConfig(ctx)
99 if err != nil {
100 return nil, err
101 }
102 if config == nil {
103 return nil, nil
104 }
105 return config.Relays, nil
106 }
107
108 // selectAuthoritativeEvent selects the authoritative event using the specified criteria
109 func (rgm *Manager) selectAuthoritativeEvent(events []*event.E) *event.E {
110 if len(events) == 0 {
111 return nil
112 }
113
114 // Sort events by timestamp (newest first), then by hash (smallest first)
115 sort.Slice(events, func(i, j int) bool {
116 // First compare timestamps (newest first)
117 if events[i].CreatedAt != events[j].CreatedAt {
118 return events[i].CreatedAt > events[j].CreatedAt
119 }
120
121 // If timestamps are equal, compare hashes (smallest first)
122 hashI := sha256.Sum256([]byte(events[i].ID))
123 hashJ := sha256.Sum256([]byte(events[j].ID))
124 return strings.Compare(hex.EncodeToString(hashI[:]), hex.EncodeToString(hashJ[:])) < 0
125 })
126
127 return events[0]
128 }
129
130 // IsAuthorizedPublisher checks if a pubkey is authorized to publish relay group configs
131 func (rgm *Manager) IsAuthorizedPublisher(pubkey []byte) bool {
132 for _, authPK := range rgm.authorizedPubkeys {
133 if string(authPK) == string(pubkey) {
134 return true
135 }
136 }
137 return false
138 }
139
140 // GetAuthorizedPubkeys returns all authorized pubkeys
141 func (rgm *Manager) GetAuthorizedPubkeys() [][]byte {
142 result := make([][]byte, len(rgm.authorizedPubkeys))
143 for i, pk := range rgm.authorizedPubkeys {
144 pkCopy := make([]byte, len(pk))
145 copy(pkCopy, pk)
146 result[i] = pkCopy
147 }
148 return result
149 }
150
151 // ValidateRelayGroupEvent validates a relay group configuration event
152 func (rgm *Manager) ValidateRelayGroupEvent(ev *event.E) error {
153 // Check if it's the right kind
154 if ev.Kind != kind.RelayGroupConfig.K {
155 return nil // Not our concern
156 }
157
158 // Check if publisher is authorized
159 if !rgm.IsAuthorizedPublisher(ev.Pubkey) {
160 return nil // Not our concern, but won't be considered authoritative
161 }
162
163 // Try to parse the content
164 var config Config
165 if err := json.Unmarshal([]byte(ev.Content), &config); err != nil {
166 return err
167 }
168
169 // Basic validation - at least one relay should be specified
170 if len(config.Relays) == 0 {
171 return nil // Empty config is allowed, just won't be selected
172 }
173
174 return nil
175 }
176
177 // HandleRelayGroupEvent processes a relay group configuration event and updates peer lists
178 func (rgm *Manager) HandleRelayGroupEvent(ev *event.E, peerUpdater PeerUpdater) {
179 if ev.Kind != kind.RelayGroupConfig.K {
180 return
181 }
182
183 // Check if this event is the new authoritative configuration
184 authConfig, err := rgm.FindAuthoritativeConfig(context.Background())
185 if err != nil {
186 log.E.F("failed to find authoritative config: %v", err)
187 return
188 }
189
190 if authConfig != nil && peerUpdater != nil {
191 // Update the sync manager's peer list
192 peerUpdater.UpdatePeers(authConfig.Relays)
193 }
194 }
195