mirror of
https://github.com/layer-systems/relay.git
synced 2026-06-04 09:31:15 +02:00
424 lines
13 KiB
Go
424 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/fiatjaf/eventstore/postgresql"
|
|
"github.com/fiatjaf/khatru"
|
|
"github.com/fiatjaf/khatru/policies"
|
|
_ "github.com/lib/pq"
|
|
"github.com/nbd-wtf/go-nostr"
|
|
"github.com/nbd-wtf/go-nostr/nip86"
|
|
)
|
|
|
|
func getEnv(key, fallback string) string {
|
|
if value, exists := os.LookupEnv(key); exists {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func initManagementDB(db *sql.DB) error {
|
|
// create allowed_pubkeys table
|
|
_, err := db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS allowed_pubkeys (
|
|
pubkey TEXT PRIMARY KEY,
|
|
reason TEXT NOT NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create allowed_pubkeys table: %w", err)
|
|
}
|
|
|
|
// create banned_pubkeys table
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS banned_pubkeys (
|
|
pubkey TEXT PRIMARY KEY,
|
|
reason TEXT NOT NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create banned_pubkeys table: %w", err)
|
|
}
|
|
|
|
// create reports table for NIP-56
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS reports (
|
|
id TEXT PRIMARY KEY,
|
|
reporter_pubkey TEXT NOT NULL,
|
|
reported_event_id TEXT,
|
|
reported_pubkey TEXT NOT NULL,
|
|
report_type TEXT NOT NULL,
|
|
content TEXT,
|
|
resolved BOOLEAN NOT NULL DEFAULT FALSE,
|
|
resolution TEXT,
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create reports table: %w", err)
|
|
}
|
|
|
|
// create banned_events table
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS banned_events (
|
|
event_id TEXT PRIMARY KEY,
|
|
reason TEXT NOT NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create banned_events table: %w", err)
|
|
}
|
|
|
|
// create allowed_events table
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS allowed_events (
|
|
event_id TEXT PRIMARY KEY,
|
|
reason TEXT NOT NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create allowed_events table: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
// create the relay instance
|
|
relay := khatru.NewRelay()
|
|
|
|
// set up some basic properties (will be returned on the NIP-11 endpoint)
|
|
relay.Info.Name = getEnv("RELAY_NAME", "layer.systems relay")
|
|
relay.Info.PubKey = getEnv("RELAY_PUBKEY", "480ec1a7516406090dc042ddf67780ef30f26f3a864e83b417c053a5a611c838")
|
|
relay.Info.Description = getEnv("RELAY_DESCRIPTION", "this is a public 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 = "https://github.com/layer-systems/relay"
|
|
relay.Info.Version = "0.1.0"
|
|
relay.Info.SupportedNIPs = []any{1, 11, 17, 40, 42, 70, 86}
|
|
|
|
queryLimit, _ := strconv.Atoi(getEnv("QUERY_LIMIT", "100"))
|
|
db := postgresql.PostgresBackend{DatabaseURL: getEnv("DATABASE_URL", "postgresql://postgres:postgres@db:5432/khatru-relay?sslmode=disable"), QueryLimit: queryLimit}
|
|
if err := db.Init(); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
|
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
|
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
|
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
|
relay.ReplaceEvent = append(relay.ReplaceEvent, db.ReplaceEvent)
|
|
|
|
relay.RejectEvent = append(relay.RejectEvent, policies.ValidateKind)
|
|
|
|
// setup management database (second connection for NIP-86)
|
|
managementDB, err := sql.Open("postgres", getEnv("DATABASE_URL", "postgresql://postgres:postgres@db:5432/khatru-relay?sslmode=disable"))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer managementDB.Close()
|
|
|
|
// initialize management tables
|
|
if err := initManagementDB(managementDB); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Store NIP-56 report events
|
|
relay.StoreEvent = append(relay.StoreEvent, func(ctx context.Context, event *nostr.Event) error {
|
|
if event.Kind == 1984 {
|
|
// Parse report tags
|
|
var reportedEventId, reportedPubkey, reportType string
|
|
|
|
// Extract reported pubkey and type from p tag
|
|
if pTag := event.Tags.Find("p"); len(pTag) >= 2 {
|
|
reportedPubkey = pTag[1]
|
|
if len(pTag) >= 3 {
|
|
reportType = pTag[2]
|
|
}
|
|
}
|
|
|
|
// Extract reported event id and type from e tag
|
|
if eTag := event.Tags.Find("e"); len(eTag) >= 2 {
|
|
reportedEventId = eTag[1]
|
|
if len(eTag) >= 3 && reportType == "" {
|
|
reportType = eTag[2]
|
|
}
|
|
}
|
|
|
|
if reportedPubkey != "" && reportType != "" {
|
|
_, err := managementDB.ExecContext(ctx, `
|
|
INSERT INTO reports (id, reporter_pubkey, reported_event_id, reported_pubkey, report_type, content, created_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
ON CONFLICT (id) DO NOTHING
|
|
`, event.ID, event.PubKey, reportedEventId, reportedPubkey, reportType, event.Content, time.Unix(int64(event.CreatedAt), 0))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to store report: %w", err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
relay.RejectEvent = append(relay.RejectEvent,
|
|
func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
|
var reason string
|
|
row := managementDB.QueryRowContext(ctx, `SELECT reason FROM banned_pubkeys WHERE pubkey = $1`, event.PubKey)
|
|
switch err := row.Scan(&reason); err {
|
|
case sql.ErrNoRows:
|
|
return false, ""
|
|
case nil:
|
|
return true, fmt.Sprintf("pubkey %s banned: %s", event.PubKey, reason)
|
|
default:
|
|
// on unexpected DB errors, do not reject the event solely because of the failure
|
|
return false, ""
|
|
}
|
|
},
|
|
)
|
|
|
|
// Check for banned events
|
|
relay.RejectEvent = append(relay.RejectEvent,
|
|
func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
|
var reason string
|
|
row := managementDB.QueryRowContext(ctx, `SELECT reason FROM banned_events WHERE event_id = $1`, event.ID)
|
|
switch err := row.Scan(&reason); err {
|
|
case sql.ErrNoRows:
|
|
return false, ""
|
|
case nil:
|
|
return true, fmt.Sprintf("event %s banned: %s", event.ID, reason)
|
|
default:
|
|
// on unexpected DB errors, do not reject the event solely because of the failure
|
|
return false, ""
|
|
}
|
|
},
|
|
)
|
|
|
|
// // there are many other configurable things you can set
|
|
// relay.RejectEvent = append(relay.RejectEvent,
|
|
// // built-in policies
|
|
// policies.ValidateKind,
|
|
|
|
// // define your own policies
|
|
// policies.PreventLargeTags(100),
|
|
// func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
|
// if event.PubKey == "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" {
|
|
// return true, "we don't allow this person to write here"
|
|
// }
|
|
// return false, "" // anyone else can
|
|
// },
|
|
// )
|
|
|
|
relay.RejectFilter = append(relay.RejectFilter, RejectNonAuthenticatedGiftWrapQueries)
|
|
|
|
// management endpoints
|
|
relay.ManagementAPI.RejectAPICall = append(relay.ManagementAPI.RejectAPICall,
|
|
func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string) {
|
|
user := khatru.GetAuthed(ctx)
|
|
ownerPubKey := getEnv("RELAY_PUBKEY", "480ec1a7516406090dc042ddf67780ef30f26f3a864e83b417c053a5a611c838")
|
|
if user != ownerPubKey {
|
|
return true, "auth-required: only relay owner can access management API"
|
|
}
|
|
return false, ""
|
|
})
|
|
|
|
relay.ManagementAPI.AllowPubKey = func(ctx context.Context, pubkey string, reason string) error {
|
|
_, err := managementDB.Exec(`
|
|
INSERT INTO allowed_pubkeys (pubkey, reason, created_at)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (pubkey) DO UPDATE SET reason = $2, created_at = $3
|
|
`, pubkey, reason, time.Now())
|
|
return err
|
|
}
|
|
|
|
relay.ManagementAPI.BanPubKey = func(ctx context.Context, pubkey string, reason string) error {
|
|
_, err := managementDB.Exec(`
|
|
INSERT INTO banned_pubkeys (pubkey, reason, created_at)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (pubkey) DO UPDATE SET reason = $2, created_at = $3
|
|
`, pubkey, reason, time.Now())
|
|
return err
|
|
}
|
|
|
|
relay.ManagementAPI.ListAllowedPubKeys = func(ctx context.Context) ([]nip86.PubKeyReason, error) {
|
|
rows, err := managementDB.Query(`SELECT pubkey, reason FROM allowed_pubkeys ORDER BY created_at DESC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var result []nip86.PubKeyReason
|
|
for rows.Next() {
|
|
var pk nip86.PubKeyReason
|
|
if err := rows.Scan(&pk.PubKey, &pk.Reason); err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, pk)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
relay.ManagementAPI.ListBannedPubKeys = func(ctx context.Context) ([]nip86.PubKeyReason, error) {
|
|
rows, err := managementDB.Query(`SELECT pubkey, reason FROM banned_pubkeys ORDER BY created_at DESC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var result []nip86.PubKeyReason
|
|
for rows.Next() {
|
|
var pk nip86.PubKeyReason
|
|
if err := rows.Scan(&pk.PubKey, &pk.Reason); err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, pk)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
relay.ManagementAPI.ListEventsNeedingModeration = func(ctx context.Context) ([]nip86.IDReason, error) {
|
|
rows, err := managementDB.Query(`
|
|
SELECT COALESCE(reported_event_id, reported_pubkey),
|
|
CONCAT(report_type, ': ', content, ' (reported by ', reporter_pubkey, ')')
|
|
FROM reports
|
|
WHERE resolved = FALSE
|
|
ORDER BY created_at DESC
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var result []nip86.IDReason
|
|
for rows.Next() {
|
|
var ir nip86.IDReason
|
|
if err := rows.Scan(&ir.ID, &ir.Reason); err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, ir)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
relay.ManagementAPI.AllowEvent = func(ctx context.Context, id string, reason string) error {
|
|
// Mark reports about this event as resolved
|
|
_, err := managementDB.Exec(`
|
|
UPDATE reports
|
|
SET resolved = TRUE, resolution = $2
|
|
WHERE reported_event_id = $1 OR reported_pubkey = $1
|
|
`, id, "allowed: "+reason)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add to allowed_events table
|
|
_, err = managementDB.Exec(`
|
|
INSERT INTO allowed_events (event_id, reason, created_at)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (event_id) DO UPDATE SET reason = $2, created_at = $3
|
|
`, id, reason, time.Now())
|
|
return err
|
|
}
|
|
|
|
relay.ManagementAPI.BanEvent = func(ctx context.Context, id string, reason string) error {
|
|
// Mark reports about this event as resolved
|
|
_, err := managementDB.Exec(`
|
|
UPDATE reports
|
|
SET resolved = TRUE, resolution = $2
|
|
WHERE reported_event_id = $1 OR reported_pubkey = $1
|
|
`, id, "banned: "+reason)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add to banned_events table
|
|
_, err = managementDB.Exec(`
|
|
INSERT INTO banned_events (event_id, reason, created_at)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (event_id) DO UPDATE SET reason = $2, created_at = $3
|
|
`, id, reason, time.Now())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Query and delete the event from the main event store (if it exists)
|
|
for _, query := range relay.QueryEvents {
|
|
ch, err := query(ctx, nostr.Filter{IDs: []string{id}})
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Read all events from the channel and delete them
|
|
for evt := range ch {
|
|
if evt != nil {
|
|
for _, deleter := range relay.DeleteEvent {
|
|
if err := deleter(ctx, evt); err != nil {
|
|
return fmt.Errorf("failed to delete event: %w", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Event successfully deleted or didn't exist - either way, ban is in place
|
|
break
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
relay.ManagementAPI.ListBannedEvents = func(ctx context.Context) ([]nip86.IDReason, error) {
|
|
rows, err := managementDB.Query(`SELECT event_id, reason FROM banned_events ORDER BY created_at DESC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var result []nip86.IDReason
|
|
for rows.Next() {
|
|
var ir nip86.IDReason
|
|
if err := rows.Scan(&ir.ID, &ir.Reason); err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, ir)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
relay.ManagementAPI.ListAllowedEvents = func(ctx context.Context) ([]nip86.IDReason, error) {
|
|
rows, err := managementDB.Query(`SELECT event_id, reason FROM allowed_events ORDER BY created_at DESC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var result []nip86.IDReason
|
|
for rows.Next() {
|
|
var ir nip86.IDReason
|
|
if err := rows.Scan(&ir.ID, &ir.Reason); err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, ir)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
mux := relay.Router()
|
|
// set up other http handlers
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("content-type", "text/html")
|
|
fmt.Fprintf(w, `Please use a <b>nostr</b> client!`)
|
|
})
|
|
|
|
// start the server
|
|
fmt.Println("running on :3334")
|
|
http.ListenAndServe(":3334", relay)
|
|
}
|