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