Browse Source
- 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
28 changed files with 10294 additions and 22 deletions
@ -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"
|
||||
@ -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, ",") |
||||
} |
||||
@ -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, ",") |
||||
} |
||||
@ -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, ",") |
||||
} |
||||
@ -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, ",") |
||||
} |
||||
@ -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) |
||||
} |
||||
} |
||||
@ -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 |
||||
@ -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 |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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 |
||||
@ -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 |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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() |
||||
} |
||||
@ -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 |
||||
@ -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 |
||||
} |
||||
@ -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() |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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 |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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); |
||||
} |
||||
@ -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…
Reference in new issue