8 Commits

5 changed files with 293 additions and 9 deletions

View File

@@ -12,6 +12,8 @@ services:
RELAY_NAME: "LUMINA Relay"
RELAY_DESCRIPTION: "LUMINA Nostr Relay"
POSTGRES_URL: "postgres://postgres:postgres@postgres/postgres?sslmode=disable"
RELAY_ADMINS: "480ec1a7516406090dc042ddf67780ef30f26f3a864e83b417c053a5a611c838"
RELAY_SERVICE_URL: "https://relay.lumina.rocks"
restart: unless-stopped
scraper:
depends_on:
@@ -31,8 +33,8 @@ services:
POSTGRES_DB: postgres
volumes:
- ./postgres:/var/lib/postgresql/data
ports:
- 5432:5432
# ports:
# - 5432:5432
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s

194
relay/database.go Normal file
View File

@@ -0,0 +1,194 @@
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
// DBManager handles the normal PostgreSQL connection for non-event data
type DBManager struct {
db *sql.DB
}
// NewDBManager creates a new database manager with the given database URL.
// It establishes a connection, verifies connectivity, and initializes required tables.
func NewDBManager(databaseURL string) (*DBManager, error) {
db, err := sql.Open("postgres", databaseURL)
if err != nil {
return nil, fmt.Errorf("failed to open database connection: %w", err)
}
if err := db.Ping(); err != nil {
db.Close()
return nil, fmt.Errorf("failed to ping database: %w", err)
}
manager := &DBManager{db: db}
if err := manager.initTables(); err != nil {
db.Close()
return nil, fmt.Errorf("failed to initialize database tables: %w", err)
}
return manager, nil
}
// initTables creates the necessary tables for the application.
// This method is called automatically during DBManager initialization.
func (dbm *DBManager) initTables() error {
query := `
CREATE TABLE IF NOT EXISTS banned_pubkeys (
pubkey VARCHAR(64) PRIMARY KEY,
reason TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
if _, err := dbm.db.Exec(query); err != nil {
return fmt.Errorf("failed to create banned_pubkeys table: %w", err)
}
return nil
}
// AddBannedPubkey adds a pubkey to the banned list with an optional reason.
// If the pubkey already exists, the operation is ignored (no error returned).
func (dbm *DBManager) AddBannedPubkey(pubkey, reason string) error {
if pubkey == "" {
return fmt.Errorf("pubkey cannot be empty")
}
query := `INSERT INTO banned_pubkeys (pubkey, reason) VALUES ($1, $2) ON CONFLICT (pubkey) DO NOTHING`
if _, err := dbm.db.Exec(query, pubkey, reason); err != nil {
return fmt.Errorf("failed to add banned pubkey %s: %w", pubkey, err)
}
return nil
}
// RemoveBannedPubkey removes a pubkey from the banned list.
// Returns an error if the pubkey is not found in the banned list.
func (dbm *DBManager) RemoveBannedPubkey(pubkey string) error {
if pubkey == "" {
return fmt.Errorf("pubkey cannot be empty")
}
query := `DELETE FROM banned_pubkeys WHERE pubkey = $1`
result, err := dbm.db.Exec(query, pubkey)
if err != nil {
return fmt.Errorf("failed to remove banned pubkey %s: %w", pubkey, err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected for pubkey %s: %w", pubkey, err)
}
if rowsAffected == 0 {
return fmt.Errorf("pubkey %s not found in banned list", pubkey)
}
return nil
}
// IsBannedPubkey checks if a pubkey is in the banned list.
// Returns true if the pubkey is banned, false otherwise.
func (dbm *DBManager) IsBannedPubkey(pubkey string) (bool, error) {
if pubkey == "" {
return false, nil
}
var exists bool
query := `SELECT EXISTS(SELECT 1 FROM banned_pubkeys WHERE pubkey = $1)`
if err := dbm.db.QueryRow(query, pubkey).Scan(&exists); err != nil {
return false, fmt.Errorf("failed to check if pubkey %s is banned: %w", pubkey, err)
}
return exists, nil
}
// GetBannedPubkeysWithReasons returns all banned pubkeys with their reasons ordered by creation time.
// Returns an empty slice if no pubkeys are found.
func (dbm *DBManager) GetBannedPubkeysWithReasons() ([]struct {
Pubkey string
Reason string
}, error) {
query := `SELECT pubkey, reason FROM banned_pubkeys ORDER BY created_at`
rows, err := dbm.db.Query(query)
if err != nil {
return nil, fmt.Errorf("failed to query banned pubkeys with reasons: %w", err)
}
defer rows.Close()
var results []struct {
Pubkey string
Reason string
}
for rows.Next() {
var result struct {
Pubkey string
Reason string
}
if err := rows.Scan(&result.Pubkey, &result.Reason); err != nil {
return nil, fmt.Errorf("failed to scan pubkey and reason row: %w", err)
}
results = append(results, result)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error occurred while iterating over pubkey and reason rows: %w", err)
}
return results, nil
}
// GetBannedPubkeys returns all banned pubkeys ordered by creation time.
// Returns an empty slice if no pubkeys are found.
func (dbm *DBManager) GetBannedPubkeys() ([]string, error) {
query := `SELECT pubkey FROM banned_pubkeys ORDER BY created_at`
rows, err := dbm.db.Query(query)
if err != nil {
return nil, fmt.Errorf("failed to query banned pubkeys: %w", err)
}
defer rows.Close()
var pubkeys []string
for rows.Next() {
var pubkey string
if err := rows.Scan(&pubkey); err != nil {
return nil, fmt.Errorf("failed to scan pubkey row: %w", err)
}
pubkeys = append(pubkeys, pubkey)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error occurred while iterating over pubkey rows: %w", err)
}
return pubkeys, nil
}
// Close closes the database connection.
// This should be called when the DBManager is no longer needed.
func (dbm *DBManager) Close() error {
if dbm.db != nil {
if err := dbm.db.Close(); err != nil {
return fmt.Errorf("failed to close database connection: %w", err)
}
}
return nil
}
// Health checks the database connection health.
// Returns nil if the connection is healthy, an error otherwise.
func (dbm *DBManager) Health() error {
if dbm.db == nil {
return fmt.Errorf("database connection is nil")
}
if err := dbm.db.Ping(); err != nil {
return fmt.Errorf("database ping failed: %w", err)
}
return nil
}

View File

@@ -2,6 +2,13 @@ module git.highperfocused.tech/highperfocused/lumina-relay/relay
go 1.23.4
require (
github.com/fiatjaf/eventstore v0.16.0
github.com/fiatjaf/khatru v0.15.2
github.com/lib/pq v1.10.9
github.com/nbd-wtf/go-nostr v0.46.0
)
require (
fiatjaf.com/lib v0.2.0 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
@@ -12,17 +19,13 @@ require (
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/fasthttp/websocket v1.5.7 // indirect
github.com/fiatjaf/eventstore v0.16.0
github.com/fiatjaf/khatru v0.15.2
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nbd-wtf/go-nostr v0.46.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect

View File

@@ -1,5 +1,6 @@
fiatjaf.com/lib v0.2.0 h1:TgIJESbbND6GjOgGHxF5jsO6EMjuAxIzZHPo5DXYexs=
fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
@@ -12,6 +13,7 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtyd
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
@@ -19,12 +21,11 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnN
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KHau4=
github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU=
github.com/fiatjaf/eventstore v0.15.0 h1:5UXe0+vIb30/cYcOWipks8nR3g+X8W224TFy5yPzivk=
github.com/fiatjaf/eventstore v0.15.0/go.mod h1:KAsld5BhkmSck48aF11Txu8X+OGNmoabw4TlYVWqInc=
github.com/fiatjaf/eventstore v0.16.0 h1:r26aJeOwJTCbEevU8RVqp9FlcAgzKKqUWFH//x+Y+7M=
github.com/fiatjaf/eventstore v0.16.0/go.mod h1:KAsld5BhkmSck48aF11Txu8X+OGNmoabw4TlYVWqInc=
github.com/fiatjaf/khatru v0.15.2 h1:4p0LGUFh+C0zFAPTQdzUdhZDabjmktyov9h5V32EdSw=
github.com/fiatjaf/khatru v0.15.2/go.mod h1:GBQJXZpitDatXF9RookRXcWB5zCJclCE4ufDK3jk80g=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
@@ -40,12 +41,15 @@ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nbd-wtf/go-nostr v0.46.0 h1:aR+xXEC6MPutNMIRhNdi+2iBPEHW7SO10sFaOAVSz3Y=
github.com/nbd-wtf/go-nostr v0.46.0/go.mod h1:xVNOqkn0GImeTmaF6VDwgYsuSkfG3yrIbd0dT6NZDIQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
@@ -55,6 +59,8 @@ github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1Avp
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@@ -70,3 +76,5 @@ golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsG
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,11 +1,13 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
"git.highperfocused.tech/highperfocused/lumina-relay/relay/cache"
@@ -13,6 +15,8 @@ import (
"github.com/fiatjaf/eventstore/postgresql"
"github.com/fiatjaf/khatru"
"github.com/fiatjaf/khatru/policies"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip86"
)
// Cache for storing generic data like event counts
@@ -79,7 +83,18 @@ func main() {
relay.Info.Description = getEnv("RELAY_DESCRIPTION", "LUMINA Relay")
relay.Info.Icon = getEnv("RELAY_ICON", "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fliquipedia.net%2Fcommons%2Fimages%2F3%2F35%2FSCProbe.jpg&f=1&nofb=1&ipt=0cbbfef25bce41da63d910e86c3c343e6c3b9d63194ca9755351bb7c2efa3359&ipo=images")
relay.Info.Software = "lumina-relay"
relay.Info.Version = "0.1.0"
relay.Info.Version = "0.1.1"
// Set the service URL explicitly to avoid 'u' tag validation issues
serviceUrl := getEnv("RELAY_SERVICE_URL", "")
if serviceUrl != "" {
relay.ServiceURL = serviceUrl
}
// load relay admins
admins := strings.Split(getEnv("RELAY_ADMINS", ""), ",")
// add default admin (relay pubkey)
admins = append(admins, relay.Info.PubKey)
// Print relay information
fmt.Printf("Name: %s\n", relay.Info.Name)
@@ -93,6 +108,13 @@ func main() {
panic(err)
}
// Initialize the normal database manager for other data
dbManager, dberr := NewDBManager(postgresURL)
if dberr != nil {
panic(fmt.Sprintf("Failed to initialize database manager: %v", dberr))
}
defer dbManager.Close()
// Initialize trending system to start background calculations
fmt.Println("Initializing trending system...")
if err := trending.Initialize(db.DB.DB); err != nil {
@@ -118,8 +140,63 @@ func main() {
policies.PreventLargeTags(120),
policies.PreventTimestampsInThePast(time.Hour*2),
policies.PreventTimestampsInTheFuture(time.Minute*30),
func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
// Check if the pubkey is banned
isBanned, err := dbManager.IsBannedPubkey(event.PubKey)
if err != nil {
fmt.Printf("Error checking banned pubkey: %v\n", err)
return false, "" // Allow the event if there's an error checking ban status
}
if isBanned {
return true, fmt.Sprintf("banned pubkey: %s", event.PubKey)
}
return false, ""
},
)
// management endpoints
relay.ManagementAPI.RejectAPICall = append(relay.ManagementAPI.RejectAPICall,
func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string) {
user := khatru.GetAuthed(ctx)
// Check if the user is in the admins array
isAdmin := false
for _, admin := range admins {
if user == admin {
isAdmin = true
break
}
}
if !isAdmin {
return true, "unauthorized: admin access required"
}
return false, ""
})
relay.ManagementAPI.AllowPubKey = func(ctx context.Context, pubkey string, reason string) error {
return dbManager.RemoveBannedPubkey(pubkey)
}
relay.ManagementAPI.BanPubKey = func(ctx context.Context, pubkey string, reason string) error {
return dbManager.AddBannedPubkey(pubkey, reason)
}
relay.ManagementAPI.ListBannedPubKeys = func(ctx context.Context) ([]nip86.PubKeyReason, error) {
results, err := dbManager.GetBannedPubkeysWithReasons()
if err != nil {
return nil, err
}
var pubkeyReasons []nip86.PubKeyReason
for _, result := range results {
pubkeyReasons = append(pubkeyReasons, nip86.PubKeyReason{
PubKey: result.Pubkey,
Reason: result.Reason,
})
}
return pubkeyReasons, nil
}
mux := relay.Router()
// set up other http handlers
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {