From 5b6fc4e9c48bf2865f7adb3455460bababca0117 Mon Sep 17 00:00:00 2001 From: deymosh Date: Sat, 25 Jul 2026 21:26:38 +0000 Subject: [PATCH] feat: add management API to relays --- config.go | 9 ++++ init.go | 4 ++ management.go | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 management.go diff --git a/config.go b/config.go index 43530696..34475aff 100644 --- a/config.go +++ b/config.go @@ -287,6 +287,15 @@ func nPubToPubkey(label, nPub string) string { } } +// pubkeyToNpub converts a hex public key string into a bech32 npub string. +func pubkeyToNpub(hexKey string) (string, error) { + pub, err := nostr.PubKeyFromHex(hexKey) + if err != nil { + return "", err + } + return nip19.EncodeNpub(pub), nil +} + var art = ` ██╗ ██╗ █████╗ ██╗ ██╗███████╗███╗ ██╗ ██║ ██║██╔══██╗██║ ██║██╔════╝████╗ ██║ diff --git a/init.go b/init.go index d2af84a6..8a23d44b 100644 --- a/init.go +++ b/init.go @@ -162,6 +162,7 @@ func initRelays(ctx context.Context) { privateRelay.UseEventstore(privateDB, 1000) + SetupManagementAPI(privateRelay) mux := privateRelay.Router() mux.HandleFunc("GET /private", func(w http.ResponseWriter, r *http.Request) { @@ -239,6 +240,7 @@ func initRelays(ctx context.Context) { chatRelay.UseEventstore(chatDB, 1000) + SetupManagementAPI(chatRelay) mux = chatRelay.Router() mux.HandleFunc("GET /chat", func(w http.ResponseWriter, r *http.Request) { @@ -308,6 +310,7 @@ func initRelays(ctx context.Context) { go blast(ctx, &event) } + SetupManagementAPI(outboxRelay) mux = outboxRelay.Router() mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { @@ -419,6 +422,7 @@ func initRelays(ctx context.Context) { inboxRelay.UseEventstore(inboxDB, 1000) + SetupManagementAPI(inboxRelay) mux = inboxRelay.Router() mux.HandleFunc("GET /inbox", func(w http.ResponseWriter, r *http.Request) { diff --git a/management.go b/management.go new file mode 100644 index 00000000..20a75b5d --- /dev/null +++ b/management.go @@ -0,0 +1,145 @@ +package main + +import ( + "context" + "encoding/json" + "log/slog" + "os" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/nip86" + "fiatjaf.com/nostr/khatru" +) + +func SetupManagementAPI(relay *khatru.Relay) { + relay.ManagementAPI.AllowPubKey = AllowPubKey + relay.ManagementAPI.UnallowPubKey = UnallowPubKey + relay.ManagementAPI.ListAllowedPubKeys = ListAllowedPubKeys + + relay.ManagementAPI.BanPubKey = BanPubKey + relay.ManagementAPI.UnbanPubKey = UnbanPubKey + relay.ManagementAPI.ListBannedPubKeys = ListBannedPubKeys +} + +// AllowPubKey adds a public key to the whitelist and persists it to the file +func AllowPubKey(ctx context.Context, pubkey nostr.PubKey, reason string) error { + slog.Debug("allowing pubkey", "pubkey", pubkey.Hex(), "reason", reason) + + hex := pubkey.Hex() + config.WhitelistedPubKeys[hex] = struct{}{} + + // Persist changes to the file if configured + filePath := getEnvString("WHITELISTED_NPUBS_FILE", "") + if filePath != "" { + savePubKeysToFile(filePath, config.WhitelistedPubKeys) + } + + return nil +} + +// UnallowPubKey removes a public key from the whitelist and persists the changes +func UnallowPubKey(ctx context.Context, pubkey nostr.PubKey, reason string) error { + slog.Debug("unallowing pubkey", "pubkey", pubkey.Hex(), "reason", reason) + + hex := pubkey.Hex() + if hex == config.OwnerPubKey { + slog.Warn("attempted to unallow relay owner pubkey, action blocked", "pubkey", hex) + return nil + } + + delete(config.WhitelistedPubKeys, hex) + + // Persist changes to the file if configured + filePath := getEnvString("WHITELISTED_NPUBS_FILE", "") + if filePath != "" { + savePubKeysToFile(filePath, config.WhitelistedPubKeys) + } + + return nil +} + +// ListAllowedPubKeys returns all currently whitelisted public keys +func ListAllowedPubKeys(ctx context.Context) ([]nip86.PubKeyReason, error) { + var allowed []nip86.PubKeyReason + for pubkey := range config.WhitelistedPubKeys { + allowed = append(allowed, nip86.PubKeyReason{ + PubKey: nostr.MustPubKeyFromHex(pubkey), + Reason: "whitelisted", + }) + } + + return allowed, nil +} + +// BanPubKey adds a public key to the blacklist and persists it to the file +func BanPubKey(ctx context.Context, pubkey nostr.PubKey, reason string) error { + slog.Debug("banning pubkey", "pubkey", pubkey.Hex(), "reason", reason) + + hex := pubkey.Hex() + if hex == config.OwnerPubKey { + slog.Warn("attempted to ban relay owner pubkey, action blocked", "pubkey", hex) + return nil + } + + config.BlacklistedPubKeys[hex] = struct{}{} + + // Persist changes to the file if configured + filePath := getEnvString("BLACKLISTED_NPUBS_FILE", "") + if filePath != "" { + savePubKeysToFile(filePath, config.BlacklistedPubKeys) + } + + return nil +} + +// UnbanPubKey removes a public key from the blacklist and persists the changes +func UnbanPubKey(ctx context.Context, pubkey nostr.PubKey, reason string) error { + slog.Debug("unbanning pubkey", "pubkey", pubkey.Hex(), "reason", reason) + + hex := pubkey.Hex() + delete(config.BlacklistedPubKeys, hex) + + // Persist changes to the file if configured + filePath := getEnvString("BLACKLISTED_NPUBS_FILE", "") + if filePath != "" { + savePubKeysToFile(filePath, config.BlacklistedPubKeys) + } + + return nil +} + +// ListBannedPubKeys returns all currently blacklisted public keys +func ListBannedPubKeys(ctx context.Context) ([]nip86.PubKeyReason, error) { + var banned []nip86.PubKeyReason + for pubkey := range config.BlacklistedPubKeys { + banned = append(banned, nip86.PubKeyReason{ + PubKey: nostr.MustPubKeyFromHex(pubkey), + Reason: "blacklisted", + }) + } + + return banned, nil +} + +// savePubKeysToFile helper function to dump map keys into a JSON array file as npubs +func savePubKeysToFile(filePath string, pubKeysMap map[string]struct{}) { + npubs := []string{} + for hexKey := range pubKeysMap { + npub, err := pubkeyToNpub(hexKey) + if err != nil { + slog.Error("failed to encode pubkey to npub during persistence", "hex", hexKey, "error", err) + continue + } + npubs = append(npubs, npub) + } + + fileData, err := json.MarshalIndent(npubs, "", " ") + if err != nil { + slog.Error("failed to marshal pubkeys for persistence", "file", filePath, "error", err) + return + } + + if err := os.WriteFile(filePath, fileData, 0644); err != nil { + slog.Error("failed to write pubkeys file for persistence", "file", filePath, "error", err) + } +} \ No newline at end of file