Files
next.orly.dev/pkg/sync/relaygroup.go
2025-11-23 08:15:06 +00:00

160 lines
4.3 KiB
Go

// Package sync provides relay group configuration management
package sync
import (
"context"
"github.com/minio/sha256-simd"
"encoding/hex"
"encoding/json"
"sort"
"strings"
"lol.mleku.dev/log"
"next.orly.dev/pkg/database"
"git.mleku.dev/mleku/nostr/encoders/bech32encoding"
"git.mleku.dev/mleku/nostr/encoders/event"
"git.mleku.dev/mleku/nostr/encoders/filter"
"git.mleku.dev/mleku/nostr/encoders/kind"
"git.mleku.dev/mleku/nostr/encoders/tag"
)
// RelayGroupConfig represents a relay group configuration event
type RelayGroupConfig struct {
Relays []string `json:"relays"`
}
// RelayGroupManager handles relay group configuration
type RelayGroupManager struct {
db *database.D
authorizedPubkeys [][]byte
}
// NewRelayGroupManager creates a new relay group manager
func NewRelayGroupManager(db *database.D, adminNpubs []string) *RelayGroupManager {
var pubkeys [][]byte
for _, npub := range adminNpubs {
if pk, err := bech32encoding.NpubOrHexToPublicKeyBinary(npub); err == nil {
pubkeys = append(pubkeys, pk)
}
}
return &RelayGroupManager{
db: db,
authorizedPubkeys: pubkeys,
}
}
// FindAuthoritativeConfig finds the authoritative relay group configuration
// by selecting the latest event by timestamp, with hash tie-breaking
func (rgm *RelayGroupManager) FindAuthoritativeConfig(ctx context.Context) (*RelayGroupConfig, error) {
if len(rgm.authorizedPubkeys) == 0 {
return nil, nil
}
// Query for all relay group config events from authorized pubkeys
f := &filter.F{
Kinds: kind.NewS(kind.RelayGroupConfig),
Authors: tag.NewFromBytesSlice(rgm.authorizedPubkeys...),
}
events, err := rgm.db.QueryEvents(ctx, f)
if err != nil {
return nil, err
}
if len(events) == 0 {
return nil, nil
}
// Find the authoritative event
authEvent := rgm.selectAuthoritativeEvent(events)
if authEvent == nil {
return nil, nil
}
// Parse the configuration from the event content
var config RelayGroupConfig
if err := json.Unmarshal([]byte(authEvent.Content), &config); err != nil {
return nil, err
}
return &config, nil
}
// selectAuthoritativeEvent selects the authoritative event using the specified criteria
func (rgm *RelayGroupManager) selectAuthoritativeEvent(events []*event.E) *event.E {
if len(events) == 0 {
return nil
}
// Sort events by timestamp (newest first), then by hash (smallest first)
sort.Slice(events, func(i, j int) bool {
// First compare timestamps (newest first)
if events[i].CreatedAt != events[j].CreatedAt {
return events[i].CreatedAt > events[j].CreatedAt
}
// If timestamps are equal, compare hashes (smallest first)
hashI := sha256.Sum256([]byte(events[i].ID))
hashJ := sha256.Sum256([]byte(events[j].ID))
return strings.Compare(hex.EncodeToString(hashI[:]), hex.EncodeToString(hashJ[:])) < 0
})
return events[0]
}
// IsAuthorizedPublisher checks if a pubkey is authorized to publish relay group configs
func (rgm *RelayGroupManager) IsAuthorizedPublisher(pubkey []byte) bool {
for _, authPK := range rgm.authorizedPubkeys {
if string(authPK) == string(pubkey) {
return true
}
}
return false
}
// ValidateRelayGroupEvent validates a relay group configuration event
func (rgm *RelayGroupManager) ValidateRelayGroupEvent(ev *event.E) error {
// Check if it's the right kind
if ev.Kind != kind.RelayGroupConfig.K {
return nil // Not our concern
}
// Check if publisher is authorized
if !rgm.IsAuthorizedPublisher(ev.Pubkey) {
return nil // Not our concern, but won't be considered authoritative
}
// Try to parse the content
var config RelayGroupConfig
if err := json.Unmarshal([]byte(ev.Content), &config); err != nil {
return err
}
// Basic validation - at least one relay should be specified
if len(config.Relays) == 0 {
return nil // Empty config is allowed, just won't be selected
}
return nil
}
// HandleRelayGroupEvent processes a relay group configuration event and updates peer lists
func (rgm *RelayGroupManager) HandleRelayGroupEvent(ev *event.E, syncManager *Manager) {
if ev.Kind != kind.RelayGroupConfig.K {
return
}
// Check if this event is the new authoritative configuration
authConfig, err := rgm.FindAuthoritativeConfig(context.Background())
if err != nil {
log.E.F("failed to find authoritative config: %v", err)
return
}
if authConfig != nil {
// Update the sync manager's peer list
syncManager.UpdatePeers(authConfig.Relays)
}
}