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