main.go raw
1 // orly-acl-curation is a standalone gRPC ACL server using the Curating mode.
2 // It provides three-tier classification: trusted, blacklisted, and unclassified (rate-limited).
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 RelayAddresses string `env:"ORLY_RELAY_ADDRESSES" usage:"comma-separated list of relay addresses (self)"`
49 }
50
51 func main() {
52 cfg := loadConfig()
53
54 // Set log level
55 lol.SetLogLevel(cfg.LogLevel)
56 log.I.F("orly-acl-curation starting with log level: %s", cfg.LogLevel)
57
58 ctx, cancel := context.WithCancel(context.Background())
59 defer cancel()
60
61 // Initialize database (direct Badger or gRPC client)
62 var db database.Database
63 var err error
64 var ownsDB bool
65
66 if cfg.DBType == "grpc" {
67 // Use gRPC database client
68 log.I.F("connecting to gRPC database server at %s", cfg.GRPCDBServer)
69 db, err = databasegrpc.New(ctx, &databasegrpc.ClientConfig{
70 ServerAddress: cfg.GRPCDBServer,
71 ConnectTimeout: 30 * time.Second,
72 })
73 if chk.E(err) {
74 log.E.F("failed to connect to gRPC database: %v", err)
75 os.Exit(1)
76 }
77 ownsDB = false // gRPC client doesn't own the database
78 } else {
79 // Use direct Badger database
80 dbCfg := &database.DatabaseConfig{
81 DataDir: cfg.DataDir,
82 LogLevel: cfg.LogLevel,
83 BlockCacheMB: cfg.BlockCacheMB,
84 IndexCacheMB: cfg.IndexCacheMB,
85 QueryCacheSizeMB: cfg.QueryCacheSizeMB,
86 QueryCacheMaxAge: cfg.QueryCacheMaxAge,
87 QueryCacheDisabled: cfg.QueryCacheDisabled,
88 SerialCachePubkeys: cfg.SerialCachePubkeys,
89 SerialCacheEventIds: cfg.SerialCacheEventIds,
90 ZSTDLevel: cfg.ZSTDLevel,
91 }
92
93 log.I.F("initializing Badger database at %s", cfg.DataDir)
94 db, err = database.NewWithConfig(ctx, cancel, dbCfg)
95 if chk.E(err) {
96 log.E.F("failed to initialize database: %v", err)
97 os.Exit(1)
98 }
99 ownsDB = true
100 }
101
102 // Wait for database to be ready
103 log.I.F("waiting for database to be ready...")
104 <-db.Ready()
105 log.I.F("database ready")
106
107 // Create server config
108 serverCfg := &server.Config{
109 Listen: cfg.Listen,
110 ACLMode: "curating", // Hardcoded for this binary
111 LogLevel: cfg.LogLevel,
112 Owners: splitList(cfg.Owners),
113 Admins: splitList(cfg.Admins),
114 RelayAddresses: splitList(cfg.RelayAddresses),
115 }
116
117 // Create and configure server
118 srv := server.New(db, serverCfg, ownsDB)
119 if err := srv.ConfigureACL(ctx); chk.E(err) {
120 log.E.F("failed to configure ACL: %v", err)
121 os.Exit(1)
122 }
123
124 // Start server
125 if err := srv.ListenAndServe(ctx, cancel); err != nil {
126 log.E.F("gRPC server error: %v", err)
127 }
128 }
129
130 func loadConfig() *Config {
131 cfg := &Config{}
132 if err := env.Load(cfg, nil); chk.E(err) {
133 log.E.F("failed to load config: %v", err)
134 os.Exit(1)
135 }
136
137 // Set default data directory if not specified
138 if cfg.DataDir == "" {
139 home, err := os.UserHomeDir()
140 if chk.E(err) {
141 log.E.F("failed to get home directory: %v", err)
142 os.Exit(1)
143 }
144 cfg.DataDir = filepath.Join(home, ".local", "share", "ORLY")
145 }
146
147 // Ensure data directory exists (for badger mode)
148 if cfg.DBType == "badger" || cfg.DBType == "" {
149 if err := os.MkdirAll(cfg.DataDir, 0700); chk.E(err) {
150 log.E.F("failed to create data directory %s: %v", cfg.DataDir, err)
151 os.Exit(1)
152 }
153 }
154
155 return cfg
156 }
157
158 func splitList(s string) []string {
159 if s == "" {
160 return nil
161 }
162 return strings.Split(s, ",")
163 }
164