Browse Source

Add split database and ACL binaries for modular deployment (v0.54.0)

- Split orly-db into orly-db-badger and orly-db-neo4j backends
- Split orly-acl into orly-acl-follows, orly-acl-managed, orly-acl-curation
- Add shared pkg/database/server/ for gRPC database service
- Add shared pkg/acl/server/ for gRPC ACL service
- Add pkg/acl/grpc/ client for relay-to-ACL communication
- Add gRPC proto definitions for ACL service (pkg/proto/orlyacl/)
- Update launcher to compute binary names from backend/mode settings
- When ACL disabled, relay runs in open mode (no restrictions)
- Add Makefile with all-split, arm64-split, and per-binary targets
- Include systemd service file for split IPC deployment

Files modified:
- Makefile: New build system with split binary targets
- app/config/config.go: Add ACL gRPC client config options
- cmd/orly-acl/: Legacy monolithic ACL server
- cmd/orly-acl-curation/main.go: Curation mode binary
- cmd/orly-acl-follows/main.go: Follows mode binary
- cmd/orly-acl-managed/main.go: Managed mode binary
- cmd/orly-db-badger/main.go: Badger backend binary
- cmd/orly-db-neo4j/main.go: Neo4j backend binary
- cmd/orly-launcher/config.go: Add DBBackend, compute binary names
- cmd/orly-launcher/supervisor.go: Handle no-ACL mode, add ACL health check
- main.go: Add gRPC ACL client initialization
- orly.service: Systemd service for split IPC mode
- pkg/acl/acl.go: Add gRPC ACL interface type
- pkg/acl/grpc/client.go: gRPC ACL client implementation
- pkg/acl/server/: Shared ACL gRPC server package
- pkg/database/server/: Shared database gRPC server package
- pkg/proto/orlyacl/: ACL service protobuf definitions
- pkg/version/version: v0.54.0
- proto/orlyacl/: ACL proto source files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
main v0.54.0
woikos 4 months ago
parent
commit
e20920d39b
No known key found for this signature in database
  1. 194
      Makefile
  2. 18
      app/config/config.go
  3. 163
      cmd/orly-acl-curation/main.go
  4. 175
      cmd/orly-acl-follows/main.go
  5. 138
      cmd/orly-acl-managed/main.go
  6. 112
      cmd/orly-acl/config.go
  7. 167
      cmd/orly-acl/main.go
  8. 788
      cmd/orly-acl/service.go
  9. 122
      cmd/orly-db-badger/main.go
  10. 108
      cmd/orly-db-neo4j/main.go
  11. 52
      cmd/orly-launcher/config.go
  12. 131
      cmd/orly-launcher/supervisor.go
  13. 36
      main.go
  14. 66
      orly.service
  15. 7
      pkg/acl/acl.go
  16. 384
      pkg/acl/grpc/client.go
  17. 32
      pkg/acl/server/config.go
  18. 144
      pkg/acl/server/server.go
  19. 788
      pkg/acl/server/service.go
  20. 33
      pkg/database/server/config.go
  21. 111
      pkg/database/server/server.go
  22. 731
      pkg/database/server/service.go
  23. 250
      pkg/proto/orlyacl/v1/acl.pb.go
  24. 1887
      pkg/proto/orlyacl/v1/acl_grpc.pb.go
  25. 3234
      pkg/proto/orlyacl/v1/types.pb.go
  26. 2
      pkg/version/version
  27. 151
      proto/orlyacl/v1/acl.proto
  28. 292
      proto/orlyacl/v1/types.proto

194
Makefile

@ -0,0 +1,194 @@ @@ -0,0 +1,194 @@
# ORLY Nostr Relay Build System
.PHONY: all orly orly-db orly-acl orly-launcher proto clean test deploy web install help
.PHONY: orly-db-badger orly-db-neo4j orly-acl-follows orly-acl-managed orly-acl-curation
.PHONY: all-split arm64-split install-split
# Build flags
CGO_ENABLED ?= 0
GOOS ?= $(shell go env GOOS)
GOARCH ?= $(shell go env GOARCH)
BUILD_FLAGS = CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH)
# Binaries
BIN_DIR = .
ORLY = $(BIN_DIR)/orly
ORLY_LAUNCHER = $(BIN_DIR)/orly-launcher
# Legacy monolithic binaries (for backwards compatibility)
ORLY_DB = $(BIN_DIR)/orly-db
ORLY_ACL = $(BIN_DIR)/orly-acl
# Split database backends
ORLY_DB_BADGER = $(BIN_DIR)/orly-db-badger
ORLY_DB_NEO4J = $(BIN_DIR)/orly-db-neo4j
# Split ACL modes
ORLY_ACL_FOLLOWS = $(BIN_DIR)/orly-acl-follows
ORLY_ACL_MANAGED = $(BIN_DIR)/orly-acl-managed
ORLY_ACL_CURATION = $(BIN_DIR)/orly-acl-curation
# === Default Targets (Legacy) ===
# Default target: build everything (legacy monolithic)
all: orly orly-db orly-launcher
@echo "All binaries built successfully"
# Build everything including ACL (when proto exists)
all-acl: proto orly orly-db orly-acl orly-launcher
@echo "All binaries (including ACL) built successfully"
# === Split Binaries (New) ===
# Build split mode: orly + orly-db-badger + orly-acl-follows + launcher
all-split: proto orly orly-db-badger orly-acl-follows orly-launcher
@echo "Split mode binaries built successfully"
# Build all split binaries (all backends and all ACL modes)
all-backends: proto orly orly-db-badger orly-db-neo4j orly-acl-follows orly-acl-managed orly-acl-curation orly-launcher
@echo "All backend and ACL mode binaries built successfully"
# Main relay binary
orly:
$(BUILD_FLAGS) go build -o $(ORLY) .
# === Database Backends ===
# Legacy monolithic database server
orly-db:
$(BUILD_FLAGS) go build -o $(ORLY_DB) ./cmd/orly-db
# Badger database server
orly-db-badger:
$(BUILD_FLAGS) go build -o $(ORLY_DB_BADGER) ./cmd/orly-db-badger
# Neo4j database server
orly-db-neo4j:
$(BUILD_FLAGS) go build -o $(ORLY_DB_NEO4J) ./cmd/orly-db-neo4j
# === ACL Modes ===
# Legacy monolithic ACL server (requires proto generation first)
orly-acl:
$(BUILD_FLAGS) go build -o $(ORLY_ACL) ./cmd/orly-acl
# Follows ACL server (whitelist based on admin follows)
orly-acl-follows:
$(BUILD_FLAGS) go build -o $(ORLY_ACL_FOLLOWS) ./cmd/orly-acl-follows
# Managed ACL server (NIP-86 fine-grained control)
orly-acl-managed:
$(BUILD_FLAGS) go build -o $(ORLY_ACL_MANAGED) ./cmd/orly-acl-managed
# Curation ACL server (rate-limited trust tiers)
orly-acl-curation:
$(BUILD_FLAGS) go build -o $(ORLY_ACL_CURATION) ./cmd/orly-acl-curation
# Process supervisor/launcher
orly-launcher:
$(BUILD_FLAGS) go build -o $(ORLY_LAUNCHER) ./cmd/orly-launcher
# Generate protobuf code
proto:
cd proto && buf generate
# === Cross-Compile for ARM64 ===
# Build for ARM64 (relay.orly.dev deployment) - legacy
arm64:
$(MAKE) GOOS=linux GOARCH=arm64 all
# Build everything for ARM64 including ACL - legacy
arm64-acl:
$(MAKE) GOOS=linux GOARCH=arm64 all-acl
# Build split mode for ARM64 (recommended for relay.orly.dev)
arm64-split:
$(MAKE) GOOS=linux GOARCH=arm64 all-split
# Build all backends for ARM64
arm64-backends:
$(MAKE) GOOS=linux GOARCH=arm64 all-backends
# === Other Targets ===
# Build web UI and embed
web:
./scripts/update-embedded-web.sh
# Clean build artifacts
clean:
rm -f $(ORLY) $(ORLY_DB) $(ORLY_ACL) $(ORLY_LAUNCHER)
rm -f $(ORLY_DB_BADGER) $(ORLY_DB_NEO4J)
rm -f $(ORLY_ACL_FOLLOWS) $(ORLY_ACL_MANAGED) $(ORLY_ACL_CURATION)
rm -f orly-db-arm64 orly-acl-arm64 orly-launcher-arm64 next.orly.dev
# Run tests
test:
./scripts/test.sh
# Deploy to relay.orly.dev (builds on remote) - legacy
deploy:
ssh relay.orly.dev 'cd ~/src/next.orly.dev && git pull origin main && make all && sudo /usr/sbin/setcap cap_net_bind_service=+ep ~/.local/bin/next.orly.dev && sudo systemctl restart orly'
# Deploy with ACL server - legacy
deploy-acl:
ssh relay.orly.dev 'cd ~/src/next.orly.dev && git pull origin main && make all-acl && sudo /usr/sbin/setcap cap_net_bind_service=+ep ~/.local/bin/next.orly.dev && sudo systemctl restart orly'
# Deploy split mode (recommended)
deploy-split:
ssh relay.orly.dev 'cd ~/src/next.orly.dev && git pull origin main && make all-split && sudo /usr/sbin/setcap cap_net_bind_service=+ep ~/.local/bin/next.orly.dev && sudo systemctl restart orly'
# Install all binaries locally - legacy
install: all
mkdir -p ~/.local/bin
cp $(ORLY) $(ORLY_DB) $(ORLY_LAUNCHER) ~/.local/bin/
# Install including ACL - legacy
install-acl: all-acl
mkdir -p ~/.local/bin
cp $(ORLY) $(ORLY_DB) $(ORLY_ACL) $(ORLY_LAUNCHER) ~/.local/bin/
# Install split mode binaries
install-split: all-split
mkdir -p ~/.local/bin
cp $(ORLY) $(ORLY_DB_BADGER) $(ORLY_ACL_FOLLOWS) $(ORLY_LAUNCHER) ~/.local/bin/
# Install all backends and modes
install-backends: all-backends
mkdir -p ~/.local/bin
cp $(ORLY) $(ORLY_DB_BADGER) $(ORLY_DB_NEO4J) $(ORLY_ACL_FOLLOWS) $(ORLY_ACL_MANAGED) $(ORLY_ACL_CURATION) $(ORLY_LAUNCHER) ~/.local/bin/
# Help
help:
@echo "ORLY Build Targets:"
@echo ""
@echo " Split Mode (Recommended):"
@echo " all-split - Build orly + orly-db-badger + orly-acl-follows + launcher"
@echo " all-backends - Build all database backends and ACL modes"
@echo " arm64-split - Cross-compile split mode for ARM64"
@echo " deploy-split - Deploy split mode to relay.orly.dev"
@echo " install-split - Install split mode to ~/.local/bin/"
@echo ""
@echo " Database Backends:"
@echo " orly-db-badger - Build Badger database server"
@echo " orly-db-neo4j - Build Neo4j database server"
@echo ""
@echo " ACL Modes:"
@echo " orly-acl-follows - Build Follows ACL server (whitelist)"
@echo " orly-acl-managed - Build Managed ACL server (NIP-86)"
@echo " orly-acl-curation - Build Curation ACL server (rate limits)"
@echo ""
@echo " Legacy (Monolithic):"
@echo " all - Build orly + orly-db + launcher"
@echo " all-acl - Build all including orly-acl"
@echo " orly-db - Build monolithic database server"
@echo " orly-acl - Build monolithic ACL server"
@echo ""
@echo " Core:"
@echo " orly - Build main relay binary"
@echo " orly-launcher - Build process supervisor"
@echo " proto - Generate protobuf code"
@echo " web - Rebuild embedded web UI"
@echo " test - Run test suite"
@echo " clean - Remove build artifacts"
@echo " help - Show this help"

18
app/config/config.go

