Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
██╗ ██╗ █████╗ ██╗ ██╗███████╗███╗ ██╗
██║ ██║██╔══██╗██║ ██║██╔════╝████╗ ██║
Expand Down
4 changes: 4 additions & 0 deletions init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
145 changes: 145 additions & 0 deletions management.go
Original file line number Diff line number Diff line change
@@ -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)
}
}