main.go raw

   1  // orly-acl-follows is a standalone gRPC ACL server using the Follows mode.
   2  // It whitelists users who are followed by the relay admins.
   3  package main
   4  
   5  import (
   6  	"context"
   7  	"os"
   8  	"path/filepath"
   9  	"strings"
  10  	"time"
  11  
  12  	"go-simpler.org/env"
  13  	"next.orly.dev/pkg/lol"
  14  	"next.orly.dev/pkg/lol/chk"
  15  	"next.orly.dev/pkg/lol/log"
  16  
  17  	"next.orly.dev/pkg/acl/server"
  18  	"next.orly.dev/pkg/database"
  19  	databasegrpc "next.orly.dev/pkg/database/grpc"
  20  )
  21  
  22  // Config holds the ACL server configuration.
  23  type Config struct {
  24  	// Listen is the gRPC server listen address
  25  	Listen string `env:"ORLY_ACL_LISTEN" default:"127.0.0.1:50052" usage:"gRPC server listen address"`
  26  
  27  	// LogLevel is the logging level
  28  	LogLevel string `env:"ORLY_ACL_LOG_LEVEL" default:"info" usage:"log level (trace, debug, info, warn, error)"`
  29  
  30  	// Database configuration
  31  	DBType       string `env:"ORLY_ACL_DB_TYPE" default:"grpc" usage:"database type: badger or grpc"`
  32  	GRPCDBServer string `env:"ORLY_ACL_GRPC_DB_SERVER" usage:"gRPC database server address (when DB_TYPE=grpc)"`
  33  	DataDir      string `env:"ORLY_DATA_DIR" usage:"database data directory (when DB_TYPE=badger)"`
  34  
  35  	// Badger configuration (when DB_TYPE=badger)
  36  	BlockCacheMB        int           `env:"ORLY_DB_BLOCK_CACHE_MB" default:"256" usage:"block cache size in MB"`
  37  	IndexCacheMB        int           `env:"ORLY_DB_INDEX_CACHE_MB" default:"128" usage:"index cache size in MB"`
  38  	ZSTDLevel           int           `env:"ORLY_DB_ZSTD_LEVEL" default:"3" usage:"ZSTD compression level"`
  39  	QueryCacheSizeMB    int           `env:"ORLY_DB_QUERY_CACHE_SIZE_MB" default:"64" usage:"query cache size in MB"`
  40  	QueryCacheMaxAge    time.Duration `env:"ORLY_DB_QUERY_CACHE_MAX_AGE" default:"5m" usage:"query cache max age"`
  41  	QueryCacheDisabled  bool          `env:"ORLY_DB_QUERY_CACHE_DISABLED" default:"false" usage:"disable query cache"`
  42  	SerialCachePubkeys  int           `env:"ORLY_SERIAL_CACHE_PUBKEYS" default:"100000" usage:"serial cache pubkeys capacity"`
  43  	SerialCacheEventIds int           `env:"ORLY_SERIAL_CACHE_EVENT_IDS" default:"500000" usage:"serial cache event IDs capacity"`
  44  
  45  	// ACL configuration
  46  	Owners          string `env:"ORLY_OWNERS" usage:"comma-separated list of owner npubs"`
  47  	Admins          string `env:"ORLY_ADMINS" usage:"comma-separated list of admin npubs"`
  48  	BootstrapRelays string `env:"ORLY_BOOTSTRAP_RELAYS" usage:"comma-separated list of bootstrap relays"`
  49  	RelayAddresses  string `env:"ORLY_RELAY_ADDRESSES" usage:"comma-separated list of relay addresses (self)"`
  50  
  51  	// Follows ACL configuration
  52  	FollowListFrequency     time.Duration `env:"ORLY_FOLLOW_LIST_FREQUENCY" default:"1h" usage:"follow list sync frequency"`
  53  	FollowsThrottleEnabled  bool          `env:"ORLY_FOLLOWS_THROTTLE_ENABLED" default:"false" usage:"enable progressive throttle for non-followed users"`
  54  	FollowsThrottlePerEvent time.Duration `env:"ORLY_FOLLOWS_THROTTLE_PER_EVENT" default:"25ms" usage:"throttle delay increment per event"`
  55  	FollowsThrottleMaxDelay time.Duration `env:"ORLY_FOLLOWS_THROTTLE_MAX_DELAY" default:"60s" usage:"maximum throttle delay"`
  56  }
  57  
  58  func main() {
  59  	cfg := loadConfig()
  60  
  61  	// Set log level
  62  	lol.SetLogLevel(cfg.LogLevel)
  63  	log.I.F("orly-acl-follows starting with log level: %s", cfg.LogLevel)
  64  
  65  	ctx, cancel := context.WithCancel(context.Background())
  66  	defer cancel()
  67  
  68  	// Initialize database (direct Badger or gRPC client)
  69  	var db database.Database
  70  	var err error
  71  	var ownsDB bool
  72  
  73  	if cfg.DBType == "grpc" {
  74  		// Use gRPC database client
  75  		log.I.F("connecting to gRPC database server at %s", cfg.GRPCDBServer)
  76  		db, err = databasegrpc.New(ctx, &databasegrpc.ClientConfig{
  77  			ServerAddress:  cfg.GRPCDBServer,
  78  			ConnectTimeout: 30 * time.Second,
  79  		})
  80  		if chk.E(err) {
  81  			log.E.F("failed to connect to gRPC database: %v", err)
  82  			os.Exit(1)
  83  		}
  84  		ownsDB = false // gRPC client doesn't own the database
  85  	} else {
  86  		// Use direct Badger database
  87  		dbCfg := &database.DatabaseConfig{
  88  			DataDir:             cfg.DataDir,
  89  			LogLevel:            cfg.LogLevel,
  90  			BlockCacheMB:        cfg.BlockCacheMB,
  91  			IndexCacheMB:        cfg.IndexCacheMB,
  92  			QueryCacheSizeMB:    cfg.QueryCacheSizeMB,
  93  			QueryCacheMaxAge:    cfg.QueryCacheMaxAge,
  94  			QueryCacheDisabled:  cfg.QueryCacheDisabled,
  95  			SerialCachePubkeys:  cfg.SerialCachePubkeys,
  96  			SerialCacheEventIds: cfg.SerialCacheEventIds,
  97  			ZSTDLevel:           cfg.ZSTDLevel,
  98  		}
  99  
 100  		log.I.F("initializing Badger database at %s", cfg.DataDir)
 101  		db, err = database.NewWithConfig(ctx, cancel, dbCfg)
 102  		if chk.E(err) {
 103  			log.E.F("failed to initialize database: %v", err)
 104  			os.Exit(1)
 105  		}
 106  		ownsDB = true
 107  	}
 108  
 109  	// Wait for database to be ready
 110  	log.I.F("waiting for database to be ready...")
 111  	<-db.Ready()
 112  	log.I.F("database ready")
 113  
 114  	// Create server config
 115  	serverCfg := &server.Config{
 116  		Listen:                  cfg.Listen,
 117  		ACLMode:                 "follows", // Hardcoded for this binary
 118  		LogLevel:                cfg.LogLevel,
 119  		Owners:                  splitList(cfg.Owners),
 120  		Admins:                  splitList(cfg.Admins),
 121  		BootstrapRelays:         splitList(cfg.BootstrapRelays),
 122  		RelayAddresses:          splitList(cfg.RelayAddresses),
 123  		FollowListFrequency:     cfg.FollowListFrequency,
 124  		FollowsThrottleEnabled:  cfg.FollowsThrottleEnabled,
 125  		FollowsThrottlePerEvent: cfg.FollowsThrottlePerEvent,
 126  		FollowsThrottleMaxDelay: cfg.FollowsThrottleMaxDelay,
 127  	}
 128  
 129  	// Create and configure server
 130  	srv := server.New(db, serverCfg, ownsDB)
 131  	if err := srv.ConfigureACL(ctx); chk.E(err) {
 132  		// Don't exit on configure error - the syncer will populate follows from
 133  		// external relays. This handles empty databases gracefully.
 134  		log.W.F("ACL configure returned error (will start with 0 follows): %v", err)
 135  	}
 136  
 137  	// Start server
 138  	if err := srv.ListenAndServe(ctx, cancel); err != nil {
 139  		log.E.F("gRPC server error: %v", err)
 140  	}
 141  }
 142  
 143  func loadConfig() *Config {
 144  	cfg := &Config{}
 145  	if err := env.Load(cfg, nil); chk.E(err) {
 146  		log.E.F("failed to load config: %v", err)
 147  		os.Exit(1)
 148  	}
 149  
 150  	// Set default data directory if not specified
 151  	if cfg.DataDir == "" {
 152  		home, err := os.UserHomeDir()
 153  		if chk.E(err) {
 154  			log.E.F("failed to get home directory: %v", err)
 155  			os.Exit(1)
 156  		}
 157  		cfg.DataDir = filepath.Join(home, ".local", "share", "ORLY")
 158  	}
 159  
 160  	// Ensure data directory exists (for badger mode)
 161  	if cfg.DBType == "badger" {
 162  		if err := os.MkdirAll(cfg.DataDir, 0700); chk.E(err) {
 163  			log.E.F("failed to create data directory %s: %v", cfg.DataDir, err)
 164  			os.Exit(1)
 165  		}
 166  	}
 167  
 168  	return cfg
 169  }
 170  
 171  func splitList(s string) []string {
 172  	if s == "" {
 173  		return nil
 174  	}
 175  	return strings.Split(s, ",")
 176  }
 177