@ -123,6 +123,11 @@ type C struct { @@ -123,6 +123,11 @@ type C struct {
GRPCServerAddress string `env:"ORLY_GRPC_SERVER" usage:"address of remote gRPC database server (only used when ORLY_DB_TYPE=grpc)"`
GRPCConnectTimeout time.Duration `env:"ORLY_GRPC_CONNECT_TIMEOUT" default:"10s" usage:"gRPC connection timeout (only used when ORLY_DB_TYPE=grpc)"`
// gRPC ACL client settings (only used when ORLY_ACL_TYPE=grpc)
ACLType string `env:"ORLY_ACL_TYPE" default:"local" usage:"ACL backend: local (in-process) or grpc (remote ACL server)"`
GRPCACLServerAddress string `env:"ORLY_GRPC_ACL_SERVER" usage:"address of remote gRPC ACL server (only used when ORLY_ACL_TYPE=grpc)"`
GRPCACLConnectTimeout time.Duration `env:"ORLY_GRPC_ACL_TIMEOUT" default:"10s" usage:"gRPC ACL connection timeout (only used when ORLY_ACL_TYPE=grpc)"`
QueryCacheSizeMB int `env:"ORLY_QUERY_CACHE_SIZE_MB" default:"512" usage:"query cache size in MB (caches database query results for faster REQ responses)"`
QueryCacheMaxAge string `env:"ORLY_QUERY_CACHE_MAX_AGE" default:"5m" usage:"maximum age for cached query results (e.g., 5m, 10m, 1h)"`
@ -843,3 +848,16 @@ func (cfg *C) GetGRPCConfigValues() ( @@ -843,3 +848,16 @@ func (cfg *C) GetGRPCConfigValues() (
return cfg.GRPCServerAddress,
cfg.GRPCConnectTimeout
}
// GetGRPCACLConfigValues returns the gRPC ACL client configuration values.
// This avoids circular imports with pkg/acl/grpc while allowing main.go to construct
// the gRPC ACL client configuration.
func (cfg *C) GetGRPCACLConfigValues() (
aclType string,
serverAddress string,
connectTimeout time.Duration,
) {
return cfg.ACLType,
cfg.GRPCACLServerAddress,
cfg.GRPCACLConnectTimeout
}

163
cmd/orly-acl-curation/main.go

@ -0,0 +1,163 @@ @@ -0,0 +1,163 @@
// orly-acl-curation is a standalone gRPC ACL server using the Curating mode.
// It provides three-tier classification: trusted, blacklisted, and unclassified (rate-limited).
package main
import (
"context"
"os"
"path/filepath"
"strings"
"time"
"go-simpler.org/env"
"lol.mleku.dev"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/pkg/acl/server"
"next.orly.dev/pkg/database"
databasegrpc "next.orly.dev/pkg/database/grpc"
)
// Config holds the ACL server configuration.
type Config struct {
// Listen is the gRPC server listen address
Listen string `env:"ORLY_ACL_LISTEN" default:"127.0.0.1:50052" usage:"gRPC server listen address"`
// LogLevel is the logging level
LogLevel string `env:"ORLY_ACL_LOG_LEVEL" default:"info" usage:"log level (trace, debug, info, warn, error)"`
// Database configuration
DBType string `env:"ORLY_ACL_DB_TYPE" default:"grpc" usage:"database type: badger or grpc"`
GRPCDBServer string `env:"ORLY_ACL_GRPC_DB_SERVER" usage:"gRPC database server address (when DB_TYPE=grpc)"`
DataDir string `env:"ORLY_DATA_DIR" usage:"database data directory (when DB_TYPE=badger)"`
// Badger configuration (when DB_TYPE=badger)
BlockCacheMB int `env:"ORLY_DB_BLOCK_CACHE_MB" default:"256" usage:"block cache size in MB"`
IndexCacheMB int `env:"ORLY_DB_INDEX_CACHE_MB" default:"128" usage:"index cache size in MB"`
ZSTDLevel int `env:"ORLY_DB_ZSTD_LEVEL" default:"3" usage:"ZSTD compression level"`
QueryCacheSizeMB int `env:"ORLY_DB_QUERY_CACHE_SIZE_MB" default:"64" usage:"query cache size in MB"`
QueryCacheMaxAge time.Duration `env:"ORLY_DB_QUERY_CACHE_MAX_AGE" default:"5m" usage:"query cache max age"`
QueryCacheDisabled bool `env:"ORLY_DB_QUERY_CACHE_DISABLED" default:"false" usage:"disable query cache"`
SerialCachePubkeys int `env:"ORLY_SERIAL_CACHE_PUBKEYS" default:"100000" usage:"serial cache pubkeys capacity"`
SerialCacheEventIds int `env:"ORLY_SERIAL_CACHE_EVENT_IDS" default:"500000" usage:"serial cache event IDs capacity"`
// ACL configuration
Owners string `env:"ORLY_OWNERS" usage:"comma-separated list of owner npubs"`
Admins string `env:"ORLY_ADMINS" usage:"comma-separated list of admin npubs"`
RelayAddresses string `env:"ORLY_RELAY_ADDRESSES" usage:"comma-separated list of relay addresses (self)"`
}
func main() {
cfg := loadConfig()
// Set log level
lol.SetLogLevel(cfg.LogLevel)
log.I.F("orly-acl-curation starting with log level: %s", cfg.LogLevel)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Initialize database (direct Badger or gRPC client)
var db database.Database
var err error
var ownsDB bool
if cfg.DBType == "grpc" {
// Use gRPC database client
log.I.F("connecting to gRPC database server at %s", cfg.GRPCDBServer)
db, err = databasegrpc.New(ctx, &databasegrpc.ClientConfig{
ServerAddress: cfg.GRPCDBServer,
ConnectTimeout: 30 * time.Second,
})
if chk.E(err) {
log.E.F("failed to connect to gRPC database: %v", err)
os.Exit(1)
}
ownsDB = false // gRPC client doesn't own the database
} else {
// Use direct Badger database
dbCfg := &database.DatabaseConfig{
DataDir: cfg.DataDir,
LogLevel: cfg.LogLevel,
BlockCacheMB: cfg.BlockCacheMB,
IndexCacheMB: cfg.IndexCacheMB,
QueryCacheSizeMB: cfg.QueryCacheSizeMB,
QueryCacheMaxAge: cfg.QueryCacheMaxAge,
QueryCacheDisabled: cfg.QueryCacheDisabled,
SerialCachePubkeys: cfg.SerialCachePubkeys,
SerialCacheEventIds: cfg.SerialCacheEventIds,
ZSTDLevel: cfg.ZSTDLevel,
}
log.I.F("initializing Badger database at %s", cfg.DataDir)
db, err = database.NewWithConfig(ctx, cancel, dbCfg)
if chk.E(err) {
log.E.F("failed to initialize database: %v", err)
os.Exit(1)
}
ownsDB = true
}
// Wait for database to be ready
log.I.F("waiting for database to be ready...")
<-db.Ready()
log.I.F("database ready")
// Create server config
serverCfg := &server.Config{
Listen: cfg.Listen,
ACLMode: "curating", // Hardcoded for this binary
LogLevel: cfg.LogLevel,
Owners: splitList(cfg.Owners),
Admins: splitList(cfg.Admins),
RelayAddresses: splitList(cfg.RelayAddresses),
}
// Create and configure server
srv := server.New(db, serverCfg, ownsDB)
if err := srv.ConfigureACL(ctx); chk.E(err) {
log.E.F("failed to configure ACL: %v", err)
os.Exit(1)
}
// Start server
if err := srv.ListenAndServe(ctx, cancel); err != nil {
log.E.F("gRPC server error: %v", err)
}
}
func loadConfig() *Config {
cfg := &Config{}
if err := env.Load(cfg, nil); chk.E(err) {
log.E.F("failed to load config: %v", err)
os.Exit(1)
}
// Set default data directory if not specified
if cfg.DataDir == "" {
home, err := os.UserHomeDir()
if chk.E(err) {
log.E.F("failed to get home directory: %v", err)
os.Exit(1)
}
cfg.DataDir = filepath.Join(home, ".local", "share", "ORLY")
}
// Ensure data directory exists (for badger mode)
if cfg.DBType == "badger" || cfg.DBType == "" {
if err := os.MkdirAll(cfg.DataDir, 0700); chk.E(err) {
log.E.F("failed to create data directory %s: %v", cfg.DataDir, err)
os.Exit(1)
}
}
return cfg
}
func splitList(s string) []string {
if s == "" {
return nil
}
return strings.Split(s, ",")
}

175
cmd/orly-acl-follows/main.go

@ -0,0 +1,175 @@ @@ -0,0 +1,175 @@
// orly-acl-follows is a standalone gRPC ACL server using the Follows mode.
// It whitelists users who are followed by the relay admins.
package main
import (
"context"
"os"
"path/filepath"
"strings"
"time"
"go-simpler.org/env"
"lol.mleku.dev"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/pkg/acl/server"
"next.orly.dev/pkg/database"
databasegrpc "next.orly.dev/pkg/database/grpc"
)
// Config holds the ACL server configuration.
type Config struct {
// Listen is the gRPC server listen address
Listen string `env:"ORLY_ACL_LISTEN" default:"127.0.0.1:50052" usage:"gRPC server listen address"`
// LogLevel is the logging level
LogLevel string `env:"ORLY_ACL_LOG_LEVEL" default:"info" usage:"log level (trace, debug, info, warn, error)"`
// Database configuration
DBType string `env:"ORLY_ACL_DB_TYPE" default:"grpc" usage:"database type: badger or grpc"`
GRPCDBServer string `env:"ORLY_ACL_GRPC_DB_SERVER" usage:"gRPC database server address (when DB_TYPE=grpc)"`
DataDir string `env:"ORLY_DATA_DIR" usage:"database data directory (when DB_TYPE=badger)"`
// Badger configuration (when DB_TYPE=badger)
BlockCacheMB int `env:"ORLY_DB_BLOCK_CACHE_MB" default:"256" usage:"block cache size in MB"`
IndexCacheMB int `env:"ORLY_DB_INDEX_CACHE_MB" default:"128" usage:"index cache size in MB"`
ZSTDLevel int `env:"ORLY_DB_ZSTD_LEVEL" default:"3" usage:"ZSTD compression level"`
QueryCacheSizeMB int `env:"ORLY_DB_QUERY_CACHE_SIZE_MB" default:"64" usage:"query cache size in MB"`
QueryCacheMaxAge time.Duration `env:"ORLY_DB_QUERY_CACHE_MAX_AGE" default:"5m" usage:"query cache max age"`
QueryCacheDisabled bool `env:"ORLY_DB_QUERY_CACHE_DISABLED" default:"false" usage:"disable query cache"`
SerialCachePubkeys int `env:"ORLY_SERIAL_CACHE_PUBKEYS" default:"100000" usage:"serial cache pubkeys capacity"`
SerialCacheEventIds int `env:"ORLY_SERIAL_CACHE_EVENT_IDS" default:"500000" usage:"serial cache event IDs capacity"`
// ACL configuration
Owners string `env:"ORLY_OWNERS" usage:"comma-separated list of owner npubs"`
Admins string `env:"ORLY_ADMINS" usage:"comma-separated list of admin npubs"`
BootstrapRelays string `env:"ORLY_BOOTSTRAP_RELAYS" usage:"comma-separated list of bootstrap relays"`
RelayAddresses string `env:"ORLY_RELAY_ADDRESSES" usage:"comma-separated list of relay addresses (self)"`
// Follows ACL configuration
FollowListFrequency time.Duration `env:"ORLY_FOLLOW_LIST_FREQUENCY" default:"1h" usage:"follow list sync frequency"`
FollowsThrottleEnabled bool `env:"ORLY_FOLLOWS_THROTTLE_ENABLED" default:"false" usage:"enable progressive throttle for non-followed users"`
FollowsThrottlePerEvent time.Duration `env:"ORLY_FOLLOWS_THROTTLE_PER_EVENT" default:"25ms" usage:"throttle delay increment per event"`
FollowsThrottleMaxDelay time.Duration `env:"ORLY_FOLLOWS_THROTTLE_MAX_DELAY" default:"60s" usage:"maximum throttle delay"`
}
func main() {
cfg := loadConfig()
// Set log level
lol.SetLogLevel(cfg.LogLevel)
log.I.F("orly-acl-follows starting with log level: %s", cfg.LogLevel)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Initialize database (direct Badger or gRPC client)
var db database.Database
var err error
var ownsDB bool
if cfg.DBType == "grpc" {
// Use gRPC database client
log.I.F("connecting to gRPC database server at %s", cfg.GRPCDBServer)
db, err = databasegrpc.New(ctx, &databasegrpc.ClientConfig{
ServerAddress: cfg.GRPCDBServer,
ConnectTimeout: 30 * time.Second,
})
if chk.E(err) {
log.E.F("failed to connect to gRPC database: %v", err)
os.Exit(1)
}
ownsDB = false // gRPC client doesn't own the database
} else {
// Use direct Badger database
dbCfg := &database.DatabaseConfig{
DataDir: cfg.DataDir,
LogLevel: cfg.LogLevel,
BlockCacheMB: cfg.BlockCacheMB,
IndexCacheMB: cfg.IndexCacheMB,
QueryCacheSizeMB: cfg.QueryCacheSizeMB,
QueryCacheMaxAge: cfg.QueryCacheMaxAge,
QueryCacheDisabled: cfg.QueryCacheDisabled,
SerialCachePubkeys: cfg.SerialCachePubkeys,
SerialCacheEventIds: cfg.SerialCacheEventIds,
ZSTDLevel: cfg.ZSTDLevel,
}
log.I.F("initializing Badger database at %s", cfg.DataDir)
db, err = database.NewWithConfig(ctx, cancel, dbCfg)
if chk.E(err) {
log.E.F("failed to initialize database: %v", err)
os.Exit(1)
}
ownsDB = true
}
// Wait for database to be ready
log.I.F("waiting for database to be ready...")
<-db.Ready()
log.I.F("database ready")
// Create server config
serverCfg := &server.Config{
Listen: cfg.Listen,
ACLMode: "follows", // Hardcoded for this binary
LogLevel: cfg.LogLevel,
Owners: splitList(cfg.Owners),
Admins: splitList(cfg.Admins),
BootstrapRelays: splitList(cfg.BootstrapRelays),
RelayAddresses: splitList(cfg.RelayAddresses),
FollowListFrequency: cfg.FollowListFrequency,
FollowsThrottleEnabled: cfg.FollowsThrottleEnabled,
FollowsThrottlePerEvent: cfg.FollowsThrottlePerEvent,
FollowsThrottleMaxDelay: cfg.FollowsThrottleMaxDelay,
}
// Create and configure server
srv := server.New(db, serverCfg, ownsDB)
if err := srv.ConfigureACL(ctx); chk.E(err) {
log.E.F("failed to configure ACL: %v", err)
os.Exit(1)
}
// Start server
if err := srv.ListenAndServe(ctx, cancel); err != nil {
log.E.F("gRPC server error: %v", err)
}
}
func loadConfig() *Config {
cfg := &Config{}
if err := env.Load(cfg, nil); chk.E(err) {
log.E.F("failed to load config: %v", err)
os.Exit(1)
}
// Set default data directory if not specified
if cfg.DataDir == "" {
home, err := os.UserHomeDir()
if chk.E(err) {
log.E.F("failed to get home directory: %v", err)
os.Exit(1)
}
cfg.DataDir = filepath.Join(home, ".local", "share", "ORLY")
}
// Ensure data directory exists (for badger mode)
if cfg.DBType == "badger" {
if err := os.MkdirAll(cfg.DataDir, 0700); chk.E(err) {
log.E.F("failed to create data directory %s: %v", cfg.DataDir, err)
os.Exit(1)
}
}
return cfg
}
func splitList(s string) []string {
if s == "" {
return nil
}
return strings.Split(s, ",")
}

138
cmd/orly-acl-managed/main.go

@ -0,0 +1,138 @@ @@ -0,0 +1,138 @@
// orly-acl-managed is a standalone gRPC ACL server using the Managed mode.
// It provides fine-grained control via NIP-86 management API.
package main
import (
"context"
"os"
"path/filepath"
"strings"
"time"
"go-simpler.org/env"
"lol.mleku.dev"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/pkg/acl/server"
"next.orly.dev/pkg/database"
)
// Config holds the ACL server configuration.
type Config struct {
// Listen is the gRPC server listen address
Listen string `env:"ORLY_ACL_LISTEN" default:"127.0.0.1:50052" usage:"gRPC server listen address"`
// LogLevel is the logging level
LogLevel string `env:"ORLY_ACL_LOG_LEVEL" default:"info" usage:"log level (trace, debug, info, warn, error)"`
// Database configuration - Managed mode requires direct Badger access
DataDir string `env:"ORLY_DATA_DIR" usage:"database data directory"`
// Badger configuration
BlockCacheMB int `env:"ORLY_DB_BLOCK_CACHE_MB" default:"256" usage:"block cache size in MB"`
IndexCacheMB int `env:"ORLY_DB_INDEX_CACHE_MB" default:"128" usage:"index cache size in MB"`
ZSTDLevel int `env:"ORLY_DB_ZSTD_LEVEL" default:"3" usage:"ZSTD compression level"`
QueryCacheSizeMB int `env:"ORLY_DB_QUERY_CACHE_SIZE_MB" default:"64" usage:"query cache size in MB"`
QueryCacheMaxAge time.Duration `env:"ORLY_DB_QUERY_CACHE_MAX_AGE" default:"5m" usage:"query cache max age"`
QueryCacheDisabled bool `env:"ORLY_DB_QUERY_CACHE_DISABLED" default:"false" usage:"disable query cache"`
SerialCachePubkeys int `env:"ORLY_SERIAL_CACHE_PUBKEYS" default:"100000" usage:"serial cache pubkeys capacity"`
SerialCacheEventIds int `env:"ORLY_SERIAL_CACHE_EVENT_IDS" default:"500000" usage:"serial cache event IDs capacity"`
// ACL configuration
Owners string `env:"ORLY_OWNERS" usage:"comma-separated list of owner npubs"`
Admins string `env:"ORLY_ADMINS" usage:"comma-separated list of admin npubs"`
RelayAddresses string `env:"ORLY_RELAY_ADDRESSES" usage:"comma-separated list of relay addresses (self)"`
}
func main() {
cfg := loadConfig()
// Set log level
lol.SetLogLevel(cfg.LogLevel)
log.I.F("orly-acl-managed starting with log level: %s", cfg.LogLevel)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Managed mode requires direct Badger database access (not gRPC)
dbCfg := &database.DatabaseConfig{
DataDir: cfg.DataDir,
LogLevel: cfg.LogLevel,
BlockCacheMB: cfg.BlockCacheMB,
IndexCacheMB: cfg.IndexCacheMB,
QueryCacheSizeMB: cfg.QueryCacheSizeMB,
QueryCacheMaxAge: cfg.QueryCacheMaxAge,
QueryCacheDisabled: cfg.QueryCacheDisabled,
SerialCachePubkeys: cfg.SerialCachePubkeys,
SerialCacheEventIds: cfg.SerialCacheEventIds,
ZSTDLevel: cfg.ZSTDLevel,
}
log.I.F("initializing Badger database at %s", cfg.DataDir)
db, err := database.NewWithConfig(ctx, cancel, dbCfg)
if chk.E(err) {
log.E.F("failed to initialize database: %v", err)
os.Exit(1)
}
// Wait for database to be ready
log.I.F("waiting for database to be ready...")
<-db.Ready()
log.I.F("database ready")
// Create server config
serverCfg := &server.Config{
Listen: cfg.Listen,
ACLMode: "managed", // Hardcoded for this binary
LogLevel: cfg.LogLevel,
Owners: splitList(cfg.Owners),
Admins: splitList(cfg.Admins),
RelayAddresses: splitList(cfg.RelayAddresses),
}
// Create and configure server
srv := server.New(db, serverCfg, true)
if err := srv.ConfigureACL(ctx); chk.E(err) {
log.E.F("failed to configure ACL: %v", err)
os.Exit(1)
}
// Start server
if err := srv.ListenAndServe(ctx, cancel); err != nil {
log.E.F("gRPC server error: %v", err)
}
}
func loadConfig() *Config {
cfg := &Config{}
if err := env.Load(cfg, nil); chk.E(err) {
log.E.F("failed to load config: %v", err)
os.Exit(1)
}
// Set default data directory if not specified
if cfg.DataDir == "" {
home, err := os.UserHomeDir()
if chk.E(err) {
log.E.F("failed to get home directory: %v", err)
os.Exit(1)
}
cfg.DataDir = filepath.Join(home, ".local", "share", "ORLY")
}
// Ensure data directory exists
if err := os.MkdirAll(cfg.DataDir, 0700); chk.E(err) {
log.E.F("failed to create data directory %s: %v", cfg.DataDir, err)
os.Exit(1)
}
return cfg
}
func splitList(s string) []string {
if s == "" {
return nil
}
return strings.Split(s, ",")
}

112
cmd/orly-acl/config.go

@ -0,0 +1,112 @@ @@ -0,0 +1,112 @@
package main
import (
"os"
"path/filepath"
"strings"
"time"
"go-simpler.org/env"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
)
// Config holds the ACL server configuration.
type Config struct {
// Listen is the gRPC server listen address
Listen string `env:"ORLY_ACL_LISTEN" default:"127.0.0.1:50052" usage:"gRPC server listen address"`
// ACLMode is the active ACL mode (none, follows, managed, curating)
ACLMode string `env:"ORLY_ACL_MODE" default:"none" usage:"ACL mode: none, follows, managed, curating"`
// LogLevel is the logging level
LogLevel string `env:"ORLY_ACL_LOG_LEVEL" default:"info" usage:"log level (trace, debug, info, warn, error)"`
// Database configuration
DBType string `env:"ORLY_ACL_DB_TYPE" default:"badger" usage:"database type: badger or grpc"`
GRPCDBServer string `env:"ORLY_ACL_GRPC_DB_SERVER" usage:"gRPC database server address (when DB_TYPE=grpc)"`
DataDir string `env:"ORLY_DATA_DIR" usage:"database data directory (when DB_TYPE=badger)"`
// Badger configuration (when DB_TYPE=badger)
BlockCacheMB int `env:"ORLY_DB_BLOCK_CACHE_MB" default:"1024" usage:"block cache size in MB"`
IndexCacheMB int `env:"ORLY_DB_INDEX_CACHE_MB" default:"512" usage:"index cache size in MB"`
ZSTDLevel int `env:"ORLY_DB_ZSTD_LEVEL" default:"3" usage:"ZSTD compression level"`
QueryCacheSizeMB int `env:"ORLY_DB_QUERY_CACHE_SIZE_MB" default:"256" usage:"query cache size in MB"`
QueryCacheMaxAge time.Duration `env:"ORLY_DB_QUERY_CACHE_MAX_AGE" default:"5m" usage:"query cache max age"`
QueryCacheDisabled bool `env:"ORLY_DB_QUERY_CACHE_DISABLED" default:"false" usage:"disable query cache"`
SerialCachePubkeys int `env:"ORLY_SERIAL_CACHE_PUBKEYS" default:"100000" usage:"serial cache pubkeys capacity"`
SerialCacheEventIds int `env:"ORLY_SERIAL_CACHE_EVENT_IDS" default:"500000" usage:"serial cache event IDs capacity"`
// ACL configuration
Owners string `env:"ORLY_OWNERS" usage:"comma-separated list of owner npubs"`
Admins string `env:"ORLY_ADMINS" usage:"comma-separated list of admin npubs"`
BootstrapRelays string `env:"ORLY_BOOTSTRAP_RELAYS" usage:"comma-separated list of bootstrap relays"`
RelayAddresses string `env:"ORLY_RELAY_ADDRESSES" usage:"comma-separated list of relay addresses (self)"`
// Follows ACL configuration
FollowListFrequency time.Duration `env:"ORLY_FOLLOW_LIST_FREQUENCY" default:"1h" usage:"follow list sync frequency"`
FollowsThrottleEnabled bool `env:"ORLY_FOLLOWS_THROTTLE_ENABLED" default:"false" usage:"enable progressive throttle for non-followed users"`
FollowsThrottlePerEvent time.Duration `env:"ORLY_FOLLOWS_THROTTLE_PER_EVENT" default:"25ms" usage:"throttle delay increment per event"`
FollowsThrottleMaxDelay time.Duration `env:"ORLY_FOLLOWS_THROTTLE_MAX_DELAY" default:"60s" usage:"maximum throttle delay"`
}
// loadConfig loads configuration from environment variables.
func loadConfig() *Config {
cfg := &Config{}
if err := env.Load(cfg, nil); chk.E(err) {
log.E.F("failed to load config: %v", err)
os.Exit(1)
}
// Set default data directory if not specified
if cfg.DataDir == "" {
home, err := os.UserHomeDir()
if chk.E(err) {
log.E.F("failed to get home directory: %v", err)
os.Exit(1)
}
cfg.DataDir = filepath.Join(home, ".local", "share", "ORLY")
}
// Ensure data directory exists (for badger mode)
if cfg.DBType == "badger" {
if err := os.MkdirAll(cfg.DataDir, 0700); chk.E(err) {
log.E.F("failed to create data directory %s: %v", cfg.DataDir, err)
os.Exit(1)
}
}
return cfg
}
// GetOwners returns the list of owner pubkeys
func (c *Config) GetOwners() []string {
if c.Owners == "" {
return nil
}
return strings.Split(c.Owners, ",")
}
// GetAdmins returns the list of admin pubkeys
func (c *Config) GetAdmins() []string {
if c.Admins == "" {
return nil
}
return strings.Split(c.Admins, ",")
}
// GetBootstrapRelays returns the list of bootstrap relays
func (c *Config) GetBootstrapRelays() []string {
if c.BootstrapRelays == "" {
return nil
}
return strings.Split(c.BootstrapRelays, ",")
}
// GetRelayAddresses returns the list of relay addresses (self)
func (c *Config) GetRelayAddresses() []string {
if c.RelayAddresses == "" {
return nil
}
return strings.Split(c.RelayAddresses, ",")
}

167
cmd/orly-acl/main.go

@ -0,0 +1,167 @@ @@ -0,0 +1,167 @@
// orly-acl is a standalone gRPC ACL server for the ORLY relay.
// It wraps the ACL implementations and exposes them via gRPC.
package main
import (
"context"
"net"
"os"
"os/signal"
"syscall"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"lol.mleku.dev"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/app/config"
"next.orly.dev/pkg/acl"
"next.orly.dev/pkg/database"
databasegrpc "next.orly.dev/pkg/database/grpc"
orlyaclv1 "next.orly.dev/pkg/proto/orlyacl/v1"
)
func main() {
cfg := loadConfig()
// Set log level
lol.SetLogLevel(cfg.LogLevel)
log.I.F("orly-acl starting with log level: %s, mode: %s", cfg.LogLevel, cfg.ACLMode)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Initialize database (direct Badger or gRPC client)
var db database.Database
var err error
if cfg.DBType == "grpc" {
// Use gRPC database client
log.I.F("connecting to gRPC database server at %s", cfg.GRPCDBServer)
dbClient, err := databasegrpc.New(ctx, &databasegrpc.ClientConfig{
ServerAddress: cfg.GRPCDBServer,
ConnectTimeout: 30 * time.Second,
})
if chk.E(err) {
log.E.F("failed to connect to gRPC database: %v", err)
os.Exit(1)
}
db = dbClient
} else {
// Use direct Badger database
dbCfg := &database.DatabaseConfig{
DataDir: cfg.DataDir,
LogLevel: cfg.LogLevel,
BlockCacheMB: cfg.BlockCacheMB,
IndexCacheMB: cfg.IndexCacheMB,
QueryCacheSizeMB: cfg.QueryCacheSizeMB,
QueryCacheMaxAge: cfg.QueryCacheMaxAge,
QueryCacheDisabled: cfg.QueryCacheDisabled,
SerialCachePubkeys: cfg.SerialCachePubkeys,
SerialCacheEventIds: cfg.SerialCacheEventIds,
ZSTDLevel: cfg.ZSTDLevel,
}
log.I.F("initializing Badger database at %s", cfg.DataDir)
db, err = database.NewWithConfig(ctx, cancel, dbCfg)
if chk.E(err) {
log.E.F("failed to initialize database: %v", err)
os.Exit(1)
}
}
// Wait for database to be ready
log.I.F("waiting for database to be ready...")
<-db.Ready()
log.I.F("database ready")
// Create app config for ACL configuration
appCfg := &config.C{
Owners: cfg.GetOwners(),
Admins: cfg.GetAdmins(),
BootstrapRelays: cfg.GetBootstrapRelays(),
RelayAddresses: cfg.GetRelayAddresses(),
FollowListFrequency: cfg.FollowListFrequency,
FollowsThrottleEnabled: cfg.FollowsThrottleEnabled,
FollowsThrottlePerEvent: cfg.FollowsThrottlePerEvent,
FollowsThrottleMaxDelay: cfg.FollowsThrottleMaxDelay,
}
// Set ACL mode and configure the registry
acl.Registry.SetMode(cfg.ACLMode)
if err := acl.Registry.Configure(appCfg, db, ctx); chk.E(err) {
log.E.F("failed to configure ACL: %v", err)
os.Exit(1)
}
// Start the syncer goroutine for background operations
acl.Registry.Syncer()
log.I.F("ACL syncer started for mode: %s", cfg.ACLMode)
// Create gRPC server
grpcServer := grpc.NewServer(
grpc.MaxRecvMsgSize(16<<20), // 16MB
grpc.MaxSendMsgSize(16<<20), // 16MB
)
// Register ACL service
service := NewACLService(cfg, db)
orlyaclv1.RegisterACLServiceServer(grpcServer, service)
// Register reflection for debugging with grpcurl
reflection.Register(grpcServer)
// Start listening
lis, err := net.Listen("tcp", cfg.Listen)
if chk.E(err) {
log.E.F("failed to listen on %s: %v", cfg.Listen, err)
os.Exit(1)
}
log.I.F("gRPC ACL server listening on %s", cfg.Listen)
// Handle graceful shutdown
go func() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT)
sig := <-sigs
log.I.F("received signal %v, shutting down...", sig)
// Cancel context to stop all operations
cancel()
// Gracefully stop gRPC server with timeout
stopped := make(chan struct{})
go func() {
grpcServer.GracefulStop()
close(stopped)
}()
select {
case <-stopped:
log.I.F("gRPC server stopped gracefully")
case <-time.After(5 * time.Second):
log.W.F("gRPC graceful stop timed out, forcing stop")
grpcServer.Stop()
}
// Sync and close database (only for direct Badger)
if cfg.DBType != "grpc" {
log.I.F("syncing database...")
if err := db.Sync(); chk.E(err) {
log.W.F("failed to sync database: %v", err)
}
log.I.F("closing database...")
if err := db.Close(); chk.E(err) {
log.W.F("failed to close database: %v", err)
}
}
log.I.F("shutdown complete")
}()
// Serve gRPC
if err := grpcServer.Serve(lis); err != nil {
log.E.F("gRPC server error: %v", err)
}
}

788
cmd/orly-acl/service.go

@ -0,0 +1,788 @@ @@ -0,0 +1,788 @@
package main
import (
"context"
"encoding/hex"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"lol.mleku.dev/log"
"next.orly.dev/pkg/acl"
"next.orly.dev/pkg/database"
orlyaclv1 "next.orly.dev/pkg/proto/orlyacl/v1"
orlydbv1 "next.orly.dev/pkg/proto/orlydb/v1"
)
// ACLService implements the orlyaclv1.ACLServiceServer interface.
type ACLService struct {
orlyaclv1.UnimplementedACLServiceServer
cfg *Config
db database.Database
}
// NewACLService creates a new ACL service.
func NewACLService(cfg *Config, db database.Database) *ACLService {
return &ACLService{
cfg: cfg,
db: db,
}
}
// === Core ACL Methods ===
func (s *ACLService) GetAccessLevel(ctx context.Context, req *orlyaclv1.AccessLevelRequest) (*orlyaclv1.AccessLevelResponse, error) {
level := acl.Registry.GetAccessLevel(req.Pubkey, req.Address)
return &orlyaclv1.AccessLevelResponse{Level: level}, nil
}
func (s *ACLService) CheckPolicy(ctx context.Context, req *orlyaclv1.PolicyCheckRequest) (*orlyaclv1.PolicyCheckResponse, error) {
ev := orlydbv1.ProtoToEvent(req.Event)
allowed, err := acl.Registry.CheckPolicy(ev)
resp := &orlyaclv1.PolicyCheckResponse{Allowed: allowed}
if err != nil {
resp.Error = err.Error()
}
return resp, nil
}
func (s *ACLService) GetACLInfo(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ACLInfoResponse, error) {
name, description, documentation := acl.Registry.GetACLInfo()
return &orlyaclv1.ACLInfoResponse{
Name: name,
Description: description,
Documentation: documentation,
}, nil
}
func (s *ACLService) GetMode(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ModeResponse, error) {
return &orlyaclv1.ModeResponse{Mode: acl.Registry.Type()}, nil
}
func (s *ACLService) Ready(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ReadyResponse, error) {
// Check if database is ready
select {
case <-s.db.Ready():
return &orlyaclv1.ReadyResponse{Ready: true}, nil
default:
return &orlyaclv1.ReadyResponse{Ready: false}, nil
}
}
// === Follows ACL Methods ===
func (s *ACLService) GetThrottleDelay(ctx context.Context, req *orlyaclv1.ThrottleDelayRequest) (*orlyaclv1.ThrottleDelayResponse, error) {
// Get the active ACL and check if it's Follows
for _, i := range acl.Registry.ACL {
if i.Type() == "follows" {
if follows, ok := i.(*acl.Follows); ok {
delay := follows.GetThrottleDelay(req.Pubkey, req.Ip)
return &orlyaclv1.ThrottleDelayResponse{DelayMs: delay.Milliseconds()}, nil
}
}
}
return &orlyaclv1.ThrottleDelayResponse{DelayMs: 0}, nil
}
func (s *ACLService) AddFollow(ctx context.Context, req *orlyaclv1.AddFollowRequest) (*orlyaclv1.Empty, error) {
acl.Registry.AddFollow(req.Pubkey)
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) GetFollowedPubkeys(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.FollowedPubkeysResponse, error) {
for _, i := range acl.Registry.ACL {
if i.Type() == "follows" {
if follows, ok := i.(*acl.Follows); ok {
pubkeys := follows.GetFollowedPubkeys()
return &orlyaclv1.FollowedPubkeysResponse{Pubkeys: pubkeys}, nil
}
}
}
return &orlyaclv1.FollowedPubkeysResponse{}, nil
}
func (s *ACLService) GetAdminRelays(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.AdminRelaysResponse, error) {
for _, i := range acl.Registry.ACL {
if i.Type() == "follows" {
if follows, ok := i.(*acl.Follows); ok {
urls := follows.AdminRelays()
return &orlyaclv1.AdminRelaysResponse{Urls: urls}, nil
}
}
}
return &orlyaclv1.AdminRelaysResponse{}, nil
}
// === Managed ACL Methods ===
func (s *ACLService) BanPubkey(ctx context.Context, req *orlyaclv1.BanPubkeyRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.SaveBannedPubkey(req.Pubkey, req.Reason); err != nil {
return nil, status.Errorf(codes.Internal, "failed to ban pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) UnbanPubkey(ctx context.Context, req *orlyaclv1.PubkeyRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.RemoveBannedPubkey(req.Pubkey); err != nil {
return nil, status.Errorf(codes.Internal, "failed to unban pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListBannedPubkeys(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListBannedPubkeysResponse, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
banned, err := managedACL.ListBannedPubkeys()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list banned pubkeys: %v", err)
}
resp := &orlyaclv1.ListBannedPubkeysResponse{}
for _, b := range banned {
resp.Pubkeys = append(resp.Pubkeys, &orlyaclv1.BannedPubkey{
Pubkey: b.Pubkey,
Reason: b.Reason,
Added: b.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) AllowPubkey(ctx context.Context, req *orlyaclv1.AllowPubkeyRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.SaveAllowedPubkey(req.Pubkey, req.Reason); err != nil {
return nil, status.Errorf(codes.Internal, "failed to allow pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) DisallowPubkey(ctx context.Context, req *orlyaclv1.PubkeyRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.RemoveAllowedPubkey(req.Pubkey); err != nil {
return nil, status.Errorf(codes.Internal, "failed to disallow pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListAllowedPubkeys(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListAllowedPubkeysResponse, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
allowed, err := managedACL.ListAllowedPubkeys()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list allowed pubkeys: %v", err)
}
resp := &orlyaclv1.ListAllowedPubkeysResponse{}
for _, a := range allowed {
resp.Pubkeys = append(resp.Pubkeys, &orlyaclv1.AllowedPubkey{
Pubkey: a.Pubkey,
Reason: a.Reason,
Added: a.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) BanEvent(ctx context.Context, req *orlyaclv1.BanEventRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.SaveBannedEvent(req.EventId, req.Reason); err != nil {
return nil, status.Errorf(codes.Internal, "failed to ban event: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) UnbanEvent(ctx context.Context, req *orlyaclv1.EventRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.RemoveBannedEvent(req.EventId); err != nil {
return nil, status.Errorf(codes.Internal, "failed to unban event: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListBannedEvents(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListBannedEventsResponse, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
banned, err := managedACL.ListBannedEvents()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list banned events: %v", err)
}
resp := &orlyaclv1.ListBannedEventsResponse{}
for _, b := range banned {
resp.Events = append(resp.Events, &orlyaclv1.BannedEvent{
EventId: b.ID,
Reason: b.Reason,
Added: b.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) AllowEvent(ctx context.Context, req *orlyaclv1.BanEventRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.SaveAllowedEvent(req.EventId, req.Reason); err != nil {
return nil, status.Errorf(codes.Internal, "failed to allow event: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) DisallowEvent(ctx context.Context, req *orlyaclv1.EventRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.RemoveAllowedEvent(req.EventId); err != nil {
return nil, status.Errorf(codes.Internal, "failed to disallow event: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListAllowedEvents(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListAllowedEventsResponse, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
allowed, err := managedACL.ListAllowedEvents()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list allowed events: %v", err)
}
resp := &orlyaclv1.ListAllowedEventsResponse{}
for _, a := range allowed {
resp.Events = append(resp.Events, &orlyaclv1.AllowedEvent{
EventId: a.ID,
Reason: a.Reason,
Added: a.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) BlockIP(ctx context.Context, req *orlyaclv1.BlockIPRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.SaveBlockedIP(req.Ip, req.Reason); err != nil {
return nil, status.Errorf(codes.Internal, "failed to block IP: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) UnblockIP(ctx context.Context, req *orlyaclv1.IPRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.RemoveBlockedIP(req.Ip); err != nil {
return nil, status.Errorf(codes.Internal, "failed to unblock IP: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListBlockedIPs(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListBlockedIPsResponse, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
blocked, err := managedACL.ListBlockedIPs()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list blocked IPs: %v", err)
}
resp := &orlyaclv1.ListBlockedIPsResponse{}
for _, b := range blocked {
resp.Ips = append(resp.Ips, &orlyaclv1.BlockedIP{
Ip: b.IP,
Reason: b.Reason,
Added: b.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) AllowKind(ctx context.Context, req *orlyaclv1.AllowKindRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.SaveAllowedKind(int(req.Kind)); err != nil {
return nil, status.Errorf(codes.Internal, "failed to allow kind: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) DisallowKind(ctx context.Context, req *orlyaclv1.KindRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.RemoveAllowedKind(int(req.Kind)); err != nil {
return nil, status.Errorf(codes.Internal, "failed to disallow kind: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListAllowedKinds(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListAllowedKindsResponse, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
kinds, err := managedACL.ListAllowedKinds()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list allowed kinds: %v", err)
}
resp := &orlyaclv1.ListAllowedKindsResponse{}
for _, k := range kinds {
resp.Kinds = append(resp.Kinds, int32(k))
}
return resp, nil
}
func (s *ACLService) UpdatePeerAdmins(ctx context.Context, req *orlyaclv1.UpdatePeerAdminsRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managed.UpdatePeerAdmins(req.PeerPubkeys)
return &orlyaclv1.Empty{}, nil
}
// === Curating ACL Methods ===
func (s *ACLService) TrustPubkey(ctx context.Context, req *orlyaclv1.TrustPubkeyRequest) (*orlyaclv1.Empty, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
if err := curating.TrustPubkey(req.Pubkey, req.Note); err != nil {
return nil, status.Errorf(codes.Internal, "failed to trust pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) UntrustPubkey(ctx context.Context, req *orlyaclv1.PubkeyRequest) (*orlyaclv1.Empty, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
if err := curating.UntrustPubkey(req.Pubkey); err != nil {
return nil, status.Errorf(codes.Internal, "failed to untrust pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListTrustedPubkeys(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListTrustedPubkeysResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
trusted, err := curatingACL.ListTrustedPubkeys()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list trusted pubkeys: %v", err)
}
resp := &orlyaclv1.ListTrustedPubkeysResponse{}
for _, t := range trusted {
resp.Pubkeys = append(resp.Pubkeys, &orlyaclv1.TrustedPubkey{
Pubkey: t.Pubkey,
Note: t.Note,
Added: t.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) BlacklistPubkey(ctx context.Context, req *orlyaclv1.BlacklistPubkeyRequest) (*orlyaclv1.Empty, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
if err := curating.BlacklistPubkey(req.Pubkey, req.Reason); err != nil {
return nil, status.Errorf(codes.Internal, "failed to blacklist pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) UnblacklistPubkey(ctx context.Context, req *orlyaclv1.PubkeyRequest) (*orlyaclv1.Empty, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
if err := curating.UnblacklistPubkey(req.Pubkey); err != nil {
return nil, status.Errorf(codes.Internal, "failed to unblacklist pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListBlacklistedPubkeys(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListBlacklistedPubkeysResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
blacklisted, err := curatingACL.ListBlacklistedPubkeys()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list blacklisted pubkeys: %v", err)
}
resp := &orlyaclv1.ListBlacklistedPubkeysResponse{}
for _, b := range blacklisted {
resp.Pubkeys = append(resp.Pubkeys, &orlyaclv1.BlacklistedPubkey{
Pubkey: b.Pubkey,
Reason: b.Reason,
Added: b.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) MarkSpam(ctx context.Context, req *orlyaclv1.MarkSpamRequest) (*orlyaclv1.Empty, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
if err := curatingACL.MarkEventAsSpam(req.EventId, req.Pubkey, req.Reason); err != nil {
return nil, status.Errorf(codes.Internal, "failed to mark spam: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) UnmarkSpam(ctx context.Context, req *orlyaclv1.EventRequest) (*orlyaclv1.Empty, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
if err := curatingACL.UnmarkEventAsSpam(req.EventId); err != nil {
return nil, status.Errorf(codes.Internal, "failed to unmark spam: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListSpamEvents(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListSpamEventsResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
spam, err := curatingACL.ListSpamEvents()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list spam events: %v", err)
}
resp := &orlyaclv1.ListSpamEventsResponse{}
for _, se := range spam {
resp.Events = append(resp.Events, &orlyaclv1.SpamEvent{
EventId: se.EventID,
Pubkey: se.Pubkey,
Reason: se.Reason,
Added: se.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) RateLimitCheck(ctx context.Context, req *orlyaclv1.RateLimitCheckRequest) (*orlyaclv1.RateLimitCheckResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
allowed, message, err := curating.RateLimitCheck(req.Pubkey, req.Ip)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to check rate limit: %v", err)
}
return &orlyaclv1.RateLimitCheckResponse{
Allowed: allowed,
Message: message,
}, nil
}
func (s *ACLService) ProcessConfigEvent(ctx context.Context, req *orlyaclv1.ConfigEventRequest) (*orlyaclv1.Empty, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
ev := orlydbv1.ProtoToEvent(req.Event)
if err := curating.ProcessConfigEvent(ev); err != nil {
return nil, status.Errorf(codes.Internal, "failed to process config event: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) GetCuratingConfig(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.CuratingConfig, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
config, err := curating.GetConfig()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get config: %v", err)
}
resp := &orlyaclv1.CuratingConfig{
ConfigEventId: config.ConfigEventID,
ConfigPubkey: config.ConfigPubkey,
ConfiguredAt: config.ConfiguredAt,
DailyLimit: int32(config.DailyLimit),
IpDailyLimit: int32(config.IPDailyLimit),
FirstBanHours: int32(config.FirstBanHours),
SecondBanHours: int32(config.SecondBanHours),
KindCategories: config.KindCategories,
AllowedRanges: config.AllowedRanges,
}
for _, k := range config.AllowedKinds {
resp.AllowedKinds = append(resp.AllowedKinds, int32(k))
}
return resp, nil
}
func (s *ACLService) IsCuratingConfigured(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.BoolResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return &orlyaclv1.BoolResponse{Value: false}, nil
}
configured, err := curating.IsConfigured()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to check if configured: %v", err)
}
return &orlyaclv1.BoolResponse{Value: configured}, nil
}
func (s *ACLService) ListUnclassifiedUsers(ctx context.Context, req *orlyaclv1.PaginationRequest) (*orlyaclv1.ListUnclassifiedUsersResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
// The underlying ListUnclassifiedUsers only takes limit, not offset
// We'll request limit+offset and skip the first offset items
limit := int(req.Limit)
offset := int(req.Offset)
if limit == 0 {
limit = 100 // Default limit
}
users, err := curatingACL.ListUnclassifiedUsers(limit + offset)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list unclassified users: %v", err)
}
// Apply offset
if offset > 0 && len(users) > offset {
users = users[offset:]
} else if offset > 0 {
users = nil
}
// Apply limit
if limit > 0 && len(users) > limit {
users = users[:limit]
}
resp := &orlyaclv1.ListUnclassifiedUsersResponse{Total: int32(len(users))}
for _, u := range users {
resp.Users = append(resp.Users, &orlyaclv1.UnclassifiedUser{
Pubkey: u.Pubkey,
EventCount: int32(u.EventCount),
FirstSeen: u.LastEvent.Format("2006-01-02T15:04:05Z"),
})
}
return resp, nil
}
func (s *ACLService) GetEventsForPubkey(ctx context.Context, req *orlyaclv1.GetEventsForPubkeyRequest) (*orlyaclv1.EventsForPubkeyResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
events, total, err := curatingACL.GetEventsForPubkey(req.Pubkey, int(req.Limit), int(req.Offset))
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get events for pubkey: %v", err)
}
resp := &orlyaclv1.EventsForPubkeyResponse{Total: int32(total)}
for _, ev := range events {
resp.Events = append(resp.Events, &orlyaclv1.EventSummary{
Id: ev.ID,
Kind: uint32(ev.Kind),
Content: []byte(ev.Content),
CreatedAt: ev.CreatedAt,
})
}
return resp, nil
}
func (s *ACLService) DeleteEventsForPubkey(ctx context.Context, req *orlyaclv1.DeleteEventsForPubkeyRequest) (*orlyaclv1.DeleteCountResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
count, err := curatingACL.DeleteEventsForPubkey(req.Pubkey)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete events for pubkey: %v", err)
}
return &orlyaclv1.DeleteCountResponse{Count: int32(count)}, nil
}
func (s *ACLService) ScanAllPubkeys(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ScanResultResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
result, err := curatingACL.ScanAllPubkeys()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to scan all pubkeys: %v", err)
}
return &orlyaclv1.ScanResultResponse{
TotalPubkeys: int32(result.TotalPubkeys),
TotalEvents: int32(result.TotalEvents),
}, nil
}
// === Helper Methods ===
func (s *ACLService) getManagedACL() *acl.Managed {
for _, i := range acl.Registry.ACL {
if i.Type() == "managed" {
if managed, ok := i.(*acl.Managed); ok {
return managed
}
}
}
return nil
}
func (s *ACLService) getCuratingACL() *acl.Curating {
for _, i := range acl.Registry.ACL {
if i.Type() == "curating" {
if curating, ok := i.(*acl.Curating); ok {
return curating
}
}
}
return nil
}
// Unused but may be needed for debugging
var _ = log.T
var _ = hex.EncodeToString

122
cmd/orly-db-badger/main.go

@ -0,0 +1,122 @@ @@ -0,0 +1,122 @@
// orly-db-badger is a standalone gRPC database server using the Badger backend.
package main
import (
"context"
"os"
"path/filepath"
"time"
"go-simpler.org/env"
"lol.mleku.dev"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/pkg/database"
"next.orly.dev/pkg/database/server"
)
// Config holds the database server configuration.
type Config struct {
// Listen is the gRPC server listen address
Listen string `env:"ORLY_DB_LISTEN" default:"127.0.0.1:50051" usage:"gRPC server listen address"`
// DataDir is the database data directory
DataDir string `env:"ORLY_DATA_DIR" usage:"database data directory"`
// LogLevel is the logging level
LogLevel string `env:"ORLY_DB_LOG_LEVEL" default:"info" usage:"log level (trace, debug, info, warn, error)"`
// Badger configuration
BlockCacheMB int `env:"ORLY_DB_BLOCK_CACHE_MB" default:"1024" usage:"block cache size in MB"`
IndexCacheMB int `env:"ORLY_DB_INDEX_CACHE_MB" default:"512" usage:"index cache size in MB"`
ZSTDLevel int `env:"ORLY_DB_ZSTD_LEVEL" default:"3" usage:"ZSTD compression level (1-19)"`
// Query cache configuration
QueryCacheSizeMB int `env:"ORLY_DB_QUERY_CACHE_SIZE_MB" default:"256" usage:"query cache size in MB"`
QueryCacheMaxAge time.Duration `env:"ORLY_DB_QUERY_CACHE_MAX_AGE" default:"5m" usage:"query cache max age"`
QueryCacheDisabled bool `env:"ORLY_DB_QUERY_CACHE_DISABLED" default:"false" usage:"disable query cache"`
// Serial cache configuration
SerialCachePubkeys int `env:"ORLY_SERIAL_CACHE_PUBKEYS" default:"100000" usage:"serial cache pubkeys capacity"`
SerialCacheEventIds int `env:"ORLY_SERIAL_CACHE_EVENT_IDS" default:"500000" usage:"serial cache event IDs capacity"`
// gRPC server configuration
StreamBatchSize int `env:"ORLY_DB_STREAM_BATCH_SIZE" default:"100" usage:"events per stream batch"`
}
func main() {
cfg := loadConfig()
// Set log level
lol.SetLogLevel(cfg.LogLevel)
log.I.F("orly-db-badger starting with log level: %s", cfg.LogLevel)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Create database configuration
dbCfg := &database.DatabaseConfig{
DataDir: cfg.DataDir,
LogLevel: cfg.LogLevel,
BlockCacheMB: cfg.BlockCacheMB,
IndexCacheMB: cfg.IndexCacheMB,
QueryCacheSizeMB: cfg.QueryCacheSizeMB,
QueryCacheMaxAge: cfg.QueryCacheMaxAge,
QueryCacheDisabled: cfg.QueryCacheDisabled,
SerialCachePubkeys: cfg.SerialCachePubkeys,
SerialCacheEventIds: cfg.SerialCacheEventIds,
ZSTDLevel: cfg.ZSTDLevel,
}
// Initialize Badger database
log.I.F("initializing Badger database at %s", cfg.DataDir)
db, err := database.NewWithConfig(ctx, cancel, dbCfg)
if chk.E(err) {
log.E.F("failed to initialize database: %v", err)
os.Exit(1)
}
// Wait for database to be ready
log.I.F("waiting for database to be ready...")
<-db.Ready()
log.I.F("database ready")
// Create and start gRPC server
serverCfg := &server.Config{
Listen: cfg.Listen,
LogLevel: cfg.LogLevel,
StreamBatchSize: cfg.StreamBatchSize,
}
srv := server.New(db, serverCfg)
if err := srv.ListenAndServe(ctx, cancel); err != nil {
log.E.F("gRPC server error: %v", err)
}
}
func loadConfig() *Config {
cfg := &Config{}
if err := env.Load(cfg, nil); chk.E(err) {
log.E.F("failed to load config: %v", err)
os.Exit(1)
}
// Set default data directory if not specified
if cfg.DataDir == "" {
home, err := os.UserHomeDir()
if chk.E(err) {
log.E.F("failed to get home directory: %v", err)
os.Exit(1)
}
cfg.DataDir = filepath.Join(home, ".local", "share", "ORLY")
}
// Ensure data directory exists
if err := os.MkdirAll(cfg.DataDir, 0700); chk.E(err) {
log.E.F("failed to create data directory %s: %v", cfg.DataDir, err)
os.Exit(1)
}
return cfg
}

108
cmd/orly-db-neo4j/main.go

@ -0,0 +1,108 @@ @@ -0,0 +1,108 @@
// orly-db-neo4j is a standalone gRPC database server using the Neo4j backend.
package main
import (
"context"
"os"
"time"
"go-simpler.org/env"
"lol.mleku.dev"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/pkg/database"
"next.orly.dev/pkg/database/server"
// Import neo4j to register the factory
_ "next.orly.dev/pkg/neo4j"
)
// Config holds the database server configuration.
type Config struct {
// Listen is the gRPC server listen address
Listen string `env:"ORLY_DB_LISTEN" default:"127.0.0.1:50051" usage:"gRPC server listen address"`
// LogLevel is the logging level
LogLevel string `env:"ORLY_DB_LOG_LEVEL" default:"info" usage:"log level (trace, debug, info, warn, error)"`
// Neo4j configuration
Neo4jURI string `env:"ORLY_NEO4J_URI" default:"bolt://localhost:7687" usage:"Neo4j connection URI"`
Neo4jUser string `env:"ORLY_NEO4J_USER" default:"neo4j" usage:"Neo4j username"`
Neo4jPassword string `env:"ORLY_NEO4J_PASSWORD" usage:"Neo4j password"`
// Neo4j driver tuning
Neo4jMaxConnPoolSize int `env:"ORLY_NEO4J_MAX_CONN_POOL" default:"25" usage:"max connection pool size"`
Neo4jFetchSize int `env:"ORLY_NEO4J_FETCH_SIZE" default:"1000" usage:"max records per fetch batch"`
Neo4jMaxTxRetrySeconds int `env:"ORLY_NEO4J_MAX_TX_RETRY_SEC" default:"30" usage:"max transaction retry time"`
Neo4jQueryResultLimit int `env:"ORLY_NEO4J_QUERY_RESULT_LIMIT" default:"10000" usage:"max results per query (0=unlimited)"`
// Query cache configuration (for the gRPC server)
QueryCacheSizeMB int `env:"ORLY_DB_QUERY_CACHE_SIZE_MB" default:"256" usage:"query cache size in MB"`
QueryCacheMaxAge time.Duration `env:"ORLY_DB_QUERY_CACHE_MAX_AGE" default:"5m" usage:"query cache max age"`
QueryCacheDisabled bool `env:"ORLY_DB_QUERY_CACHE_DISABLED" default:"false" usage:"disable query cache"`
// gRPC server configuration
StreamBatchSize int `env:"ORLY_DB_STREAM_BATCH_SIZE" default:"100" usage:"events per stream batch"`
}
func main() {
cfg := loadConfig()
// Set log level
lol.SetLogLevel(cfg.LogLevel)
log.I.F("orly-db-neo4j starting with log level: %s", cfg.LogLevel)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Create database configuration
dbCfg := &database.DatabaseConfig{
LogLevel: cfg.LogLevel,
Neo4jURI: cfg.Neo4jURI,
Neo4jUser: cfg.Neo4jUser,
Neo4jPassword: cfg.Neo4jPassword,
Neo4jMaxConnPoolSize: cfg.Neo4jMaxConnPoolSize,
Neo4jFetchSize: cfg.Neo4jFetchSize,
Neo4jMaxTxRetrySeconds: cfg.Neo4jMaxTxRetrySeconds,
Neo4jQueryResultLimit: cfg.Neo4jQueryResultLimit,
QueryCacheSizeMB: cfg.QueryCacheSizeMB,
QueryCacheMaxAge: cfg.QueryCacheMaxAge,
QueryCacheDisabled: cfg.QueryCacheDisabled,
}
// Initialize Neo4j database via factory
log.I.F("connecting to Neo4j at %s", cfg.Neo4jURI)
db, err := database.NewDatabaseWithConfig(ctx, cancel, "neo4j", dbCfg)
if chk.E(err) {
log.E.F("failed to initialize Neo4j database: %v", err)
os.Exit(1)
}
// Wait for database to be ready
log.I.F("waiting for database to be ready...")
<-db.Ready()
log.I.F("database ready")
// Create and start gRPC server
serverCfg := &server.Config{
Listen: cfg.Listen,
LogLevel: cfg.LogLevel,
StreamBatchSize: cfg.StreamBatchSize,
}
srv := server.New(db, serverCfg)
if err := srv.ListenAndServe(ctx, cancel); err != nil {
log.E.F("gRPC server error: %v", err)
}
}
func loadConfig() *Config {
cfg := &Config{}
if err := env.Load(cfg, nil); chk.E(err) {
log.E.F("failed to load config: %v", err)
os.Exit(1)
}
return cfg
}

52
cmd/orly-launcher/config.go

@ -10,37 +10,71 @@ import ( @@ -10,37 +10,71 @@ import (
// Config holds the launcher configuration.
type Config struct {
// DBBinary is the path to the orly-db binary
// DBBackend is the database backend: badger or neo4j
DBBackend string
// DBBinary is the path to the database server binary (computed from DBBackend if not set)
DBBinary string
// RelayBinary is the path to the orly binary
RelayBinary string
// ACLBinary is the path to the ACL server binary (computed from ACLMode if not set)
ACLBinary string
// DBListen is the address the database server listens on
DBListen string
// ACLListen is the address the ACL server listens on
ACLListen string
// ACLEnabled controls whether to run the ACL server as a separate process
// When false, the relay runs in open mode (no ACL restrictions)
ACLEnabled bool
// ACLMode is the ACL mode: follows, managed, curation
// Determines which ACL binary to use when ACLEnabled is true
ACLMode string
// DBReadyTimeout is how long to wait for the database to be ready
DBReadyTimeout time.Duration
// ACLReadyTimeout is how long to wait for the ACL server to be ready
ACLReadyTimeout time.Duration
// StopTimeout is how long to wait for processes to stop gracefully
StopTimeout time.Duration
// DataDir is the data directory to pass to orly-db
DataDir string
// LogLevel is the log level to use for both processes
// LogLevel is the log level to use for all processes
LogLevel string
}
func loadConfig() (*Config, error) {
// Get backend and mode first to compute default binary names
dbBackend := getEnvOrDefault("ORLY_LAUNCHER_DB_BACKEND", "badger")
aclMode := getEnvOrDefault("ORLY_ACL_MODE", "follows")
// Compute default binary names based on backend/mode
defaultDBBinary := "orly-db-" + dbBackend
defaultACLBinary := "orly-acl-" + aclMode
cfg := &Config{
DBBinary: getEnvOrDefault("ORLY_LAUNCHER_DB_BINARY", "orly-db"),
RelayBinary: getEnvOrDefault("ORLY_LAUNCHER_RELAY_BINARY", "orly"),
DBListen: getEnvOrDefault("ORLY_LAUNCHER_DB_LISTEN", "127.0.0.1:50051"),
DBReadyTimeout: parseDuration("ORLY_LAUNCHER_DB_READY_TIMEOUT", 30*time.Second),
StopTimeout: parseDuration("ORLY_LAUNCHER_STOP_TIMEOUT", 30*time.Second), // Increased for DB flush
DataDir: getEnvOrDefault("ORLY_DATA_DIR", filepath.Join(xdg.DataHome, "ORLY")),
LogLevel: getEnvOrDefault("ORLY_LOG_LEVEL", "info"),
DBBackend: dbBackend,
DBBinary: getEnvOrDefault("ORLY_LAUNCHER_DB_BINARY", defaultDBBinary),
RelayBinary: getEnvOrDefault("ORLY_LAUNCHER_RELAY_BINARY", "orly"),
ACLBinary: getEnvOrDefault("ORLY_LAUNCHER_ACL_BINARY", defaultACLBinary),
DBListen: getEnvOrDefault("ORLY_LAUNCHER_DB_LISTEN", "127.0.0.1:50051"),
ACLListen: getEnvOrDefault("ORLY_LAUNCHER_ACL_LISTEN", "127.0.0.1:50052"),
ACLEnabled: getEnvOrDefault("ORLY_LAUNCHER_ACL_ENABLED", "false") == "true",
ACLMode: aclMode,
DBReadyTimeout: parseDuration("ORLY_LAUNCHER_DB_READY_TIMEOUT", 30*time.Second),
ACLReadyTimeout: parseDuration("ORLY_LAUNCHER_ACL_READY_TIMEOUT", 30*time.Second),
StopTimeout: parseDuration("ORLY_LAUNCHER_STOP_TIMEOUT", 30*time.Second), // Increased for DB flush
DataDir: getEnvOrDefault("ORLY_DATA_DIR", filepath.Join(xdg.DataHome, "ORLY")),
LogLevel: getEnvOrDefault("ORLY_LOG_LEVEL", "info"),
}
return cfg, nil

131
cmd/orly-launcher/supervisor.go

@ -14,13 +14,14 @@ import ( @@ -14,13 +14,14 @@ import (
"lol.mleku.dev/log"
)
// Supervisor manages the database and relay processes.
// Supervisor manages the database, ACL, and relay processes.
type Supervisor struct {
cfg *Config
ctx context.Context
cancel context.CancelFunc
dbProc *Process
aclProc *Process
relayProc *Process
wg sync.WaitGroup
@ -46,7 +47,7 @@ func NewSupervisor(ctx context.Context, cancel context.CancelFunc, cfg *Config) @@ -46,7 +47,7 @@ func NewSupervisor(ctx context.Context, cancel context.CancelFunc, cfg *Config)
}
}
// Start starts the database and relay processes.
// Start starts the database, optional ACL server, and relay processes.
func (s *Supervisor) Start() error {
// 1. Start database server
if err := s.startDB(); err != nil {
@ -61,15 +62,42 @@ func (s *Supervisor) Start() error { @@ -61,15 +62,42 @@ func (s *Supervisor) Start() error {
log.I.F("database is ready")
// 3. Start relay with gRPC backend
// 3. Start ACL server if enabled
if s.cfg.ACLEnabled {
if err := s.startACL(); err != nil {
s.stopDB()
return fmt.Errorf("failed to start ACL server: %w", err)
}
// Wait for ACL to be ready
if err := s.waitForACLReady(s.cfg.ACLReadyTimeout); err != nil {
s.stopACL()
s.stopDB()
return fmt.Errorf("ACL server not ready: %w", err)
}
log.I.F("ACL server is ready")
}
// 4. Start relay with gRPC backend(s)
if err := s.startRelay(); err != nil {
if s.cfg.ACLEnabled {
s.stopACL()
}
s.stopDB()
return fmt.Errorf("failed to start relay: %w", err)
}
// 4. Start monitoring goroutines
s.wg.Add(2)
// 5. Start monitoring goroutines
monitorCount := 2
if s.cfg.ACLEnabled {
monitorCount = 3
}
s.wg.Add(monitorCount)
go s.monitorProcess(s.dbProc, "db", s.startDB)
if s.cfg.ACLEnabled {
go s.monitorProcess(s.aclProc, "acl", s.startACL)
}
go s.monitorProcess(s.relayProc, "relay", s.startRelay)
return nil
@ -85,10 +113,16 @@ func (s *Supervisor) Stop() error { @@ -85,10 +113,16 @@ func (s *Supervisor) Stop() error {
s.closed = true
s.mu.Unlock()
// Stop relay first (it depends on DB)
// Stop relay first (it depends on ACL and DB)
log.I.F("stopping relay...")
s.stopProcess(s.relayProc, 5*time.Second)
// Stop ACL if enabled (it depends on DB)
if s.cfg.ACLEnabled && s.aclProc != nil {
log.I.F("stopping ACL server...")
s.stopProcess(s.aclProc, 5*time.Second)
}
// Stop DB with longer timeout for flush
log.I.F("stopping database...")
s.stopProcess(s.dbProc, s.cfg.StopTimeout)
@ -135,6 +169,72 @@ func (s *Supervisor) startDB() error { @@ -135,6 +169,72 @@ func (s *Supervisor) startDB() error {
return nil
}
func (s *Supervisor) startACL() error {
s.mu.Lock()
defer s.mu.Unlock()
// Build environment for ACL process
env := os.Environ()
env = append(env, fmt.Sprintf("ORLY_ACL_LISTEN=%s", s.cfg.ACLListen))
env = append(env, "ORLY_ACL_DB_TYPE=grpc")
env = append(env, fmt.Sprintf("ORLY_ACL_GRPC_DB_SERVER=%s", s.cfg.DBListen))
env = append(env, fmt.Sprintf("ORLY_ACL_MODE=%s", s.cfg.ACLMode))
env = append(env, fmt.Sprintf("ORLY_ACL_LOG_LEVEL=%s", s.cfg.LogLevel))
cmd := exec.CommandContext(s.ctx, s.cfg.ACLBinary)
cmd.Env = env
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); chk.E(err) {
return err
}
exited := make(chan struct{})
s.aclProc = &Process{
name: "orly-acl",
cmd: cmd,
exited: exited,
}
// Start a goroutine to wait for the process and close the exited channel
go func() {
cmd.Wait()
close(exited)
}()
log.I.F("started ACL server (pid %d)", cmd.Process.Pid)
return nil
}
func (s *Supervisor) waitForACLReady(timeout time.Duration) error {
deadline := time.Now().Add(timeout)
ticker := time.NewTicker(250 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-s.ctx.Done():
return s.ctx.Err()
case <-ticker.C:
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for ACL server")
}
// Try to connect to the gRPC port
conn, err := net.DialTimeout("tcp", s.cfg.ACLListen, time.Second)
if err == nil {
conn.Close()
return nil // ACL server is accepting connections
}
}
}
}
func (s *Supervisor) stopACL() {
s.stopProcess(s.aclProc, 5*time.Second)
}
func (s *Supervisor) startRelay() error {
s.mu.Lock()
defer s.mu.Unlock()
@ -145,6 +245,18 @@ func (s *Supervisor) startRelay() error { @@ -145,6 +245,18 @@ func (s *Supervisor) startRelay() error {
env = append(env, fmt.Sprintf("ORLY_GRPC_SERVER=%s", s.cfg.DBListen))
env = append(env, fmt.Sprintf("ORLY_LOG_LEVEL=%s", s.cfg.LogLevel))
// If ACL is enabled, configure relay to use gRPC ACL
// Otherwise, run in open mode (no ACL restrictions)
if s.cfg.ACLEnabled {
env = append(env, "ORLY_ACL_TYPE=grpc")
env = append(env, fmt.Sprintf("ORLY_GRPC_ACL_SERVER=%s", s.cfg.ACLListen))
env = append(env, fmt.Sprintf("ORLY_ACL_MODE=%s", s.cfg.ACLMode))
} else {
// Open relay - no ACL restrictions
env = append(env, "ORLY_ACL_TYPE=local")
env = append(env, "ORLY_ACL_MODE=none")
}
cmd := exec.CommandContext(s.ctx, s.cfg.RelayBinary)
cmd.Env = env
cmd.Stdout = os.Stdout
@ -299,9 +411,12 @@ func (s *Supervisor) monitorProcess(p *Process, procType string, restart func() @@ -299,9 +411,12 @@ func (s *Supervisor) monitorProcess(p *Process, procType string, restart func()
} else {
// Update p to point to the new process
s.mu.Lock()
if procType == "db" {
switch procType {
case "db":
p = s.dbProc
} else {
case "acl":
p = s.aclProc
default:
p = s.relayProc
}
s.mu.Unlock()

36
main.go

@ -25,6 +25,7 @@ import ( @@ -25,6 +25,7 @@ import (
"next.orly.dev/app/branding"
"next.orly.dev/app/config"
"next.orly.dev/pkg/acl"
aclgrpc "next.orly.dev/pkg/acl/grpc"
"git.mleku.dev/mleku/nostr/crypto/keys"
"git.mleku.dev/mleku/nostr/encoders/bech32encoding"
"next.orly.dev/pkg/database"
@ -580,11 +581,38 @@ func main() { @@ -580,11 +581,38 @@ func main() {
os.Exit(1)
}
log.I.F("%s database initialized successfully", cfg.DBType)
acl.Registry.SetMode(cfg.ACLMode)
if err = acl.Registry.Configure(cfg, db, ctx); chk.E(err) {
os.Exit(1)
// Initialize ACL - either remote gRPC or in-process
aclType, aclServerAddr, aclConnTimeout := cfg.GetGRPCACLConfigValues()
if aclType == "grpc" {
// Use remote ACL server via gRPC
log.I.F("connecting to gRPC ACL server at %s", aclServerAddr)
aclClient, aclErr := aclgrpc.New(ctx, &aclgrpc.ClientConfig{
ServerAddress: aclServerAddr,
ConnectTimeout: aclConnTimeout,
})
if chk.E(aclErr) {
log.E.F("failed to connect to gRPC ACL server: %v", aclErr)
os.Exit(1)
}
// Wait for ACL server to be ready
select {
case <-aclClient.Ready():
log.I.F("gRPC ACL client connected, mode: %s", aclClient.Type())
case <-time.After(30 * time.Second):
log.E.F("timeout waiting for gRPC ACL server")
os.Exit(1)
}
// Register and activate the gRPC client as the ACL backend
acl.Registry.RegisterAndActivate(aclClient)
} else {
// Use in-process ACL (existing behavior)
acl.Registry.SetMode(cfg.ACLMode)
if err = acl.Registry.Configure(cfg, db, ctx); chk.E(err) {
os.Exit(1)
}
acl.Registry.Syncer()
}
acl.Registry.Syncer()
// Create rate limiter if enabled
var limiter *ratelimit.Limiter

66
orly.service

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
[Unit]
Description=ORLY Nostr Relay (Split IPC Mode)
After=network.target
[Service]
Type=simple
User=mleku
Group=mleku
WorkingDirectory=/home/mleku/src/next.orly.dev
# Use orly-launcher which manages orly-db-badger, orly-acl-follows, and the relay
ExecStart=/home/mleku/.local/bin/orly-launcher
# Launcher config - paths to split binaries
Environment=ORLY_LAUNCHER_DB_BACKEND=badger
Environment=ORLY_LAUNCHER_DB_BINARY=/home/mleku/.local/bin/orly-db-badger
Environment=ORLY_LAUNCHER_ACL_BINARY=/home/mleku/.local/bin/orly-acl-follows
Environment=ORLY_LAUNCHER_RELAY_BINARY=/home/mleku/.local/bin/next.orly.dev
Environment=ORLY_LAUNCHER_DB_LISTEN=127.0.0.1:50051
Environment=ORLY_LAUNCHER_ACL_LISTEN=127.0.0.1:50052
Environment=ORLY_LAUNCHER_ACL_ENABLED=true
Environment=ORLY_ACL_MODE=follows
# gRPC client settings (for relay to connect to db and acl)
Environment=ORLY_DB_TYPE=grpc
Environment=ORLY_GRPC_SERVER=127.0.0.1:50051
Environment=ORLY_ACL_TYPE=grpc
Environment=ORLY_GRPC_ACL_SERVER=127.0.0.1:50052
# Relay settings
Environment=ORLY_PORT=3334
Environment=ORLY_LISTEN=127.0.0.1
Environment=ORLY_LOG_LEVEL=info
Environment=ORLY_ADMINS=npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmleku
Environment=ORLY_OWNERS=npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmleku
Environment=ORLY_AUTH_REQUIRED=false
Environment=ORLY_AUTH_TO_WRITE=false
Environment=ORLY_NIP46_BYPASS_AUTH=true
Environment=ORLY_FOLLOWS_THROTTLE=true
Environment=ORLY_BLOSSOM_RATE_LIMIT=true
Environment=ORLY_BLOSSOM_DAILY_LIMIT_MB=10
# Memory settings for database server (orly-db-badger)
Environment=ORLY_DB_BLOCK_CACHE_MB=256
Environment=ORLY_DB_INDEX_CACHE_MB=128
Environment=ORLY_QUERY_CACHE_DISABLED=false
Environment=ORLY_QUERY_CACHE_SIZE_MB=64
Environment=ORLY_SERIAL_CACHE_PUBKEYS=100000
Environment=ORLY_SERIAL_CACHE_EVENT_IDS=500000
Environment=ORLY_GC_ENABLED=false
# Rate limiting for relay process
Environment=ORLY_RATE_LIMIT_TARGET_MB=2000
# Connection and query limits
Environment=ORLY_MAX_CONN_PER_IP=5
Environment=ORLY_QUERY_RESULT_LIMIT=256
# Enable pprof HTTP endpoint for debugging
Environment=ORLY_PPROF_HTTP=true
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

7
pkg/acl/acl.go

@ -27,6 +27,13 @@ func (s *S) Register(i acliface.I) { @@ -27,6 +27,13 @@ func (s *S) Register(i acliface.I) {
(*s).ACL = append((*s).ACL, i)
}
// RegisterAndActivate registers an ACL implementation and sets it as the active one.
// This is used for gRPC clients where the mode is determined by the remote server.
func (s *S) RegisterAndActivate(i acliface.I) {
s.ACL = []acliface.I{i}
s.SetMode(i.Type())
}
func (s *S) Configure(cfg ...any) (err error) {
for _, i := range s.ACL {
if i.Type() == s.Active.Load() {

384
pkg/acl/grpc/client.go

@ -0,0 +1,384 @@ @@ -0,0 +1,384 @@
// Package grpc provides a gRPC client that implements the acl.I interface.
// This allows the relay to use a remote ACL server via gRPC.
package grpc
import (
"context"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"git.mleku.dev/mleku/nostr/encoders/event"
acliface "next.orly.dev/pkg/interfaces/acl"
orlyaclv1 "next.orly.dev/pkg/proto/orlyacl/v1"
orlydbv1 "next.orly.dev/pkg/proto/orlydb/v1"
)
// Client implements the acl.I interface via gRPC.
type Client struct {
conn *grpc.ClientConn
client orlyaclv1.ACLServiceClient
ready chan struct{}
mode string
}
// Verify Client implements acl.I at compile time.
var _ acliface.I = (*Client)(nil)
// Verify Client implements acl.PolicyChecker at compile time.
var _ acliface.PolicyChecker = (*Client)(nil)
// ClientConfig holds configuration for the gRPC ACL client.
type ClientConfig struct {
ServerAddress string
ConnectTimeout time.Duration
}
// New creates a new gRPC ACL client.
func New(ctx context.Context, cfg *ClientConfig) (*Client, error) {
timeout := cfg.ConnectTimeout
if timeout == 0 {
timeout = 10 * time.Second
}
dialCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
conn, err := grpc.DialContext(dialCtx, cfg.ServerAddress,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(16<<20), // 16MB
grpc.MaxCallSendMsgSize(16<<20), // 16MB
),
)
if err != nil {
return nil, err
}
c := &Client{
conn: conn,
client: orlyaclv1.NewACLServiceClient(conn),
ready: make(chan struct{}),
}
// Check if server is ready and get mode
go c.waitForReady(ctx)
return c, nil
}
func (c *Client) waitForReady(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
resp, err := c.client.Ready(ctx, &orlyaclv1.Empty{})
if err == nil && resp.Ready {
// Get mode from server
modeResp, err := c.client.GetMode(ctx, &orlyaclv1.Empty{})
if err == nil {
c.mode = modeResp.Mode
}
close(c.ready)
log.I.F("gRPC ACL client connected and ready, mode: %s", c.mode)
return
}
time.Sleep(100 * time.Millisecond)
}
}
}
// Close closes the gRPC connection.
func (c *Client) Close() error {
if c.conn != nil {
return c.conn.Close()
}
return nil
}
// Ready returns a channel that closes when the client is ready.
func (c *Client) Ready() <-chan struct{} {
return c.ready
}
// === acl.I Interface Implementation ===
func (c *Client) Configure(cfg ...any) error {
// Configuration is done on the server side
// The client just passes through to the server
return nil
}
func (c *Client) GetAccessLevel(pub []byte, address string) string {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := c.client.GetAccessLevel(ctx, &orlyaclv1.AccessLevelRequest{
Pubkey: pub,
Address: address,
})
if chk.E(err) {
return "none"
}
return resp.Level
}
func (c *Client) GetACLInfo() (name, description, documentation string) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := c.client.GetACLInfo(ctx, &orlyaclv1.Empty{})
if chk.E(err) {
return "", "", ""
}
return resp.Name, resp.Description, resp.Documentation
}
func (c *Client) Syncer() {
// The syncer runs on the ACL server, not the client
// This is a no-op for the gRPC client
}
func (c *Client) Type() string {
return c.mode
}
// === acl.PolicyChecker Interface Implementation ===
func (c *Client) CheckPolicy(ev *event.E) (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := c.client.CheckPolicy(ctx, &orlyaclv1.PolicyCheckRequest{
Event: orlydbv1.EventToProto(ev),
})
if err != nil {
return false, err
}
if resp.Error != "" {
return resp.Allowed, &policyError{msg: resp.Error}
}
return resp.Allowed, nil
}
// policyError is a simple error type for policy check failures
type policyError struct {
msg string
}
func (e *policyError) Error() string {
return e.msg
}
// === Follows ACL Methods ===
// GetThrottleDelay returns the progressive throttle delay for a pubkey.
func (c *Client) GetThrottleDelay(pubkey []byte, ip string) time.Duration {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := c.client.GetThrottleDelay(ctx, &orlyaclv1.ThrottleDelayRequest{
Pubkey: pubkey,
Ip: ip,
})
if chk.E(err) {
return 0
}
return time.Duration(resp.DelayMs) * time.Millisecond
}
// AddFollow adds a pubkey to the followed list.
func (c *Client) AddFollow(pubkey []byte) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := c.client.AddFollow(ctx, &orlyaclv1.AddFollowRequest{
Pubkey: pubkey,
})
return err
}
// GetFollowedPubkeys returns all followed pubkeys.
func (c *Client) GetFollowedPubkeys() [][]byte {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
resp, err := c.client.GetFollowedPubkeys(ctx, &orlyaclv1.Empty{})
if chk.E(err) {
return nil
}
return resp.Pubkeys
}
// GetAdminRelays returns the admin relay URLs.
func (c *Client) GetAdminRelays() []string {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := c.client.GetAdminRelays(ctx, &orlyaclv1.Empty{})
if chk.E(err) {
return nil
}
return resp.Urls
}
// === Managed ACL Methods ===
// BanPubkey adds a pubkey to the ban list.
func (c *Client) BanPubkey(pubkey, reason string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := c.client.BanPubkey(ctx, &orlyaclv1.BanPubkeyRequest{
Pubkey: pubkey,
Reason: reason,
})
return err
}
// UnbanPubkey removes a pubkey from the ban list.
func (c *Client) UnbanPubkey(pubkey string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := c.client.UnbanPubkey(ctx, &orlyaclv1.PubkeyRequest{
Pubkey: pubkey,
})
return err
}
// AllowPubkey adds a pubkey to the allow list.
func (c *Client) AllowPubkey(pubkey, reason string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := c.client.AllowPubkey(ctx, &orlyaclv1.AllowPubkeyRequest{
Pubkey: pubkey,
Reason: reason,
})
return err
}
// DisallowPubkey removes a pubkey from the allow list.
func (c *Client) DisallowPubkey(pubkey string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := c.client.DisallowPubkey(ctx, &orlyaclv1.PubkeyRequest{
Pubkey: pubkey,
})
return err
}
// BlockIP adds an IP to the block list.
func (c *Client) BlockIP(ip, reason string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := c.client.BlockIP(ctx, &orlyaclv1.BlockIPRequest{
Ip: ip,
Reason: reason,
})
return err
}
// UnblockIP removes an IP from the block list.
func (c *Client) UnblockIP(ip string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := c.client.UnblockIP(ctx, &orlyaclv1.IPRequest{
Ip: ip,
})
return err
}
// UpdatePeerAdmins updates the peer relay identity pubkeys.
func (c *Client) UpdatePeerAdmins(peerPubkeys [][]byte) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := c.client.UpdatePeerAdmins(ctx, &orlyaclv1.UpdatePeerAdminsRequest{
PeerPubkeys: peerPubkeys,
})
return err
}
// === Curating ACL Methods ===
// TrustPubkey adds a pubkey to the trusted list.
func (c *Client) TrustPubkey(pubkey, note string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := c.client.TrustPubkey(ctx, &orlyaclv1.TrustPubkeyRequest{
Pubkey: pubkey,
Note: note,
})
return err
}
// UntrustPubkey removes a pubkey from the trusted list.
func (c *Client) UntrustPubkey(pubkey string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := c.client.UntrustPubkey(ctx, &orlyaclv1.PubkeyRequest{
Pubkey: pubkey,
})
return err
}
// BlacklistPubkey adds a pubkey to the blacklist.
func (c *Client) BlacklistPubkey(pubkey, reason string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := c.client.BlacklistPubkey(ctx, &orlyaclv1.BlacklistPubkeyRequest{
Pubkey: pubkey,
Reason: reason,
})
return err
}
// UnblacklistPubkey removes a pubkey from the blacklist.
func (c *Client) UnblacklistPubkey(pubkey string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := c.client.UnblacklistPubkey(ctx, &orlyaclv1.PubkeyRequest{
Pubkey: pubkey,
})
return err
}
// RateLimitCheck checks if a pubkey/IP can publish.
func (c *Client) RateLimitCheck(pubkey, ip string) (allowed bool, message string, err error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := c.client.RateLimitCheck(ctx, &orlyaclv1.RateLimitCheckRequest{
Pubkey: pubkey,
Ip: ip,
})
if err != nil {
return false, "", err
}
return resp.Allowed, resp.Message, nil
}
// IsCuratingConfigured checks if curating mode is configured.
func (c *Client) IsCuratingConfigured() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := c.client.IsCuratingConfigured(ctx, &orlyaclv1.Empty{})
if err != nil {
return false, err
}
return resp.Value, nil
}

32
pkg/acl/server/config.go

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
// Package server provides a shared gRPC ACL server implementation.
package server
import "time"
// Config holds configuration for the ACL gRPC server.
type Config struct {
// Listen is the gRPC server listen address
Listen string
// ACLMode is the active ACL mode (none, follows, managed, curating)
ACLMode string
// LogLevel is the logging level
LogLevel string
// Owner and admin lists
Owners []string
Admins []string
// Bootstrap relays for follow list syncing
BootstrapRelays []string
// Relay addresses (self)
RelayAddresses []string
// Follows ACL configuration
FollowListFrequency time.Duration
FollowsThrottleEnabled bool
FollowsThrottlePerEvent time.Duration
FollowsThrottleMaxDelay time.Duration
}

144
pkg/acl/server/server.go

@ -0,0 +1,144 @@ @@ -0,0 +1,144 @@
package server
import (
"context"
"net"
"os"
"os/signal"
"syscall"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/app/config"
"next.orly.dev/pkg/acl"
"next.orly.dev/pkg/database"
orlyaclv1 "next.orly.dev/pkg/proto/orlyacl/v1"
)
// Server wraps a gRPC ACL server.
type Server struct {
grpcServer *grpc.Server
db database.Database
cfg *Config
listener net.Listener
ownsDB bool // Whether we own the database and should close it
}
// New creates a new ACL gRPC server.
func New(db database.Database, cfg *Config, ownsDB bool) *Server {
// Create gRPC server
grpcServer := grpc.NewServer(
grpc.MaxRecvMsgSize(16<<20), // 16MB
grpc.MaxSendMsgSize(16<<20), // 16MB
)
// Register ACL service
service := NewACLService(cfg, db)
orlyaclv1.RegisterACLServiceServer(grpcServer, service)
// Register reflection for debugging with grpcurl
reflection.Register(grpcServer)
return &Server{
grpcServer: grpcServer,
db: db,
cfg: cfg,
ownsDB: ownsDB,
}
}
// ConfigureACL sets up the ACL mode and configures the registry.
func (s *Server) ConfigureACL(ctx context.Context) error {
// Create app config for ACL configuration
appCfg := &config.C{
Owners: s.cfg.Owners,
Admins: s.cfg.Admins,
BootstrapRelays: s.cfg.BootstrapRelays,
RelayAddresses: s.cfg.RelayAddresses,
FollowListFrequency: s.cfg.FollowListFrequency,
FollowsThrottleEnabled: s.cfg.FollowsThrottleEnabled,
FollowsThrottlePerEvent: s.cfg.FollowsThrottlePerEvent,
FollowsThrottleMaxDelay: s.cfg.FollowsThrottleMaxDelay,
}
// Set ACL mode and configure the registry
acl.Registry.SetMode(s.cfg.ACLMode)
if err := acl.Registry.Configure(appCfg, s.db, ctx); chk.E(err) {
return err
}
// Start the syncer goroutine for background operations
acl.Registry.Syncer()
log.I.F("ACL syncer started for mode: %s", s.cfg.ACLMode)
return nil
}
// ListenAndServe starts the gRPC server.
func (s *Server) ListenAndServe(ctx context.Context, cancel context.CancelFunc) error {
// Start listening
lis, err := net.Listen("tcp", s.cfg.Listen)
if chk.E(err) {
return err
}
s.listener = lis
log.I.F("gRPC ACL server listening on %s", s.cfg.Listen)
// Handle graceful shutdown
go s.handleShutdown(ctx, cancel)
// Serve gRPC
return s.grpcServer.Serve(lis)
}
func (s *Server) handleShutdown(ctx context.Context, cancel context.CancelFunc) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT)
select {
case sig := <-sigs:
log.I.F("received signal %v, shutting down...", sig)
case <-ctx.Done():
log.I.F("context cancelled, shutting down...")
}
// Cancel context to stop all operations
cancel()
// Gracefully stop gRPC server with timeout
stopped := make(chan struct{})
go func() {
s.grpcServer.GracefulStop()
close(stopped)
}()
select {
case <-stopped:
log.I.F("gRPC server stopped gracefully")
case <-time.After(5 * time.Second):
log.W.F("gRPC graceful stop timed out, forcing stop")
s.grpcServer.Stop()
}
// Sync and close database if we own it
if s.ownsDB {
log.I.F("syncing database...")
if err := s.db.Sync(); chk.E(err) {
log.W.F("failed to sync database: %v", err)
}
log.I.F("closing database...")
if err := s.db.Close(); chk.E(err) {
log.W.F("failed to close database: %v", err)
}
}
log.I.F("shutdown complete")
}
// Stop stops the server.
func (s *Server) Stop() {
s.grpcServer.Stop()
}

788
pkg/acl/server/service.go

@ -0,0 +1,788 @@ @@ -0,0 +1,788 @@
package server
import (
"context"
"encoding/hex"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"lol.mleku.dev/log"
"next.orly.dev/pkg/acl"
"next.orly.dev/pkg/database"
orlyaclv1 "next.orly.dev/pkg/proto/orlyacl/v1"
orlydbv1 "next.orly.dev/pkg/proto/orlydb/v1"
)
// ACLService implements the orlyaclv1.ACLServiceServer interface.
type ACLService struct {
orlyaclv1.UnimplementedACLServiceServer
cfg *Config
db database.Database
}
// NewACLService creates a new ACL service.
func NewACLService(cfg *Config, db database.Database) *ACLService {
return &ACLService{
cfg: cfg,
db: db,
}
}
// === Core ACL Methods ===
func (s *ACLService) GetAccessLevel(ctx context.Context, req *orlyaclv1.AccessLevelRequest) (*orlyaclv1.AccessLevelResponse, error) {
level := acl.Registry.GetAccessLevel(req.Pubkey, req.Address)
return &orlyaclv1.AccessLevelResponse{Level: level}, nil
}
func (s *ACLService) CheckPolicy(ctx context.Context, req *orlyaclv1.PolicyCheckRequest) (*orlyaclv1.PolicyCheckResponse, error) {
ev := orlydbv1.ProtoToEvent(req.Event)
allowed, err := acl.Registry.CheckPolicy(ev)
resp := &orlyaclv1.PolicyCheckResponse{Allowed: allowed}
if err != nil {
resp.Error = err.Error()
}
return resp, nil
}
func (s *ACLService) GetACLInfo(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ACLInfoResponse, error) {
name, description, documentation := acl.Registry.GetACLInfo()
return &orlyaclv1.ACLInfoResponse{
Name: name,
Description: description,
Documentation: documentation,
}, nil
}
func (s *ACLService) GetMode(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ModeResponse, error) {
return &orlyaclv1.ModeResponse{Mode: acl.Registry.Type()}, nil
}
func (s *ACLService) Ready(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ReadyResponse, error) {
// Check if database is ready
select {
case <-s.db.Ready():
return &orlyaclv1.ReadyResponse{Ready: true}, nil
default:
return &orlyaclv1.ReadyResponse{Ready: false}, nil
}
}
// === Follows ACL Methods ===
func (s *ACLService) GetThrottleDelay(ctx context.Context, req *orlyaclv1.ThrottleDelayRequest) (*orlyaclv1.ThrottleDelayResponse, error) {
// Get the active ACL and check if it's Follows
for _, i := range acl.Registry.ACL {
if i.Type() == "follows" {
if follows, ok := i.(*acl.Follows); ok {
delay := follows.GetThrottleDelay(req.Pubkey, req.Ip)
return &orlyaclv1.ThrottleDelayResponse{DelayMs: delay.Milliseconds()}, nil
}
}
}
return &orlyaclv1.ThrottleDelayResponse{DelayMs: 0}, nil
}
func (s *ACLService) AddFollow(ctx context.Context, req *orlyaclv1.AddFollowRequest) (*orlyaclv1.Empty, error) {
acl.Registry.AddFollow(req.Pubkey)
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) GetFollowedPubkeys(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.FollowedPubkeysResponse, error) {
for _, i := range acl.Registry.ACL {
if i.Type() == "follows" {
if follows, ok := i.(*acl.Follows); ok {
pubkeys := follows.GetFollowedPubkeys()
return &orlyaclv1.FollowedPubkeysResponse{Pubkeys: pubkeys}, nil
}
}
}
return &orlyaclv1.FollowedPubkeysResponse{}, nil
}
func (s *ACLService) GetAdminRelays(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.AdminRelaysResponse, error) {
for _, i := range acl.Registry.ACL {
if i.Type() == "follows" {
if follows, ok := i.(*acl.Follows); ok {
urls := follows.AdminRelays()
return &orlyaclv1.AdminRelaysResponse{Urls: urls}, nil
}
}
}
return &orlyaclv1.AdminRelaysResponse{}, nil
}
// === Managed ACL Methods ===
func (s *ACLService) BanPubkey(ctx context.Context, req *orlyaclv1.BanPubkeyRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.SaveBannedPubkey(req.Pubkey, req.Reason); err != nil {
return nil, status.Errorf(codes.Internal, "failed to ban pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) UnbanPubkey(ctx context.Context, req *orlyaclv1.PubkeyRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.RemoveBannedPubkey(req.Pubkey); err != nil {
return nil, status.Errorf(codes.Internal, "failed to unban pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListBannedPubkeys(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListBannedPubkeysResponse, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
banned, err := managedACL.ListBannedPubkeys()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list banned pubkeys: %v", err)
}
resp := &orlyaclv1.ListBannedPubkeysResponse{}
for _, b := range banned {
resp.Pubkeys = append(resp.Pubkeys, &orlyaclv1.BannedPubkey{
Pubkey: b.Pubkey,
Reason: b.Reason,
Added: b.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) AllowPubkey(ctx context.Context, req *orlyaclv1.AllowPubkeyRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.SaveAllowedPubkey(req.Pubkey, req.Reason); err != nil {
return nil, status.Errorf(codes.Internal, "failed to allow pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) DisallowPubkey(ctx context.Context, req *orlyaclv1.PubkeyRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.RemoveAllowedPubkey(req.Pubkey); err != nil {
return nil, status.Errorf(codes.Internal, "failed to disallow pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListAllowedPubkeys(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListAllowedPubkeysResponse, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
allowed, err := managedACL.ListAllowedPubkeys()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list allowed pubkeys: %v", err)
}
resp := &orlyaclv1.ListAllowedPubkeysResponse{}
for _, a := range allowed {
resp.Pubkeys = append(resp.Pubkeys, &orlyaclv1.AllowedPubkey{
Pubkey: a.Pubkey,
Reason: a.Reason,
Added: a.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) BanEvent(ctx context.Context, req *orlyaclv1.BanEventRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.SaveBannedEvent(req.EventId, req.Reason); err != nil {
return nil, status.Errorf(codes.Internal, "failed to ban event: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) UnbanEvent(ctx context.Context, req *orlyaclv1.EventRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.RemoveBannedEvent(req.EventId); err != nil {
return nil, status.Errorf(codes.Internal, "failed to unban event: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListBannedEvents(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListBannedEventsResponse, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
banned, err := managedACL.ListBannedEvents()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list banned events: %v", err)
}
resp := &orlyaclv1.ListBannedEventsResponse{}
for _, b := range banned {
resp.Events = append(resp.Events, &orlyaclv1.BannedEvent{
EventId: b.ID,
Reason: b.Reason,
Added: b.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) AllowEvent(ctx context.Context, req *orlyaclv1.BanEventRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.SaveAllowedEvent(req.EventId, req.Reason); err != nil {
return nil, status.Errorf(codes.Internal, "failed to allow event: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) DisallowEvent(ctx context.Context, req *orlyaclv1.EventRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.RemoveAllowedEvent(req.EventId); err != nil {
return nil, status.Errorf(codes.Internal, "failed to disallow event: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListAllowedEvents(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListAllowedEventsResponse, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
allowed, err := managedACL.ListAllowedEvents()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list allowed events: %v", err)
}
resp := &orlyaclv1.ListAllowedEventsResponse{}
for _, a := range allowed {
resp.Events = append(resp.Events, &orlyaclv1.AllowedEvent{
EventId: a.ID,
Reason: a.Reason,
Added: a.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) BlockIP(ctx context.Context, req *orlyaclv1.BlockIPRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.SaveBlockedIP(req.Ip, req.Reason); err != nil {
return nil, status.Errorf(codes.Internal, "failed to block IP: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) UnblockIP(ctx context.Context, req *orlyaclv1.IPRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.RemoveBlockedIP(req.Ip); err != nil {
return nil, status.Errorf(codes.Internal, "failed to unblock IP: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListBlockedIPs(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListBlockedIPsResponse, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
blocked, err := managedACL.ListBlockedIPs()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list blocked IPs: %v", err)
}
resp := &orlyaclv1.ListBlockedIPsResponse{}
for _, b := range blocked {
resp.Ips = append(resp.Ips, &orlyaclv1.BlockedIP{
Ip: b.IP,
Reason: b.Reason,
Added: b.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) AllowKind(ctx context.Context, req *orlyaclv1.AllowKindRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.SaveAllowedKind(int(req.Kind)); err != nil {
return nil, status.Errorf(codes.Internal, "failed to allow kind: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) DisallowKind(ctx context.Context, req *orlyaclv1.KindRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
if err := managedACL.RemoveAllowedKind(int(req.Kind)); err != nil {
return nil, status.Errorf(codes.Internal, "failed to disallow kind: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListAllowedKinds(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListAllowedKindsResponse, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managedACL := managed.GetManagedACL()
if managedACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL database not available")
}
kinds, err := managedACL.ListAllowedKinds()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list allowed kinds: %v", err)
}
resp := &orlyaclv1.ListAllowedKindsResponse{}
for _, k := range kinds {
resp.Kinds = append(resp.Kinds, int32(k))
}
return resp, nil
}
func (s *ACLService) UpdatePeerAdmins(ctx context.Context, req *orlyaclv1.UpdatePeerAdminsRequest) (*orlyaclv1.Empty, error) {
managed := s.getManagedACL()
if managed == nil {
return nil, status.Errorf(codes.FailedPrecondition, "managed ACL not available")
}
managed.UpdatePeerAdmins(req.PeerPubkeys)
return &orlyaclv1.Empty{}, nil
}
// === Curating ACL Methods ===
func (s *ACLService) TrustPubkey(ctx context.Context, req *orlyaclv1.TrustPubkeyRequest) (*orlyaclv1.Empty, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
if err := curating.TrustPubkey(req.Pubkey, req.Note); err != nil {
return nil, status.Errorf(codes.Internal, "failed to trust pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) UntrustPubkey(ctx context.Context, req *orlyaclv1.PubkeyRequest) (*orlyaclv1.Empty, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
if err := curating.UntrustPubkey(req.Pubkey); err != nil {
return nil, status.Errorf(codes.Internal, "failed to untrust pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListTrustedPubkeys(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListTrustedPubkeysResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
trusted, err := curatingACL.ListTrustedPubkeys()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list trusted pubkeys: %v", err)
}
resp := &orlyaclv1.ListTrustedPubkeysResponse{}
for _, t := range trusted {
resp.Pubkeys = append(resp.Pubkeys, &orlyaclv1.TrustedPubkey{
Pubkey: t.Pubkey,
Note: t.Note,
Added: t.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) BlacklistPubkey(ctx context.Context, req *orlyaclv1.BlacklistPubkeyRequest) (*orlyaclv1.Empty, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
if err := curating.BlacklistPubkey(req.Pubkey, req.Reason); err != nil {
return nil, status.Errorf(codes.Internal, "failed to blacklist pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) UnblacklistPubkey(ctx context.Context, req *orlyaclv1.PubkeyRequest) (*orlyaclv1.Empty, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
if err := curating.UnblacklistPubkey(req.Pubkey); err != nil {
return nil, status.Errorf(codes.Internal, "failed to unblacklist pubkey: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListBlacklistedPubkeys(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListBlacklistedPubkeysResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
blacklisted, err := curatingACL.ListBlacklistedPubkeys()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list blacklisted pubkeys: %v", err)
}
resp := &orlyaclv1.ListBlacklistedPubkeysResponse{}
for _, b := range blacklisted {
resp.Pubkeys = append(resp.Pubkeys, &orlyaclv1.BlacklistedPubkey{
Pubkey: b.Pubkey,
Reason: b.Reason,
Added: b.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) MarkSpam(ctx context.Context, req *orlyaclv1.MarkSpamRequest) (*orlyaclv1.Empty, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
if err := curatingACL.MarkEventAsSpam(req.EventId, req.Pubkey, req.Reason); err != nil {
return nil, status.Errorf(codes.Internal, "failed to mark spam: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) UnmarkSpam(ctx context.Context, req *orlyaclv1.EventRequest) (*orlyaclv1.Empty, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
if err := curatingACL.UnmarkEventAsSpam(req.EventId); err != nil {
return nil, status.Errorf(codes.Internal, "failed to unmark spam: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) ListSpamEvents(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ListSpamEventsResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
spam, err := curatingACL.ListSpamEvents()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list spam events: %v", err)
}
resp := &orlyaclv1.ListSpamEventsResponse{}
for _, se := range spam {
resp.Events = append(resp.Events, &orlyaclv1.SpamEvent{
EventId: se.EventID,
Pubkey: se.Pubkey,
Reason: se.Reason,
Added: se.Added.Unix(),
})
}
return resp, nil
}
func (s *ACLService) RateLimitCheck(ctx context.Context, req *orlyaclv1.RateLimitCheckRequest) (*orlyaclv1.RateLimitCheckResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
allowed, message, err := curating.RateLimitCheck(req.Pubkey, req.Ip)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to check rate limit: %v", err)
}
return &orlyaclv1.RateLimitCheckResponse{
Allowed: allowed,
Message: message,
}, nil
}
func (s *ACLService) ProcessConfigEvent(ctx context.Context, req *orlyaclv1.ConfigEventRequest) (*orlyaclv1.Empty, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
ev := orlydbv1.ProtoToEvent(req.Event)
if err := curating.ProcessConfigEvent(ev); err != nil {
return nil, status.Errorf(codes.Internal, "failed to process config event: %v", err)
}
return &orlyaclv1.Empty{}, nil
}
func (s *ACLService) GetCuratingConfig(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.CuratingConfig, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
config, err := curating.GetConfig()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get config: %v", err)
}
resp := &orlyaclv1.CuratingConfig{
ConfigEventId: config.ConfigEventID,
ConfigPubkey: config.ConfigPubkey,
ConfiguredAt: config.ConfiguredAt,
DailyLimit: int32(config.DailyLimit),
IpDailyLimit: int32(config.IPDailyLimit),
FirstBanHours: int32(config.FirstBanHours),
SecondBanHours: int32(config.SecondBanHours),
KindCategories: config.KindCategories,
AllowedRanges: config.AllowedRanges,
}
for _, k := range config.AllowedKinds {
resp.AllowedKinds = append(resp.AllowedKinds, int32(k))
}
return resp, nil
}
func (s *ACLService) IsCuratingConfigured(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.BoolResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return &orlyaclv1.BoolResponse{Value: false}, nil
}
configured, err := curating.IsConfigured()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to check if configured: %v", err)
}
return &orlyaclv1.BoolResponse{Value: configured}, nil
}
func (s *ACLService) ListUnclassifiedUsers(ctx context.Context, req *orlyaclv1.PaginationRequest) (*orlyaclv1.ListUnclassifiedUsersResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
// The underlying ListUnclassifiedUsers only takes limit, not offset
// We'll request limit+offset and skip the first offset items
limit := int(req.Limit)
offset := int(req.Offset)
if limit == 0 {
limit = 100 // Default limit
}
users, err := curatingACL.ListUnclassifiedUsers(limit + offset)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list unclassified users: %v", err)
}
// Apply offset
if offset > 0 && len(users) > offset {
users = users[offset:]
} else if offset > 0 {
users = nil
}
// Apply limit
if limit > 0 && len(users) > limit {
users = users[:limit]
}
resp := &orlyaclv1.ListUnclassifiedUsersResponse{Total: int32(len(users))}
for _, u := range users {
resp.Users = append(resp.Users, &orlyaclv1.UnclassifiedUser{
Pubkey: u.Pubkey,
EventCount: int32(u.EventCount),
FirstSeen: u.LastEvent.Format("2006-01-02T15:04:05Z"),
})
}
return resp, nil
}
func (s *ACLService) GetEventsForPubkey(ctx context.Context, req *orlyaclv1.GetEventsForPubkeyRequest) (*orlyaclv1.EventsForPubkeyResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
events, total, err := curatingACL.GetEventsForPubkey(req.Pubkey, int(req.Limit), int(req.Offset))
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get events for pubkey: %v", err)
}
resp := &orlyaclv1.EventsForPubkeyResponse{Total: int32(total)}
for _, ev := range events {
resp.Events = append(resp.Events, &orlyaclv1.EventSummary{
Id: ev.ID,
Kind: uint32(ev.Kind),
Content: []byte(ev.Content),
CreatedAt: ev.CreatedAt,
})
}
return resp, nil
}
func (s *ACLService) DeleteEventsForPubkey(ctx context.Context, req *orlyaclv1.DeleteEventsForPubkeyRequest) (*orlyaclv1.DeleteCountResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
count, err := curatingACL.DeleteEventsForPubkey(req.Pubkey)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete events for pubkey: %v", err)
}
return &orlyaclv1.DeleteCountResponse{Count: int32(count)}, nil
}
func (s *ACLService) ScanAllPubkeys(ctx context.Context, req *orlyaclv1.Empty) (*orlyaclv1.ScanResultResponse, error) {
curating := s.getCuratingACL()
if curating == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL not available")
}
curatingACL := curating.GetCuratingACL()
if curatingACL == nil {
return nil, status.Errorf(codes.FailedPrecondition, "curating ACL database not available")
}
result, err := curatingACL.ScanAllPubkeys()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to scan all pubkeys: %v", err)
}
return &orlyaclv1.ScanResultResponse{
TotalPubkeys: int32(result.TotalPubkeys),
TotalEvents: int32(result.TotalEvents),
}, nil
}
// === Helper Methods ===
func (s *ACLService) getManagedACL() *acl.Managed {
for _, i := range acl.Registry.ACL {
if i.Type() == "managed" {
if managed, ok := i.(*acl.Managed); ok {
return managed
}
}
}
return nil
}
func (s *ACLService) getCuratingACL() *acl.Curating {
for _, i := range acl.Registry.ACL {
if i.Type() == "curating" {
if curating, ok := i.(*acl.Curating); ok {
return curating
}
}
}
return nil
}
// Unused but may be needed for debugging
var _ = log.T
var _ = hex.EncodeToString

33
pkg/database/server/config.go

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
// Package server provides a shared gRPC database server implementation.
package server
import "time"
// Config holds configuration for the database gRPC server.
type Config struct {
// Listen is the gRPC server listen address
Listen string
// LogLevel is the logging level
LogLevel string
// StreamBatchSize is the number of events per stream batch
StreamBatchSize int
// MaxConcurrentQueries is the max concurrent queries
MaxConcurrentQueries int
}
// DatabaseConfig holds Badger-specific configuration.
type DatabaseConfig struct {
DataDir string
LogLevel string
BlockCacheMB int
IndexCacheMB int
ZSTDLevel int
QueryCacheSizeMB int
QueryCacheMaxAge time.Duration
QueryCacheDisabled bool
SerialCachePubkeys int
SerialCacheEventIds int
}

111
pkg/database/server/server.go

@ -0,0 +1,111 @@ @@ -0,0 +1,111 @@
package server
import (
"context"
"net"
"os"
"os/signal"
"syscall"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/pkg/database"
orlydbv1 "next.orly.dev/pkg/proto/orlydb/v1"
)
// Server wraps a gRPC database server.
type Server struct {
grpcServer *grpc.Server
db database.Database
cfg *Config
listener net.Listener
}
// New creates a new database gRPC server.
func New(db database.Database, cfg *Config) *Server {
// Create gRPC server with large message sizes for events
grpcServer := grpc.NewServer(
grpc.MaxRecvMsgSize(64<<20), // 64MB
grpc.MaxSendMsgSize(64<<20), // 64MB
)
// Register database service
service := NewDatabaseService(db, cfg)
orlydbv1.RegisterDatabaseServiceServer(grpcServer, service)
// Register reflection for debugging with grpcurl
reflection.Register(grpcServer)
return &Server{
grpcServer: grpcServer,
db: db,
cfg: cfg,
}
}
// ListenAndServe starts the gRPC server.
func (s *Server) ListenAndServe(ctx context.Context, cancel context.CancelFunc) error {
// Start listening
lis, err := net.Listen("tcp", s.cfg.Listen)
if chk.E(err) {
return err
}
s.listener = lis
log.I.F("gRPC database server listening on %s", s.cfg.Listen)
// Handle graceful shutdown
go s.handleShutdown(ctx, cancel)
// Serve gRPC
return s.grpcServer.Serve(lis)
}
func (s *Server) handleShutdown(ctx context.Context, cancel context.CancelFunc) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT)
select {
case sig := <-sigs:
log.I.F("received signal %v, shutting down...", sig)
case <-ctx.Done():
log.I.F("context cancelled, shutting down...")
}
// Cancel context to stop all operations
cancel()
// Gracefully stop gRPC server with timeout
stopped := make(chan struct{})
go func() {
s.grpcServer.GracefulStop()
close(stopped)
}()
select {
case <-stopped:
log.I.F("gRPC server stopped gracefully")
case <-time.After(5 * time.Second):
log.W.F("gRPC graceful stop timed out, forcing stop")
s.grpcServer.Stop()
}
// Sync and close database
log.I.F("syncing database...")
if err := s.db.Sync(); chk.E(err) {
log.W.F("failed to sync database: %v", err)
}
log.I.F("closing database...")
if err := s.db.Close(); chk.E(err) {
log.W.F("failed to close database: %v", err)
}
log.I.F("shutdown complete")
}
// Stop stops the server.
func (s *Server) Stop() {
s.grpcServer.Stop()
}

731
pkg/database/server/service.go

@ -0,0 +1,731 @@ @@ -0,0 +1,731 @@
package server
import (
"context"
"io"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/pkg/database"
orlydbv1 "next.orly.dev/pkg/proto/orlydb/v1"
)
// DatabaseService implements the orlydbv1.DatabaseServiceServer interface.
type DatabaseService struct {
orlydbv1.UnimplementedDatabaseServiceServer
db database.Database
cfg *Config
}
// NewDatabaseService creates a new database service.
func NewDatabaseService(db database.Database, cfg *Config) *DatabaseService {
return &DatabaseService{
db: db,
cfg: cfg,
}
}
// === Lifecycle Methods ===
func (s *DatabaseService) GetPath(ctx context.Context, req *orlydbv1.Empty) (*orlydbv1.PathResponse, error) {
return &orlydbv1.PathResponse{Path: s.db.Path()}, nil
}
func (s *DatabaseService) Sync(ctx context.Context, req *orlydbv1.Empty) (*orlydbv1.Empty, error) {
if err := s.db.Sync(); err != nil {
return nil, status.Errorf(codes.Internal, "sync failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) Ready(ctx context.Context, req *orlydbv1.Empty) (*orlydbv1.ReadyResponse, error) {
// Check if ready channel is closed
select {
case <-s.db.Ready():
return &orlydbv1.ReadyResponse{Ready: true}, nil
default:
return &orlydbv1.ReadyResponse{Ready: false}, nil
}
}
func (s *DatabaseService) SetLogLevel(ctx context.Context, req *orlydbv1.SetLogLevelRequest) (*orlydbv1.Empty, error) {
s.db.SetLogLevel(req.Level)
return &orlydbv1.Empty{}, nil
}
// === Event Storage ===
func (s *DatabaseService) SaveEvent(ctx context.Context, req *orlydbv1.SaveEventRequest) (*orlydbv1.SaveEventResponse, error) {
ev := orlydbv1.ProtoToEvent(req.Event)
exists, err := s.db.SaveEvent(ctx, ev)
if err != nil {
return nil, status.Errorf(codes.Internal, "save event failed: %v", err)
}
return &orlydbv1.SaveEventResponse{Exists: exists}, nil
}
func (s *DatabaseService) GetSerialsFromFilter(ctx context.Context, req *orlydbv1.GetSerialsFromFilterRequest) (*orlydbv1.SerialList, error) {
f := orlydbv1.ProtoToFilter(req.Filter)
serials, err := s.db.GetSerialsFromFilter(f)
if err != nil {
return nil, status.Errorf(codes.Internal, "get serials failed: %v", err)
}
return orlydbv1.Uint40sToProto(serials), nil
}
func (s *DatabaseService) WouldReplaceEvent(ctx context.Context, req *orlydbv1.WouldReplaceEventRequest) (*orlydbv1.WouldReplaceEventResponse, error) {
ev := orlydbv1.ProtoToEvent(req.Event)
wouldReplace, replacedSerials, err := s.db.WouldReplaceEvent(ev)
if err != nil {
return nil, status.Errorf(codes.Internal, "would replace check failed: %v", err)
}
resp := &orlydbv1.WouldReplaceEventResponse{
WouldReplace: wouldReplace,
}
for _, ser := range replacedSerials {
resp.ReplacedSerials = append(resp.ReplacedSerials, ser.Get())
}
return resp, nil
}
// === Event Queries (Streaming) ===
func (s *DatabaseService) QueryEvents(req *orlydbv1.QueryEventsRequest, stream orlydbv1.DatabaseService_QueryEventsServer) error {
f := orlydbv1.ProtoToFilter(req.Filter)
events, err := s.db.QueryEvents(stream.Context(), f)
if err != nil {
return status.Errorf(codes.Internal, "query events failed: %v", err)
}
return s.streamEvents(orlydbv1.EventsToProto(events), stream)
}
func (s *DatabaseService) QueryAllVersions(req *orlydbv1.QueryEventsRequest, stream orlydbv1.DatabaseService_QueryAllVersionsServer) error {
f := orlydbv1.ProtoToFilter(req.Filter)
events, err := s.db.QueryAllVersions(stream.Context(), f)
if err != nil {
return status.Errorf(codes.Internal, "query all versions failed: %v", err)
}
return s.streamEvents(orlydbv1.EventsToProto(events), stream)
}
func (s *DatabaseService) QueryEventsWithOptions(req *orlydbv1.QueryEventsWithOptionsRequest, stream orlydbv1.DatabaseService_QueryEventsWithOptionsServer) error {
f := orlydbv1.ProtoToFilter(req.Filter)
events, err := s.db.QueryEventsWithOptions(stream.Context(), f, req.IncludeDeleteEvents, req.ShowAllVersions)
if err != nil {
return status.Errorf(codes.Internal, "query events with options failed: %v", err)
}
return s.streamEvents(orlydbv1.EventsToProto(events), stream)
}
func (s *DatabaseService) QueryDeleteEventsByTargetId(req *orlydbv1.QueryDeleteEventsByTargetIdRequest, stream orlydbv1.DatabaseService_QueryDeleteEventsByTargetIdServer) error {
events, err := s.db.QueryDeleteEventsByTargetId(stream.Context(), req.TargetEventId)
if err != nil {
return status.Errorf(codes.Internal, "query delete events failed: %v", err)
}
return s.streamEvents(orlydbv1.EventsToProto(events), stream)
}
func (s *DatabaseService) QueryForSerials(ctx context.Context, req *orlydbv1.QueryEventsRequest) (*orlydbv1.SerialList, error) {
f := orlydbv1.ProtoToFilter(req.Filter)
serials, err := s.db.QueryForSerials(ctx, f)
if err != nil {
return nil, status.Errorf(codes.Internal, "query for serials failed: %v", err)
}
return orlydbv1.Uint40sToProto(serials), nil
}
func (s *DatabaseService) QueryForIds(ctx context.Context, req *orlydbv1.QueryEventsRequest) (*orlydbv1.IdPkTsList, error) {
f := orlydbv1.ProtoToFilter(req.Filter)
idPkTs, err := s.db.QueryForIds(ctx, f)
if err != nil {
return nil, status.Errorf(codes.Internal, "query for ids failed: %v", err)
}
return orlydbv1.IdPkTsListToProto(idPkTs), nil
}
func (s *DatabaseService) CountEvents(ctx context.Context, req *orlydbv1.QueryEventsRequest) (*orlydbv1.CountEventsResponse, error) {
f := orlydbv1.ProtoToFilter(req.Filter)
count, approximate, err := s.db.CountEvents(ctx, f)
if err != nil {
return nil, status.Errorf(codes.Internal, "count events failed: %v", err)
}
return &orlydbv1.CountEventsResponse{
Count: int32(count),
Approximate: approximate,
}, nil
}
// === Event Retrieval by Serial ===
func (s *DatabaseService) FetchEventBySerial(ctx context.Context, req *orlydbv1.FetchEventBySerialRequest) (*orlydbv1.FetchEventBySerialResponse, error) {
ser := orlydbv1.ProtoToUint40(&orlydbv1.Uint40{Value: req.Serial})
ev, err := s.db.FetchEventBySerial(ser)
if err != nil {
return nil, status.Errorf(codes.Internal, "fetch event by serial failed: %v", err)
}
return &orlydbv1.FetchEventBySerialResponse{
Event: orlydbv1.EventToProto(ev),
Found: ev != nil,
}, nil
}
func (s *DatabaseService) FetchEventsBySerials(ctx context.Context, req *orlydbv1.FetchEventsBySerialRequest) (*orlydbv1.EventMap, error) {
serials := orlydbv1.ProtoToUint40s(&orlydbv1.SerialList{Serials: req.Serials})
events, err := s.db.FetchEventsBySerials(serials)
if err != nil {
return nil, status.Errorf(codes.Internal, "fetch events by serials failed: %v", err)
}
return orlydbv1.EventMapToProto(events), nil
}
func (s *DatabaseService) GetSerialById(ctx context.Context, req *orlydbv1.GetSerialByIdRequest) (*orlydbv1.GetSerialByIdResponse, error) {
ser, err := s.db.GetSerialById(req.Id)
if err != nil {
return nil, status.Errorf(codes.Internal, "get serial by id failed: %v", err)
}
if ser == nil {
return &orlydbv1.GetSerialByIdResponse{Found: false}, nil
}
return &orlydbv1.GetSerialByIdResponse{
Serial: ser.Get(),
Found: true,
}, nil
}
func (s *DatabaseService) GetSerialsByIds(ctx context.Context, req *orlydbv1.GetSerialsByIdsRequest) (*orlydbv1.SerialMap, error) {
// Convert request IDs to tag format
ids := orlydbv1.BytesToTag(req.Ids)
serials, err := s.db.GetSerialsByIds(ids)
if err != nil {
return nil, status.Errorf(codes.Internal, "get serials by ids failed: %v", err)
}
result := &orlydbv1.SerialMap{
Serials: make(map[string]uint64),
}
for k, v := range serials {
if v != nil {
result.Serials[k] = v.Get()
}
}
return result, nil
}
func (s *DatabaseService) GetSerialsByRange(ctx context.Context, req *orlydbv1.GetSerialsByRangeRequest) (*orlydbv1.SerialList, error) {
r := orlydbv1.ProtoToRange(req.Range)
serials, err := s.db.GetSerialsByRange(r)
if err != nil {
return nil, status.Errorf(codes.Internal, "get serials by range failed: %v", err)
}
return orlydbv1.Uint40sToProto(serials), nil
}
func (s *DatabaseService) GetFullIdPubkeyBySerial(ctx context.Context, req *orlydbv1.GetFullIdPubkeyBySerialRequest) (*orlydbv1.IdPkTs, error) {
ser := orlydbv1.ProtoToUint40(&orlydbv1.Uint40{Value: req.Serial})
idPkTs, err := s.db.GetFullIdPubkeyBySerial(ser)
if err != nil {
return nil, status.Errorf(codes.Internal, "get full id pubkey by serial failed: %v", err)
}
return orlydbv1.IdPkTsToProto(idPkTs), nil
}
func (s *DatabaseService) GetFullIdPubkeyBySerials(ctx context.Context, req *orlydbv1.GetFullIdPubkeyBySerialsRequest) (*orlydbv1.IdPkTsList, error) {
serials := orlydbv1.ProtoToUint40s(&orlydbv1.SerialList{Serials: req.Serials})
idPkTs, err := s.db.GetFullIdPubkeyBySerials(serials)
if err != nil {
return nil, status.Errorf(codes.Internal, "get full id pubkey by serials failed: %v", err)
}
return orlydbv1.IdPkTsListToProto(idPkTs), nil
}
// === Event Deletion ===
func (s *DatabaseService) DeleteEvent(ctx context.Context, req *orlydbv1.DeleteEventRequest) (*orlydbv1.Empty, error) {
if err := s.db.DeleteEvent(ctx, req.EventId); err != nil {
return nil, status.Errorf(codes.Internal, "delete event failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) DeleteEventBySerial(ctx context.Context, req *orlydbv1.DeleteEventBySerialRequest) (*orlydbv1.Empty, error) {
ser := orlydbv1.ProtoToUint40(&orlydbv1.Uint40{Value: req.Serial})
ev := orlydbv1.ProtoToEvent(req.Event)
if err := s.db.DeleteEventBySerial(ctx, ser, ev); err != nil {
return nil, status.Errorf(codes.Internal, "delete event by serial failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) DeleteExpired(ctx context.Context, req *orlydbv1.Empty) (*orlydbv1.Empty, error) {
s.db.DeleteExpired()
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) ProcessDelete(ctx context.Context, req *orlydbv1.ProcessDeleteRequest) (*orlydbv1.Empty, error) {
ev := orlydbv1.ProtoToEvent(req.Event)
if err := s.db.ProcessDelete(ev, req.Admins); err != nil {
return nil, status.Errorf(codes.Internal, "process delete failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) CheckForDeleted(ctx context.Context, req *orlydbv1.CheckForDeletedRequest) (*orlydbv1.Empty, error) {
ev := orlydbv1.ProtoToEvent(req.Event)
if err := s.db.CheckForDeleted(ev, req.Admins); err != nil {
return nil, status.Errorf(codes.Internal, "check for deleted failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
// === Import/Export ===
func (s *DatabaseService) Import(stream orlydbv1.DatabaseService_ImportServer) error {
pr, pw := io.Pipe()
// Goroutine to read from gRPC stream and write to pipe
go func() {
defer pw.Close()
for {
chunk, err := stream.Recv()
if err == io.EOF {
return
}
if err != nil {
log.E.F("import stream error: %v", err)
pw.CloseWithError(err)
return
}
if _, err := pw.Write(chunk.Data); chk.E(err) {
return
}
}
}()
// Import from pipe
s.db.Import(pr)
return stream.SendAndClose(&orlydbv1.ImportResponse{
EventsImported: 0, // TODO: Track count
EventsSkipped: 0,
})
}
func (s *DatabaseService) Export(req *orlydbv1.ExportRequest, stream orlydbv1.DatabaseService_ExportServer) error {
pr, pw := io.Pipe()
// Goroutine to export to pipe
go func() {
defer pw.Close()
s.db.Export(stream.Context(), pw, req.Pubkeys...)
}()
// Read from pipe and send to stream
buf := make([]byte, 64*1024) // 64KB chunks
for {
n, err := pr.Read(buf)
if err == io.EOF {
return nil
}
if err != nil {
return status.Errorf(codes.Internal, "export failed: %v", err)
}
if err := stream.Send(&orlydbv1.ExportChunk{Data: buf[:n]}); err != nil {
return err
}
}
}
func (s *DatabaseService) ImportEventsFromStrings(ctx context.Context, req *orlydbv1.ImportEventsFromStringsRequest) (*orlydbv1.ImportResponse, error) {
// Note: We can't pass policy manager over gRPC, so we pass nil
if err := s.db.ImportEventsFromStrings(ctx, req.EventJsons, nil); err != nil {
return nil, status.Errorf(codes.Internal, "import events from strings failed: %v", err)
}
return &orlydbv1.ImportResponse{
EventsImported: int64(len(req.EventJsons)),
}, nil
}
// === Relay Identity ===
func (s *DatabaseService) GetRelayIdentitySecret(ctx context.Context, req *orlydbv1.Empty) (*orlydbv1.GetRelayIdentitySecretResponse, error) {
secret, err := s.db.GetRelayIdentitySecret()
if err != nil {
return nil, status.Errorf(codes.Internal, "get relay identity secret failed: %v", err)
}
return &orlydbv1.GetRelayIdentitySecretResponse{SecretKey: secret}, nil
}
func (s *DatabaseService) SetRelayIdentitySecret(ctx context.Context, req *orlydbv1.SetRelayIdentitySecretRequest) (*orlydbv1.Empty, error) {
if err := s.db.SetRelayIdentitySecret(req.SecretKey); err != nil {
return nil, status.Errorf(codes.Internal, "set relay identity secret failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) GetOrCreateRelayIdentitySecret(ctx context.Context, req *orlydbv1.Empty) (*orlydbv1.GetRelayIdentitySecretResponse, error) {
secret, err := s.db.GetOrCreateRelayIdentitySecret()
if err != nil {
return nil, status.Errorf(codes.Internal, "get or create relay identity secret failed: %v", err)
}
return &orlydbv1.GetRelayIdentitySecretResponse{SecretKey: secret}, nil
}
// === Markers ===
func (s *DatabaseService) SetMarker(ctx context.Context, req *orlydbv1.SetMarkerRequest) (*orlydbv1.Empty, error) {
if err := s.db.SetMarker(req.Key, req.Value); err != nil {
return nil, status.Errorf(codes.Internal, "set marker failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) GetMarker(ctx context.Context, req *orlydbv1.GetMarkerRequest) (*orlydbv1.GetMarkerResponse, error) {
value, err := s.db.GetMarker(req.Key)
if err != nil {
return nil, status.Errorf(codes.Internal, "get marker failed: %v", err)
}
return &orlydbv1.GetMarkerResponse{
Value: value,
Found: value != nil,
}, nil
}
func (s *DatabaseService) HasMarker(ctx context.Context, req *orlydbv1.HasMarkerRequest) (*orlydbv1.HasMarkerResponse, error) {
exists := s.db.HasMarker(req.Key)
return &orlydbv1.HasMarkerResponse{Exists: exists}, nil
}
func (s *DatabaseService) DeleteMarker(ctx context.Context, req *orlydbv1.DeleteMarkerRequest) (*orlydbv1.Empty, error) {
if err := s.db.DeleteMarker(req.Key); err != nil {
return nil, status.Errorf(codes.Internal, "delete marker failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
// === Subscriptions ===
func (s *DatabaseService) GetSubscription(ctx context.Context, req *orlydbv1.GetSubscriptionRequest) (*orlydbv1.Subscription, error) {
sub, err := s.db.GetSubscription(req.Pubkey)
if err != nil {
return nil, status.Errorf(codes.Internal, "get subscription failed: %v", err)
}
return orlydbv1.SubscriptionToProto(sub, req.Pubkey), nil
}
func (s *DatabaseService) IsSubscriptionActive(ctx context.Context, req *orlydbv1.IsSubscriptionActiveRequest) (*orlydbv1.IsSubscriptionActiveResponse, error) {
active, err := s.db.IsSubscriptionActive(req.Pubkey)
if err != nil {
return nil, status.Errorf(codes.Internal, "is subscription active failed: %v", err)
}
return &orlydbv1.IsSubscriptionActiveResponse{Active: active}, nil
}
func (s *DatabaseService) ExtendSubscription(ctx context.Context, req *orlydbv1.ExtendSubscriptionRequest) (*orlydbv1.Empty, error) {
if err := s.db.ExtendSubscription(req.Pubkey, int(req.Days)); err != nil {
return nil, status.Errorf(codes.Internal, "extend subscription failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) RecordPayment(ctx context.Context, req *orlydbv1.RecordPaymentRequest) (*orlydbv1.Empty, error) {
if err := s.db.RecordPayment(req.Pubkey, req.Amount, req.Invoice, req.Preimage); err != nil {
return nil, status.Errorf(codes.Internal, "record payment failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) GetPaymentHistory(ctx context.Context, req *orlydbv1.GetPaymentHistoryRequest) (*orlydbv1.PaymentList, error) {
payments, err := s.db.GetPaymentHistory(req.Pubkey)
if err != nil {
return nil, status.Errorf(codes.Internal, "get payment history failed: %v", err)
}
return orlydbv1.PaymentListToProto(payments), nil
}
func (s *DatabaseService) ExtendBlossomSubscription(ctx context.Context, req *orlydbv1.ExtendBlossomSubscriptionRequest) (*orlydbv1.Empty, error) {
if err := s.db.ExtendBlossomSubscription(req.Pubkey, req.Tier, req.StorageMb, int(req.DaysExtended)); err != nil {
return nil, status.Errorf(codes.Internal, "extend blossom subscription failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) GetBlossomStorageQuota(ctx context.Context, req *orlydbv1.GetBlossomStorageQuotaRequest) (*orlydbv1.GetBlossomStorageQuotaResponse, error) {
quota, err := s.db.GetBlossomStorageQuota(req.Pubkey)
if err != nil {
return nil, status.Errorf(codes.Internal, "get blossom storage quota failed: %v", err)
}
return &orlydbv1.GetBlossomStorageQuotaResponse{QuotaMb: quota}, nil
}
func (s *DatabaseService) IsFirstTimeUser(ctx context.Context, req *orlydbv1.IsFirstTimeUserRequest) (*orlydbv1.IsFirstTimeUserResponse, error) {
firstTime, err := s.db.IsFirstTimeUser(req.Pubkey)
if err != nil {
return nil, status.Errorf(codes.Internal, "is first time user failed: %v", err)
}
return &orlydbv1.IsFirstTimeUserResponse{FirstTime: firstTime}, nil
}
// === NIP-43 ===
func (s *DatabaseService) AddNIP43Member(ctx context.Context, req *orlydbv1.AddNIP43MemberRequest) (*orlydbv1.Empty, error) {
if err := s.db.AddNIP43Member(req.Pubkey, req.InviteCode); err != nil {
return nil, status.Errorf(codes.Internal, "add NIP-43 member failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) RemoveNIP43Member(ctx context.Context, req *orlydbv1.RemoveNIP43MemberRequest) (*orlydbv1.Empty, error) {
if err := s.db.RemoveNIP43Member(req.Pubkey); err != nil {
return nil, status.Errorf(codes.Internal, "remove NIP-43 member failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) IsNIP43Member(ctx context.Context, req *orlydbv1.IsNIP43MemberRequest) (*orlydbv1.IsNIP43MemberResponse, error) {
isMember, err := s.db.IsNIP43Member(req.Pubkey)
if err != nil {
return nil, status.Errorf(codes.Internal, "is NIP-43 member failed: %v", err)
}
return &orlydbv1.IsNIP43MemberResponse{IsMember: isMember}, nil
}
func (s *DatabaseService) GetNIP43Membership(ctx context.Context, req *orlydbv1.GetNIP43MembershipRequest) (*orlydbv1.NIP43Membership, error) {
membership, err := s.db.GetNIP43Membership(req.Pubkey)
if err != nil {
return nil, status.Errorf(codes.Internal, "get NIP-43 membership failed: %v", err)
}
return orlydbv1.NIP43MembershipToProto(membership), nil
}
func (s *DatabaseService) GetAllNIP43Members(ctx context.Context, req *orlydbv1.Empty) (*orlydbv1.PubkeyList, error) {
members, err := s.db.GetAllNIP43Members()
if err != nil {
return nil, status.Errorf(codes.Internal, "get all NIP-43 members failed: %v", err)
}
return &orlydbv1.PubkeyList{Pubkeys: members}, nil
}
func (s *DatabaseService) StoreInviteCode(ctx context.Context, req *orlydbv1.StoreInviteCodeRequest) (*orlydbv1.Empty, error) {
expiresAt := orlydbv1.TimeFromUnix(req.ExpiresAt)
if err := s.db.StoreInviteCode(req.Code, expiresAt); err != nil {
return nil, status.Errorf(codes.Internal, "store invite code failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) ValidateInviteCode(ctx context.Context, req *orlydbv1.ValidateInviteCodeRequest) (*orlydbv1.ValidateInviteCodeResponse, error) {
valid, err := s.db.ValidateInviteCode(req.Code)
if err != nil {
return nil, status.Errorf(codes.Internal, "validate invite code failed: %v", err)
}
return &orlydbv1.ValidateInviteCodeResponse{Valid: valid}, nil
}
func (s *DatabaseService) DeleteInviteCode(ctx context.Context, req *orlydbv1.DeleteInviteCodeRequest) (*orlydbv1.Empty, error) {
if err := s.db.DeleteInviteCode(req.Code); err != nil {
return nil, status.Errorf(codes.Internal, "delete invite code failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) PublishNIP43MembershipEvent(ctx context.Context, req *orlydbv1.PublishNIP43MembershipEventRequest) (*orlydbv1.Empty, error) {
if err := s.db.PublishNIP43MembershipEvent(int(req.Kind), req.Pubkey); err != nil {
return nil, status.Errorf(codes.Internal, "publish NIP-43 membership event failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
// === Query Cache ===
func (s *DatabaseService) GetCachedJSON(ctx context.Context, req *orlydbv1.GetCachedJSONRequest) (*orlydbv1.GetCachedJSONResponse, error) {
f := orlydbv1.ProtoToFilter(req.Filter)
jsonItems, found := s.db.GetCachedJSON(f)
return &orlydbv1.GetCachedJSONResponse{
JsonItems: jsonItems,
Found: found,
}, nil
}
func (s *DatabaseService) CacheMarshaledJSON(ctx context.Context, req *orlydbv1.CacheMarshaledJSONRequest) (*orlydbv1.Empty, error) {
f := orlydbv1.ProtoToFilter(req.Filter)
s.db.CacheMarshaledJSON(f, req.JsonItems)
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) GetCachedEvents(ctx context.Context, req *orlydbv1.GetCachedEventsRequest) (*orlydbv1.GetCachedEventsResponse, error) {
f := orlydbv1.ProtoToFilter(req.Filter)
events, found := s.db.GetCachedEvents(f)
return &orlydbv1.GetCachedEventsResponse{
Events: orlydbv1.EventsToProto(events),
Found: found,
}, nil
}
func (s *DatabaseService) CacheEvents(ctx context.Context, req *orlydbv1.CacheEventsRequest) (*orlydbv1.Empty, error) {
f := orlydbv1.ProtoToFilter(req.Filter)
events := orlydbv1.ProtoToEvents(req.Events)
s.db.CacheEvents(f, events)
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) InvalidateQueryCache(ctx context.Context, req *orlydbv1.Empty) (*orlydbv1.Empty, error) {
s.db.InvalidateQueryCache()
return &orlydbv1.Empty{}, nil
}
// === Access Tracking ===
func (s *DatabaseService) RecordEventAccess(ctx context.Context, req *orlydbv1.RecordEventAccessRequest) (*orlydbv1.Empty, error) {
if err := s.db.RecordEventAccess(req.Serial, req.ConnectionId); err != nil {
return nil, status.Errorf(codes.Internal, "record event access failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) GetEventAccessInfo(ctx context.Context, req *orlydbv1.GetEventAccessInfoRequest) (*orlydbv1.GetEventAccessInfoResponse, error) {
lastAccess, accessCount, err := s.db.GetEventAccessInfo(req.Serial)
if err != nil {
return nil, status.Errorf(codes.Internal, "get event access info failed: %v", err)
}
return &orlydbv1.GetEventAccessInfoResponse{
LastAccess: lastAccess,
AccessCount: accessCount,
}, nil
}
func (s *DatabaseService) GetLeastAccessedEvents(ctx context.Context, req *orlydbv1.GetLeastAccessedEventsRequest) (*orlydbv1.SerialList, error) {
serials, err := s.db.GetLeastAccessedEvents(int(req.Limit), req.MinAgeSec)
if err != nil {
return nil, status.Errorf(codes.Internal, "get least accessed events failed: %v", err)
}
return &orlydbv1.SerialList{Serials: serials}, nil
}
// === Utility ===
func (s *DatabaseService) EventIdsBySerial(ctx context.Context, req *orlydbv1.EventIdsBySerialRequest) (*orlydbv1.EventIdsBySerialResponse, error) {
eventIds, err := s.db.EventIdsBySerial(req.Start, int(req.Count))
if err != nil {
return nil, status.Errorf(codes.Internal, "event ids by serial failed: %v", err)
}
return &orlydbv1.EventIdsBySerialResponse{EventIds: eventIds}, nil
}
func (s *DatabaseService) RunMigrations(ctx context.Context, req *orlydbv1.Empty) (*orlydbv1.Empty, error) {
s.db.RunMigrations()
return &orlydbv1.Empty{}, nil
}
// === Blob Storage (Blossom) ===
func (s *DatabaseService) SaveBlob(ctx context.Context, req *orlydbv1.SaveBlobRequest) (*orlydbv1.Empty, error) {
if err := s.db.SaveBlob(req.Sha256Hash, req.Data, req.Pubkey, req.MimeType, req.Extension); err != nil {
return nil, status.Errorf(codes.Internal, "save blob failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) GetBlob(ctx context.Context, req *orlydbv1.GetBlobRequest) (*orlydbv1.GetBlobResponse, error) {
data, metadata, err := s.db.GetBlob(req.Sha256Hash)
if err != nil {
// Return not found as a response, not an error
return &orlydbv1.GetBlobResponse{Found: false}, nil
}
return &orlydbv1.GetBlobResponse{
Found: true,
Data: data,
Metadata: orlydbv1.BlobMetadataToProto(metadata),
}, nil
}
func (s *DatabaseService) HasBlob(ctx context.Context, req *orlydbv1.HasBlobRequest) (*orlydbv1.HasBlobResponse, error) {
exists, err := s.db.HasBlob(req.Sha256Hash)
if err != nil {
return nil, status.Errorf(codes.Internal, "has blob failed: %v", err)
}
return &orlydbv1.HasBlobResponse{Exists: exists}, nil
}
func (s *DatabaseService) DeleteBlob(ctx context.Context, req *orlydbv1.DeleteBlobRequest) (*orlydbv1.Empty, error) {
if err := s.db.DeleteBlob(req.Sha256Hash, req.Pubkey); err != nil {
return nil, status.Errorf(codes.Internal, "delete blob failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) ListBlobs(ctx context.Context, req *orlydbv1.ListBlobsRequest) (*orlydbv1.ListBlobsResponse, error) {
descriptors, err := s.db.ListBlobs(req.Pubkey, req.Since, req.Until)
if err != nil {
return nil, status.Errorf(codes.Internal, "list blobs failed: %v", err)
}
return &orlydbv1.ListBlobsResponse{
Descriptors: orlydbv1.BlobDescriptorListToProto(descriptors),
}, nil
}
func (s *DatabaseService) GetBlobMetadata(ctx context.Context, req *orlydbv1.GetBlobMetadataRequest) (*orlydbv1.BlobMetadata, error) {
metadata, err := s.db.GetBlobMetadata(req.Sha256Hash)
if err != nil {
return nil, status.Errorf(codes.NotFound, "blob metadata not found: %v", err)
}
return orlydbv1.BlobMetadataToProto(metadata), nil
}
func (s *DatabaseService) GetTotalBlobStorageUsed(ctx context.Context, req *orlydbv1.GetTotalBlobStorageUsedRequest) (*orlydbv1.GetTotalBlobStorageUsedResponse, error) {
totalMB, err := s.db.GetTotalBlobStorageUsed(req.Pubkey)
if err != nil {
return nil, status.Errorf(codes.Internal, "get total blob storage used failed: %v", err)
}
return &orlydbv1.GetTotalBlobStorageUsedResponse{TotalMb: totalMB}, nil
}
func (s *DatabaseService) SaveBlobReport(ctx context.Context, req *orlydbv1.SaveBlobReportRequest) (*orlydbv1.Empty, error) {
if err := s.db.SaveBlobReport(req.Sha256Hash, req.ReportData); err != nil {
return nil, status.Errorf(codes.Internal, "save blob report failed: %v", err)
}
return &orlydbv1.Empty{}, nil
}
func (s *DatabaseService) ListAllBlobUserStats(ctx context.Context, req *orlydbv1.Empty) (*orlydbv1.ListAllBlobUserStatsResponse, error) {
stats, err := s.db.ListAllBlobUserStats()
if err != nil {
return nil, status.Errorf(codes.Internal, "list all blob user stats failed: %v", err)
}
return &orlydbv1.ListAllBlobUserStatsResponse{
Stats: orlydbv1.UserBlobStatsListToProto(stats),
}, nil
}
// === Helper Methods ===
// streamEvents is a helper to stream events in batches.
type eventStreamer interface {
Send(*orlydbv1.EventBatch) error
Context() context.Context
}
func (s *DatabaseService) streamEvents(events []*orlydbv1.Event, stream eventStreamer) error {
batchSize := s.cfg.StreamBatchSize
if batchSize == 0 {
batchSize = 100
}
for i := 0; i < len(events); i += batchSize {
end := i + batchSize
if end > len(events) {
end = len(events)
}
batch := &orlydbv1.EventBatch{
Events: events[i:end],
}
if err := stream.Send(batch); err != nil {
return err
}
}
return nil
}

250
pkg/proto/orlyacl/v1/acl.pb.go

@ -0,0 +1,250 @@ @@ -0,0 +1,250 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: orlyacl/v1/acl.proto
package orlyaclv1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
var File_orlyacl_v1_acl_proto protoreflect.FileDescriptor
const file_orlyacl_v1_acl_proto_rawDesc = "" +
"\n" +
"\x14orlyacl/v1/acl.proto\x12\n" +
"orlyacl.v1\x1a\x16orlyacl/v1/types.proto2\x91\x19\n" +
"\n" +
"ACLService\x12Q\n" +
"\x0eGetAccessLevel\x12\x1e.orlyacl.v1.AccessLevelRequest\x1a\x1f.orlyacl.v1.AccessLevelResponse\x12N\n" +
"\vCheckPolicy\x12\x1e.orlyacl.v1.PolicyCheckRequest\x1a\x1f.orlyacl.v1.PolicyCheckResponse\x12<\n" +
"\n" +
"GetACLInfo\x12\x11.orlyacl.v1.Empty\x1a\x1b.orlyacl.v1.ACLInfoResponse\x126\n" +
"\aGetMode\x12\x11.orlyacl.v1.Empty\x1a\x18.orlyacl.v1.ModeResponse\x125\n" +
"\x05Ready\x12\x11.orlyacl.v1.Empty\x1a\x19.orlyacl.v1.ReadyResponse\x12W\n" +
"\x10GetThrottleDelay\x12 .orlyacl.v1.ThrottleDelayRequest\x1a!.orlyacl.v1.ThrottleDelayResponse\x12<\n" +
"\tAddFollow\x12\x1c.orlyacl.v1.AddFollowRequest\x1a\x11.orlyacl.v1.Empty\x12L\n" +
"\x12GetFollowedPubkeys\x12\x11.orlyacl.v1.Empty\x1a#.orlyacl.v1.FollowedPubkeysResponse\x12D\n" +
"\x0eGetAdminRelays\x12\x11.orlyacl.v1.Empty\x1a\x1f.orlyacl.v1.AdminRelaysResponse\x12<\n" +
"\tBanPubkey\x12\x1c.orlyacl.v1.BanPubkeyRequest\x1a\x11.orlyacl.v1.Empty\x12;\n" +
"\vUnbanPubkey\x12\x19.orlyacl.v1.PubkeyRequest\x1a\x11.orlyacl.v1.Empty\x12M\n" +
"\x11ListBannedPubkeys\x12\x11.orlyacl.v1.Empty\x1a%.orlyacl.v1.ListBannedPubkeysResponse\x12@\n" +
"\vAllowPubkey\x12\x1e.orlyacl.v1.AllowPubkeyRequest\x1a\x11.orlyacl.v1.Empty\x12>\n" +
"\x0eDisallowPubkey\x12\x19.orlyacl.v1.PubkeyRequest\x1a\x11.orlyacl.v1.Empty\x12O\n" +
"\x12ListAllowedPubkeys\x12\x11.orlyacl.v1.Empty\x1a&.orlyacl.v1.ListAllowedPubkeysResponse\x12:\n" +
"\bBanEvent\x12\x1b.orlyacl.v1.BanEventRequest\x1a\x11.orlyacl.v1.Empty\x129\n" +
"\n" +
"UnbanEvent\x12\x18.orlyacl.v1.EventRequest\x1a\x11.orlyacl.v1.Empty\x12K\n" +
"\x10ListBannedEvents\x12\x11.orlyacl.v1.Empty\x1a$.orlyacl.v1.ListBannedEventsResponse\x12<\n" +
"\n" +
"AllowEvent\x12\x1b.orlyacl.v1.BanEventRequest\x1a\x11.orlyacl.v1.Empty\x12<\n" +
"\rDisallowEvent\x12\x18.orlyacl.v1.EventRequest\x1a\x11.orlyacl.v1.Empty\x12M\n" +
"\x11ListAllowedEvents\x12\x11.orlyacl.v1.Empty\x1a%.orlyacl.v1.ListAllowedEventsResponse\x128\n" +
"\aBlockIP\x12\x1a.orlyacl.v1.BlockIPRequest\x1a\x11.orlyacl.v1.Empty\x125\n" +
"\tUnblockIP\x12\x15.orlyacl.v1.IPRequest\x1a\x11.orlyacl.v1.Empty\x12G\n" +
"\x0eListBlockedIPs\x12\x11.orlyacl.v1.Empty\x1a\".orlyacl.v1.ListBlockedIPsResponse\x12<\n" +
"\tAllowKind\x12\x1c.orlyacl.v1.AllowKindRequest\x1a\x11.orlyacl.v1.Empty\x12:\n" +
"\fDisallowKind\x12\x17.orlyacl.v1.KindRequest\x1a\x11.orlyacl.v1.Empty\x12K\n" +
"\x10ListAllowedKinds\x12\x11.orlyacl.v1.Empty\x1a$.orlyacl.v1.ListAllowedKindsResponse\x12J\n" +
"\x10UpdatePeerAdmins\x12#.orlyacl.v1.UpdatePeerAdminsRequest\x1a\x11.orlyacl.v1.Empty\x12@\n" +
"\vTrustPubkey\x12\x1e.orlyacl.v1.TrustPubkeyRequest\x1a\x11.orlyacl.v1.Empty\x12=\n" +
"\rUntrustPubkey\x12\x19.orlyacl.v1.PubkeyRequest\x1a\x11.orlyacl.v1.Empty\x12O\n" +
"\x12ListTrustedPubkeys\x12\x11.orlyacl.v1.Empty\x1a&.orlyacl.v1.ListTrustedPubkeysResponse\x12H\n" +
"\x0fBlacklistPubkey\x12\".orlyacl.v1.BlacklistPubkeyRequest\x1a\x11.orlyacl.v1.Empty\x12A\n" +
"\x11UnblacklistPubkey\x12\x19.orlyacl.v1.PubkeyRequest\x1a\x11.orlyacl.v1.Empty\x12W\n" +
"\x16ListBlacklistedPubkeys\x12\x11.orlyacl.v1.Empty\x1a*.orlyacl.v1.ListBlacklistedPubkeysResponse\x12:\n" +
"\bMarkSpam\x12\x1b.orlyacl.v1.MarkSpamRequest\x1a\x11.orlyacl.v1.Empty\x129\n" +
"\n" +
"UnmarkSpam\x12\x18.orlyacl.v1.EventRequest\x1a\x11.orlyacl.v1.Empty\x12G\n" +
"\x0eListSpamEvents\x12\x11.orlyacl.v1.Empty\x1a\".orlyacl.v1.ListSpamEventsResponse\x12W\n" +
"\x0eRateLimitCheck\x12!.orlyacl.v1.RateLimitCheckRequest\x1a\".orlyacl.v1.RateLimitCheckResponse\x12G\n" +
"\x12ProcessConfigEvent\x12\x1e.orlyacl.v1.ConfigEventRequest\x1a\x11.orlyacl.v1.Empty\x12B\n" +
"\x11GetCuratingConfig\x12\x11.orlyacl.v1.Empty\x1a\x1a.orlyacl.v1.CuratingConfig\x12C\n" +
"\x14IsCuratingConfigured\x12\x11.orlyacl.v1.Empty\x1a\x18.orlyacl.v1.BoolResponse\x12a\n" +
"\x15ListUnclassifiedUsers\x12\x1d.orlyacl.v1.PaginationRequest\x1a).orlyacl.v1.ListUnclassifiedUsersResponse\x12`\n" +
"\x12GetEventsForPubkey\x12%.orlyacl.v1.GetEventsForPubkeyRequest\x1a#.orlyacl.v1.EventsForPubkeyResponse\x12b\n" +
"\x15DeleteEventsForPubkey\x12(.orlyacl.v1.DeleteEventsForPubkeyRequest\x1a\x1f.orlyacl.v1.DeleteCountResponse\x12C\n" +
"\x0eScanAllPubkeys\x12\x11.orlyacl.v1.Empty\x1a\x1e.orlyacl.v1.ScanResultResponseB.Z,next.orly.dev/pkg/proto/orlyacl/v1;orlyaclv1b\x06proto3"
var file_orlyacl_v1_acl_proto_goTypes = []any{
(*AccessLevelRequest)(nil), // 0: orlyacl.v1.AccessLevelRequest
(*PolicyCheckRequest)(nil), // 1: orlyacl.v1.PolicyCheckRequest
(*Empty)(nil), // 2: orlyacl.v1.Empty
(*ThrottleDelayRequest)(nil), // 3: orlyacl.v1.ThrottleDelayRequest
(*AddFollowRequest)(nil), // 4: orlyacl.v1.AddFollowRequest
(*BanPubkeyRequest)(nil), // 5: orlyacl.v1.BanPubkeyRequest
(*PubkeyRequest)(nil), // 6: orlyacl.v1.PubkeyRequest
(*AllowPubkeyRequest)(nil), // 7: orlyacl.v1.AllowPubkeyRequest
(*BanEventRequest)(nil), // 8: orlyacl.v1.BanEventRequest
(*EventRequest)(nil), // 9: orlyacl.v1.EventRequest
(*BlockIPRequest)(nil), // 10: orlyacl.v1.BlockIPRequest
(*IPRequest)(nil), // 11: orlyacl.v1.IPRequest
(*AllowKindRequest)(nil), // 12: orlyacl.v1.AllowKindRequest
(*KindRequest)(nil), // 13: orlyacl.v1.KindRequest
(*UpdatePeerAdminsRequest)(nil), // 14: orlyacl.v1.UpdatePeerAdminsRequest
(*TrustPubkeyRequest)(nil), // 15: orlyacl.v1.TrustPubkeyRequest
(*BlacklistPubkeyRequest)(nil), // 16: orlyacl.v1.BlacklistPubkeyRequest
(*MarkSpamRequest)(nil), // 17: orlyacl.v1.MarkSpamRequest
(*RateLimitCheckRequest)(nil), // 18: orlyacl.v1.RateLimitCheckRequest
(*ConfigEventRequest)(nil), // 19: orlyacl.v1.ConfigEventRequest
(*PaginationRequest)(nil), // 20: orlyacl.v1.PaginationRequest
(*GetEventsForPubkeyRequest)(nil), // 21: orlyacl.v1.GetEventsForPubkeyRequest
(*DeleteEventsForPubkeyRequest)(nil), // 22: orlyacl.v1.DeleteEventsForPubkeyRequest
(*AccessLevelResponse)(nil), // 23: orlyacl.v1.AccessLevelResponse
(*PolicyCheckResponse)(nil), // 24: orlyacl.v1.PolicyCheckResponse
(*ACLInfoResponse)(nil), // 25: orlyacl.v1.ACLInfoResponse
(*ModeResponse)(nil), // 26: orlyacl.v1.ModeResponse
(*ReadyResponse)(nil), // 27: orlyacl.v1.ReadyResponse
(*ThrottleDelayResponse)(nil), // 28: orlyacl.v1.ThrottleDelayResponse
(*FollowedPubkeysResponse)(nil), // 29: orlyacl.v1.FollowedPubkeysResponse
(*AdminRelaysResponse)(nil), // 30: orlyacl.v1.AdminRelaysResponse
(*ListBannedPubkeysResponse)(nil), // 31: orlyacl.v1.ListBannedPubkeysResponse
(*ListAllowedPubkeysResponse)(nil), // 32: orlyacl.v1.ListAllowedPubkeysResponse
(*ListBannedEventsResponse)(nil), // 33: orlyacl.v1.ListBannedEventsResponse
(*ListAllowedEventsResponse)(nil), // 34: orlyacl.v1.ListAllowedEventsResponse
(*ListBlockedIPsResponse)(nil), // 35: orlyacl.v1.ListBlockedIPsResponse
(*ListAllowedKindsResponse)(nil), // 36: orlyacl.v1.ListAllowedKindsResponse
(*ListTrustedPubkeysResponse)(nil), // 37: orlyacl.v1.ListTrustedPubkeysResponse
(*ListBlacklistedPubkeysResponse)(nil), // 38: orlyacl.v1.ListBlacklistedPubkeysResponse
(*ListSpamEventsResponse)(nil), // 39: orlyacl.v1.ListSpamEventsResponse
(*RateLimitCheckResponse)(nil), // 40: orlyacl.v1.RateLimitCheckResponse
(*CuratingConfig)(nil), // 41: orlyacl.v1.CuratingConfig
(*BoolResponse)(nil), // 42: orlyacl.v1.BoolResponse
(*ListUnclassifiedUsersResponse)(nil), // 43: orlyacl.v1.ListUnclassifiedUsersResponse
(*EventsForPubkeyResponse)(nil), // 44: orlyacl.v1.EventsForPubkeyResponse
(*DeleteCountResponse)(nil), // 45: orlyacl.v1.DeleteCountResponse
(*ScanResultResponse)(nil), // 46: orlyacl.v1.ScanResultResponse
}
var file_orlyacl_v1_acl_proto_depIdxs = []int32{
0, // 0: orlyacl.v1.ACLService.GetAccessLevel:input_type -> orlyacl.v1.AccessLevelRequest
1, // 1: orlyacl.v1.ACLService.CheckPolicy:input_type -> orlyacl.v1.PolicyCheckRequest
2, // 2: orlyacl.v1.ACLService.GetACLInfo:input_type -> orlyacl.v1.Empty
2, // 3: orlyacl.v1.ACLService.GetMode:input_type -> orlyacl.v1.Empty
2, // 4: orlyacl.v1.ACLService.Ready:input_type -> orlyacl.v1.Empty
3, // 5: orlyacl.v1.ACLService.GetThrottleDelay:input_type -> orlyacl.v1.ThrottleDelayRequest
4, // 6: orlyacl.v1.ACLService.AddFollow:input_type -> orlyacl.v1.AddFollowRequest
2, // 7: orlyacl.v1.ACLService.GetFollowedPubkeys:input_type -> orlyacl.v1.Empty
2, // 8: orlyacl.v1.ACLService.GetAdminRelays:input_type -> orlyacl.v1.Empty
5, // 9: orlyacl.v1.ACLService.BanPubkey:input_type -> orlyacl.v1.BanPubkeyRequest
6, // 10: orlyacl.v1.ACLService.UnbanPubkey:input_type -> orlyacl.v1.PubkeyRequest
2, // 11: orlyacl.v1.ACLService.ListBannedPubkeys:input_type -> orlyacl.v1.Empty
7, // 12: orlyacl.v1.ACLService.AllowPubkey:input_type -> orlyacl.v1.AllowPubkeyRequest
6, // 13: orlyacl.v1.ACLService.DisallowPubkey:input_type -> orlyacl.v1.PubkeyRequest
2, // 14: orlyacl.v1.ACLService.ListAllowedPubkeys:input_type -> orlyacl.v1.Empty
8, // 15: orlyacl.v1.ACLService.BanEvent:input_type -> orlyacl.v1.BanEventRequest
9, // 16: orlyacl.v1.ACLService.UnbanEvent:input_type -> orlyacl.v1.EventRequest
2, // 17: orlyacl.v1.ACLService.ListBannedEvents:input_type -> orlyacl.v1.Empty
8, // 18: orlyacl.v1.ACLService.AllowEvent:input_type -> orlyacl.v1.BanEventRequest
9, // 19: orlyacl.v1.ACLService.DisallowEvent:input_type -> orlyacl.v1.EventRequest
2, // 20: orlyacl.v1.ACLService.ListAllowedEvents:input_type -> orlyacl.v1.Empty
10, // 21: orlyacl.v1.ACLService.BlockIP:input_type -> orlyacl.v1.BlockIPRequest
11, // 22: orlyacl.v1.ACLService.UnblockIP:input_type -> orlyacl.v1.IPRequest
2, // 23: orlyacl.v1.ACLService.ListBlockedIPs:input_type -> orlyacl.v1.Empty
12, // 24: orlyacl.v1.ACLService.AllowKind:input_type -> orlyacl.v1.AllowKindRequest
13, // 25: orlyacl.v1.ACLService.DisallowKind:input_type -> orlyacl.v1.KindRequest
2, // 26: orlyacl.v1.ACLService.ListAllowedKinds:input_type -> orlyacl.v1.Empty
14, // 27: orlyacl.v1.ACLService.UpdatePeerAdmins:input_type -> orlyacl.v1.UpdatePeerAdminsRequest
15, // 28: orlyacl.v1.ACLService.TrustPubkey:input_type -> orlyacl.v1.TrustPubkeyRequest
6, // 29: orlyacl.v1.ACLService.UntrustPubkey:input_type -> orlyacl.v1.PubkeyRequest
2, // 30: orlyacl.v1.ACLService.ListTrustedPubkeys:input_type -> orlyacl.v1.Empty
16, // 31: orlyacl.v1.ACLService.BlacklistPubkey:input_type -> orlyacl.v1.BlacklistPubkeyRequest
6, // 32: orlyacl.v1.ACLService.UnblacklistPubkey:input_type -> orlyacl.v1.PubkeyRequest
2, // 33: orlyacl.v1.ACLService.ListBlacklistedPubkeys:input_type -> orlyacl.v1.Empty
17, // 34: orlyacl.v1.ACLService.MarkSpam:input_type -> orlyacl.v1.MarkSpamRequest
9, // 35: orlyacl.v1.ACLService.UnmarkSpam:input_type -> orlyacl.v1.EventRequest
2, // 36: orlyacl.v1.ACLService.ListSpamEvents:input_type -> orlyacl.v1.Empty
18, // 37: orlyacl.v1.ACLService.RateLimitCheck:input_type -> orlyacl.v1.RateLimitCheckRequest
19, // 38: orlyacl.v1.ACLService.ProcessConfigEvent:input_type -> orlyacl.v1.ConfigEventRequest
2, // 39: orlyacl.v1.ACLService.GetCuratingConfig:input_type -> orlyacl.v1.Empty
2, // 40: orlyacl.v1.ACLService.IsCuratingConfigured:input_type -> orlyacl.v1.Empty
20, // 41: orlyacl.v1.ACLService.ListUnclassifiedUsers:input_type -> orlyacl.v1.PaginationRequest
21, // 42: orlyacl.v1.ACLService.GetEventsForPubkey:input_type -> orlyacl.v1.GetEventsForPubkeyRequest
22, // 43: orlyacl.v1.ACLService.DeleteEventsForPubkey:input_type -> orlyacl.v1.DeleteEventsForPubkeyRequest
2, // 44: orlyacl.v1.ACLService.ScanAllPubkeys:input_type -> orlyacl.v1.Empty
23, // 45: orlyacl.v1.ACLService.GetAccessLevel:output_type -> orlyacl.v1.AccessLevelResponse
24, // 46: orlyacl.v1.ACLService.CheckPolicy:output_type -> orlyacl.v1.PolicyCheckResponse
25, // 47: orlyacl.v1.ACLService.GetACLInfo:output_type -> orlyacl.v1.ACLInfoResponse
26, // 48: orlyacl.v1.ACLService.GetMode:output_type -> orlyacl.v1.ModeResponse
27, // 49: orlyacl.v1.ACLService.Ready:output_type -> orlyacl.v1.ReadyResponse
28, // 50: orlyacl.v1.ACLService.GetThrottleDelay:output_type -> orlyacl.v1.ThrottleDelayResponse
2, // 51: orlyacl.v1.ACLService.AddFollow:output_type -> orlyacl.v1.Empty
29, // 52: orlyacl.v1.ACLService.GetFollowedPubkeys:output_type -> orlyacl.v1.FollowedPubkeysResponse
30, // 53: orlyacl.v1.ACLService.GetAdminRelays:output_type -> orlyacl.v1.AdminRelaysResponse
2, // 54: orlyacl.v1.ACLService.BanPubkey:output_type -> orlyacl.v1.Empty
2, // 55: orlyacl.v1.ACLService.UnbanPubkey:output_type -> orlyacl.v1.Empty
31, // 56: orlyacl.v1.ACLService.ListBannedPubkeys:output_type -> orlyacl.v1.ListBannedPubkeysResponse
2, // 57: orlyacl.v1.ACLService.AllowPubkey:output_type -> orlyacl.v1.Empty
2, // 58: orlyacl.v1.ACLService.DisallowPubkey:output_type -> orlyacl.v1.Empty
32, // 59: orlyacl.v1.ACLService.ListAllowedPubkeys:output_type -> orlyacl.v1.ListAllowedPubkeysResponse
2, // 60: orlyacl.v1.ACLService.BanEvent:output_type -> orlyacl.v1.Empty
2, // 61: orlyacl.v1.ACLService.UnbanEvent:output_type -> orlyacl.v1.Empty
33, // 62: orlyacl.v1.ACLService.ListBannedEvents:output_type -> orlyacl.v1.ListBannedEventsResponse
2, // 63: orlyacl.v1.ACLService.AllowEvent:output_type -> orlyacl.v1.Empty
2, // 64: orlyacl.v1.ACLService.DisallowEvent:output_type -> orlyacl.v1.Empty
34, // 65: orlyacl.v1.ACLService.ListAllowedEvents:output_type -> orlyacl.v1.ListAllowedEventsResponse
2, // 66: orlyacl.v1.ACLService.BlockIP:output_type -> orlyacl.v1.Empty
2, // 67: orlyacl.v1.ACLService.UnblockIP:output_type -> orlyacl.v1.Empty
35, // 68: orlyacl.v1.ACLService.ListBlockedIPs:output_type -> orlyacl.v1.ListBlockedIPsResponse
2, // 69: orlyacl.v1.ACLService.AllowKind:output_type -> orlyacl.v1.Empty
2, // 70: orlyacl.v1.ACLService.DisallowKind:output_type -> orlyacl.v1.Empty
36, // 71: orlyacl.v1.ACLService.ListAllowedKinds:output_type -> orlyacl.v1.ListAllowedKindsResponse
2, // 72: orlyacl.v1.ACLService.UpdatePeerAdmins:output_type -> orlyacl.v1.Empty
2, // 73: orlyacl.v1.ACLService.TrustPubkey:output_type -> orlyacl.v1.Empty
2, // 74: orlyacl.v1.ACLService.UntrustPubkey:output_type -> orlyacl.v1.Empty
37, // 75: orlyacl.v1.ACLService.ListTrustedPubkeys:output_type -> orlyacl.v1.ListTrustedPubkeysResponse
2, // 76: orlyacl.v1.ACLService.BlacklistPubkey:output_type -> orlyacl.v1.Empty
2, // 77: orlyacl.v1.ACLService.UnblacklistPubkey:output_type -> orlyacl.v1.Empty
38, // 78: orlyacl.v1.ACLService.ListBlacklistedPubkeys:output_type -> orlyacl.v1.ListBlacklistedPubkeysResponse
2, // 79: orlyacl.v1.ACLService.MarkSpam:output_type -> orlyacl.v1.Empty
2, // 80: orlyacl.v1.ACLService.UnmarkSpam:output_type -> orlyacl.v1.Empty
39, // 81: orlyacl.v1.ACLService.ListSpamEvents:output_type -> orlyacl.v1.ListSpamEventsResponse
40, // 82: orlyacl.v1.ACLService.RateLimitCheck:output_type -> orlyacl.v1.RateLimitCheckResponse
2, // 83: orlyacl.v1.ACLService.ProcessConfigEvent:output_type -> orlyacl.v1.Empty
41, // 84: orlyacl.v1.ACLService.GetCuratingConfig:output_type -> orlyacl.v1.CuratingConfig
42, // 85: orlyacl.v1.ACLService.IsCuratingConfigured:output_type -> orlyacl.v1.BoolResponse
43, // 86: orlyacl.v1.ACLService.ListUnclassifiedUsers:output_type -> orlyacl.v1.ListUnclassifiedUsersResponse
44, // 87: orlyacl.v1.ACLService.GetEventsForPubkey:output_type -> orlyacl.v1.EventsForPubkeyResponse
45, // 88: orlyacl.v1.ACLService.DeleteEventsForPubkey:output_type -> orlyacl.v1.DeleteCountResponse
46, // 89: orlyacl.v1.ACLService.ScanAllPubkeys:output_type -> orlyacl.v1.ScanResultResponse
45, // [45:90] is the sub-list for method output_type
0, // [0:45] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_orlyacl_v1_acl_proto_init() }
func file_orlyacl_v1_acl_proto_init() {
if File_orlyacl_v1_acl_proto != nil {
return
}
file_orlyacl_v1_types_proto_init()
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_orlyacl_v1_acl_proto_rawDesc), len(file_orlyacl_v1_acl_proto_rawDesc)),
NumEnums: 0,
NumMessages: 0,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_orlyacl_v1_acl_proto_goTypes,
DependencyIndexes: file_orlyacl_v1_acl_proto_depIdxs,
}.Build()
File_orlyacl_v1_acl_proto = out.File
file_orlyacl_v1_acl_proto_goTypes = nil
file_orlyacl_v1_acl_proto_depIdxs = nil
}

1887
pkg/proto/orlyacl/v1/acl_grpc.pb.go

File diff suppressed because it is too large Load Diff

3234
pkg/proto/orlyacl/v1/types.pb.go

File diff suppressed because it is too large Load Diff

2
pkg/version/version

@ -1 +1 @@ @@ -1 +1 @@
v0.53.1
v0.54.0

151
proto/orlyacl/v1/acl.proto

@ -0,0 +1,151 @@ @@ -0,0 +1,151 @@
syntax = "proto3";
package orlyacl.v1;
option go_package = "next.orly.dev/pkg/proto/orlyacl/v1;orlyaclv1";
import "orlyacl/v1/types.proto";
// ACLService provides access control operations for the Nostr relay
service ACLService {
// Core ACL interface methods
// GetAccessLevel returns the access level for a pubkey from an IP address
rpc GetAccessLevel(AccessLevelRequest) returns (AccessLevelResponse);
// CheckPolicy checks if an event passes policy checks
rpc CheckPolicy(PolicyCheckRequest) returns (PolicyCheckResponse);
// GetACLInfo returns information about the active ACL mode
rpc GetACLInfo(Empty) returns (ACLInfoResponse);
// GetMode returns the current ACL mode (none/follows/managed/curating)
rpc GetMode(Empty) returns (ModeResponse);
// Ready checks if the ACL service is ready
rpc Ready(Empty) returns (ReadyResponse);
// Follows ACL methods
// GetThrottleDelay returns the progressive throttle delay for a pubkey
rpc GetThrottleDelay(ThrottleDelayRequest) returns (ThrottleDelayResponse);
// AddFollow adds a pubkey to the followed list
rpc AddFollow(AddFollowRequest) returns (Empty);
// GetFollowedPubkeys returns all followed pubkeys
rpc GetFollowedPubkeys(Empty) returns (FollowedPubkeysResponse);
// GetAdminRelays returns the relay URLs from admin kind 10002 events
rpc GetAdminRelays(Empty) returns (AdminRelaysResponse);
// Managed ACL methods
// BanPubkey adds a pubkey to the ban list
rpc BanPubkey(BanPubkeyRequest) returns (Empty);
// UnbanPubkey removes a pubkey from the ban list
rpc UnbanPubkey(PubkeyRequest) returns (Empty);
// ListBannedPubkeys returns all banned pubkeys
rpc ListBannedPubkeys(Empty) returns (ListBannedPubkeysResponse);
// AllowPubkey adds a pubkey to the allow list
rpc AllowPubkey(AllowPubkeyRequest) returns (Empty);
// DisallowPubkey removes a pubkey from the allow list
rpc DisallowPubkey(PubkeyRequest) returns (Empty);
// ListAllowedPubkeys returns all explicitly allowed pubkeys
rpc ListAllowedPubkeys(Empty) returns (ListAllowedPubkeysResponse);
// BanEvent adds an event to the ban list
rpc BanEvent(BanEventRequest) returns (Empty);
// UnbanEvent removes an event from the ban list
rpc UnbanEvent(EventRequest) returns (Empty);
// ListBannedEvents returns all banned events
rpc ListBannedEvents(Empty) returns (ListBannedEventsResponse);
// AllowEvent adds an event to the allow list
rpc AllowEvent(BanEventRequest) returns (Empty);
// DisallowEvent removes an event from the allow list
rpc DisallowEvent(EventRequest) returns (Empty);
// ListAllowedEvents returns all explicitly allowed events
rpc ListAllowedEvents(Empty) returns (ListAllowedEventsResponse);
// BlockIP adds an IP to the block list
rpc BlockIP(BlockIPRequest) returns (Empty);
// UnblockIP removes an IP from the block list
rpc UnblockIP(IPRequest) returns (Empty);
// ListBlockedIPs returns all blocked IPs
rpc ListBlockedIPs(Empty) returns (ListBlockedIPsResponse);
// AllowKind adds a kind to the allow list
rpc AllowKind(AllowKindRequest) returns (Empty);
// DisallowKind removes a kind from the allow list
rpc DisallowKind(KindRequest) returns (Empty);
// ListAllowedKinds returns all allowed kinds
rpc ListAllowedKinds(Empty) returns (ListAllowedKindsResponse);
// UpdatePeerAdmins updates the peer relay identity pubkeys
rpc UpdatePeerAdmins(UpdatePeerAdminsRequest) returns (Empty);
// Curating ACL methods
// TrustPubkey adds a pubkey to the trusted list
rpc TrustPubkey(TrustPubkeyRequest) returns (Empty);
// UntrustPubkey removes a pubkey from the trusted list
rpc UntrustPubkey(PubkeyRequest) returns (Empty);
// ListTrustedPubkeys returns all trusted pubkeys
rpc ListTrustedPubkeys(Empty) returns (ListTrustedPubkeysResponse);
// BlacklistPubkey adds a pubkey to the blacklist
rpc BlacklistPubkey(BlacklistPubkeyRequest) returns (Empty);
// UnblacklistPubkey removes a pubkey from the blacklist
rpc UnblacklistPubkey(PubkeyRequest) returns (Empty);
// ListBlacklistedPubkeys returns all blacklisted pubkeys
rpc ListBlacklistedPubkeys(Empty) returns (ListBlacklistedPubkeysResponse);
// MarkSpam marks an event as spam
rpc MarkSpam(MarkSpamRequest) returns (Empty);
// UnmarkSpam removes the spam flag from an event
rpc UnmarkSpam(EventRequest) returns (Empty);
// ListSpamEvents returns all spam-flagged events
rpc ListSpamEvents(Empty) returns (ListSpamEventsResponse);
// RateLimitCheck checks if a pubkey/IP can publish (rate limiting)
rpc RateLimitCheck(RateLimitCheckRequest) returns (RateLimitCheckResponse);
// ProcessConfigEvent processes a curating config event (kind 30078)
rpc ProcessConfigEvent(ConfigEventRequest) returns (Empty);
// GetCuratingConfig returns the current curating configuration
rpc GetCuratingConfig(Empty) returns (CuratingConfig);
// IsCuratingConfigured checks if curating mode is configured
rpc IsCuratingConfigured(Empty) returns (BoolResponse);
// ListUnclassifiedUsers returns users who are not trusted or blacklisted
rpc ListUnclassifiedUsers(PaginationRequest) returns (ListUnclassifiedUsersResponse);
// GetEventsForPubkey returns events for a specific pubkey
rpc GetEventsForPubkey(GetEventsForPubkeyRequest) returns (EventsForPubkeyResponse);
// DeleteEventsForPubkey deletes all events for a pubkey
rpc DeleteEventsForPubkey(DeleteEventsForPubkeyRequest) returns (DeleteCountResponse);
// ScanAllPubkeys scans and indexes all pubkeys in the database
rpc ScanAllPubkeys(Empty) returns (ScanResultResponse);
}

292
proto/orlyacl/v1/types.proto

@ -0,0 +1,292 @@ @@ -0,0 +1,292 @@
syntax = "proto3";
package orlyacl.v1;
option go_package = "next.orly.dev/pkg/proto/orlyacl/v1;orlyaclv1";
import "orlydb/v1/types.proto";
// Empty is used for requests/responses with no data
message Empty {}
// Core ACL Messages
message AccessLevelRequest {
bytes pubkey = 1;
string address = 2;
}
message AccessLevelResponse {
string level = 1; // none/read/write/admin/owner/banned/blocked
}
message ACLInfoResponse {
string name = 1;
string description = 2;
string documentation = 3;
}
message ModeResponse {
string mode = 1; // none/follows/managed/curating
}
message ReadyResponse {
bool ready = 1;
}
message BoolResponse {
bool value = 1;
}
message PolicyCheckRequest {
orlydb.v1.Event event = 1;
}
message PolicyCheckResponse {
bool allowed = 1;
string error = 2;
}
// Follows ACL Messages
message ThrottleDelayRequest {
bytes pubkey = 1;
string ip = 2;
}
message ThrottleDelayResponse {
int64 delay_ms = 1;
}
message AddFollowRequest {
bytes pubkey = 1;
}
message FollowedPubkeysResponse {
repeated bytes pubkeys = 1;
}
message AdminRelaysResponse {
repeated string urls = 1;
}
// Managed ACL Messages
message PubkeyRequest {
string pubkey = 1;
}
message IPRequest {
string ip = 1;
}
message BanPubkeyRequest {
string pubkey = 1;
string reason = 2;
}
message AllowPubkeyRequest {
string pubkey = 1;
string reason = 2;
}
message BannedPubkey {
string pubkey = 1;
string reason = 2;
int64 added = 3;
}
message AllowedPubkey {
string pubkey = 1;
string reason = 2;
int64 added = 3;
}
message BanEventRequest {
string event_id = 1;
string reason = 2;
}
message EventRequest {
string event_id = 1;
}
message BannedEvent {
string event_id = 1;
string reason = 2;
int64 added = 3;
}
message AllowedEvent {
string event_id = 1;
string reason = 2;
int64 added = 3;
}
message BlockIPRequest {
string ip = 1;
string reason = 2;
}
message BlockedIP {
string ip = 1;
string reason = 2;
int64 added = 3;
}
message AllowKindRequest {
int32 kind = 1;
}
message KindRequest {
int32 kind = 1;
}
message UpdatePeerAdminsRequest {
repeated bytes peer_pubkeys = 1;
}
message ListBannedPubkeysResponse {
repeated BannedPubkey pubkeys = 1;
}
message ListAllowedPubkeysResponse {
repeated AllowedPubkey pubkeys = 1;
}
message ListBannedEventsResponse {
repeated BannedEvent events = 1;
}
message ListAllowedEventsResponse {
repeated AllowedEvent events = 1;
}
message ListBlockedIPsResponse {
repeated BlockedIP ips = 1;
}
message ListAllowedKindsResponse {
repeated int32 kinds = 1;
}
// Curating ACL Messages
message TrustPubkeyRequest {
string pubkey = 1;
string note = 2;
}
message BlacklistPubkeyRequest {
string pubkey = 1;
string reason = 2;
}
message TrustedPubkey {
string pubkey = 1;
string note = 2;
int64 added = 3;
}
message BlacklistedPubkey {
string pubkey = 1;
string reason = 2;
int64 added = 3;
}
message MarkSpamRequest {
string event_id = 1;
string pubkey = 2;
string reason = 3;
}
message SpamEvent {
string event_id = 1;
string pubkey = 2;
string reason = 3;
int64 added = 4;
}
message RateLimitCheckRequest {
string pubkey = 1;
string ip = 2;
}
message RateLimitCheckResponse {
bool allowed = 1;
string message = 2;
}
message ConfigEventRequest {
orlydb.v1.Event event = 1;
}
message CuratingConfig {
string config_event_id = 1;
string config_pubkey = 2;
int64 configured_at = 3;
int32 daily_limit = 4;
int32 ip_daily_limit = 5;
int32 first_ban_hours = 6;
int32 second_ban_hours = 7;
repeated string kind_categories = 8;
repeated string allowed_ranges = 9;
repeated int32 allowed_kinds = 10;
}
message UnclassifiedUser {
string pubkey = 1;
int32 event_count = 2;
string first_seen = 3;
}
message PaginationRequest {
int32 limit = 1;
int32 offset = 2;
}
message ListTrustedPubkeysResponse {
repeated TrustedPubkey pubkeys = 1;
}
message ListBlacklistedPubkeysResponse {
repeated BlacklistedPubkey pubkeys = 1;
}
message ListSpamEventsResponse {
repeated SpamEvent events = 1;
}
message ListUnclassifiedUsersResponse {
repeated UnclassifiedUser users = 1;
int32 total = 2;
}
message GetEventsForPubkeyRequest {
string pubkey = 1;
int32 limit = 2;
int32 offset = 3;
}
message EventSummary {
string id = 1;
uint32 kind = 2;
bytes content = 3;
int64 created_at = 4;
}
message EventsForPubkeyResponse {
repeated EventSummary events = 1;
int32 total = 2;
}
message DeleteEventsForPubkeyRequest {
string pubkey = 1;
}
message DeleteCountResponse {
int32 count = 1;
}
message ScanResultResponse {
int32 total_pubkeys = 1;
int32 total_events = 2;
}
Loading…
Cancel
Save