handle-relayinfo.go raw

   1  package app
   2  
   3  import (
   4  	"encoding/json"
   5  	"net/http"
   6  	"sort"
   7  	"strings"
   8  
   9  	"next.orly.dev/pkg/lol/chk"
  10  	"next.orly.dev/pkg/lol/log"
  11  	"next.orly.dev/pkg/acl"
  12  	"next.orly.dev/pkg/nostr/interfaces/signer/p8k"
  13  	"next.orly.dev/pkg/nostr/encoders/hex"
  14  	"next.orly.dev/pkg/nostr/relayinfo"
  15  	"next.orly.dev/pkg/version"
  16  )
  17  
  18  // GraphQueryConfig describes graph query capabilities for NIP-11 advertisement.
  19  type GraphQueryConfig struct {
  20  	Enabled    bool     `json:"enabled"`
  21  	MaxDepth   int      `json:"max_depth"`
  22  	MaxResults int      `json:"max_results"`
  23  	Methods    []string `json:"methods"`
  24  }
  25  
  26  // ExtendedRelayInfo extends the standard NIP-11 relay info with additional fields.
  27  // The Addresses field contains alternative WebSocket URLs for the relay (e.g., .onion).
  28  type ExtendedRelayInfo struct {
  29  	*relayinfo.T
  30  	Addresses      []string          `json:"addresses,omitempty"`
  31  	GraphQuery     *GraphQueryConfig `json:"graph_query,omitempty"`
  32  	Theme          string            `json:"theme,omitempty"`
  33  	BlossomEnabled bool              `json:"blossom_enabled,omitempty"`
  34  	DBType         string            `json:"db_type,omitempty"`
  35  }
  36  
  37  // HandleRelayInfo generates and returns a relay information document in JSON
  38  // format based on the server's configuration and supported NIPs.
  39  //
  40  // # Parameters
  41  //
  42  //   - w: HTTP response writer used to send the generated document.
  43  //
  44  //   - r: HTTP request object containing incoming client request data.
  45  //
  46  // # Expected Behaviour
  47  //
  48  // The function constructs a relay information document using either the
  49  // Informer interface implementation or predefined server configuration. It
  50  // returns this document as a JSON response to the client.
  51  func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
  52  	w.Header().Set("Content-Type", "application/json")
  53  	w.Header().Set("Vary", "Accept")
  54  	log.D.Ln("handling relay information document")
  55  	var info *relayinfo.T
  56  	nips := []relayinfo.NIP{
  57  		relayinfo.BasicProtocol,
  58  		relayinfo.Authentication,
  59  		relayinfo.EncryptedDirectMessage,
  60  		relayinfo.EventDeletion,
  61  		relayinfo.RelayInformationDocument,
  62  		relayinfo.GenericTagQueries,
  63  		// relayinfo.NostrMarketplace,
  64  		relayinfo.CountingResults,
  65  		relayinfo.EventTreatment,
  66  		relayinfo.CommandResults,
  67  		relayinfo.ParameterizedReplaceableEvents,
  68  		relayinfo.ExpirationTimestamp,
  69  		relayinfo.ProtectedEvents,
  70  		relayinfo.RelayListMetadata,
  71  		relayinfo.SearchCapability,
  72  	}
  73  	// Add NIP-43 if enabled
  74  	if s.Config.NIP43Enabled {
  75  		nips = append(nips, relayinfo.RelayAccessMetadata)
  76  	}
  77  	// Add NIP-77 (negentropy) if enabled
  78  	if s.Config.NegentropyEnabled {
  79  		nips = append(nips, relayinfo.NIP{Number: 77, Description: "Negentropy-based sync"})
  80  	}
  81  	// Add NIP-86 (Relay Management API) if ACL mode supports it
  82  	if s.Config.ACLMode == "managed" || s.Config.ACLMode == "curating" {
  83  		nips = append(nips, relayinfo.NIP{Number: 86, Description: "Relay Management API"})
  84  	}
  85  	supportedNIPs := relayinfo.GetList(nips...)
  86  	if s.Config.ACLMode != "none" {
  87  		nipsACL := []relayinfo.NIP{
  88  			relayinfo.BasicProtocol,
  89  			relayinfo.Authentication,
  90  			relayinfo.EncryptedDirectMessage,
  91  			relayinfo.EventDeletion,
  92  			relayinfo.RelayInformationDocument,
  93  			relayinfo.GenericTagQueries,
  94  			// relayinfo.NostrMarketplace,
  95  			relayinfo.CountingResults,
  96  			relayinfo.EventTreatment,
  97  			relayinfo.CommandResults,
  98  			relayinfo.ParameterizedReplaceableEvents,
  99  			relayinfo.ExpirationTimestamp,
 100  			relayinfo.ProtectedEvents,
 101  			relayinfo.RelayListMetadata,
 102  			relayinfo.SearchCapability,
 103  		}
 104  		// Add NIP-43 if enabled
 105  		if s.Config.NIP43Enabled {
 106  			nipsACL = append(nipsACL, relayinfo.RelayAccessMetadata)
 107  		}
 108  		// Add NIP-77 (negentropy) if enabled
 109  		if s.Config.NegentropyEnabled {
 110  			nipsACL = append(nipsACL, relayinfo.NIP{Number: 77, Description: "Negentropy-based sync"})
 111  		}
 112  		// Add NIP-86 (Relay Management API) if ACL mode supports it
 113  		if s.Config.ACLMode == "managed" || s.Config.ACLMode == "curating" {
 114  			nipsACL = append(nipsACL, relayinfo.NIP{Number: 86, Description: "Relay Management API"})
 115  		}
 116  		supportedNIPs = relayinfo.GetList(nipsACL...)
 117  	}
 118  	sort.Sort(supportedNIPs)
 119  	log.I.Ln("supported NIPs", supportedNIPs)
 120  	// Get relay identity pubkey as hex
 121  	var relayPubkey string
 122  	if skb, err := s.DB.GetRelayIdentitySecret(); err == nil && len(skb) == 32 {
 123  		var sign *p8k.Signer
 124  		var sigErr error
 125  		if sign, sigErr = p8k.New(); sigErr == nil {
 126  			if err := sign.InitSec(skb); err == nil {
 127  				relayPubkey = hex.Enc(sign.Pub())
 128  			}
 129  		}
 130  	}
 131  
 132  	// Default relay info
 133  	name := s.Config.AppName
 134  	description := version.Description + " dashboard: " + s.DashboardURL(r)
 135  	icon := "https://i.nostr.build/6wGXAn7Zaw9mHxFg.png"
 136  
 137  	// Override with branding config if available
 138  	if s.brandingMgr != nil {
 139  		nip11 := s.brandingMgr.NIP11Config()
 140  		if nip11.Name != "" {
 141  			name = nip11.Name
 142  		}
 143  		if nip11.Description != "" {
 144  			description = nip11.Description
 145  		}
 146  		if nip11.Icon != "" {
 147  			icon = nip11.Icon
 148  		}
 149  	}
 150  
 151  	// Override with managed ACL config if in managed mode
 152  	if s.Config.ACLMode == "managed" {
 153  		// Get managed ACL instance
 154  		for _, aclInstance := range acl.Registry.ACLs() {
 155  			if aclInstance.Type() == "managed" {
 156  				if managed, ok := aclInstance.(*acl.Managed); ok {
 157  					managedACL := managed.GetManagedACL()
 158  					if managedACL != nil {
 159  						if config, err := managedACL.GetRelayConfig(); err == nil {
 160  							if config.RelayName != "" {
 161  								name = config.RelayName
 162  							}
 163  							if config.RelayDescription != "" {
 164  								description = config.RelayDescription
 165  							}
 166  							if config.RelayIcon != "" {
 167  								icon = config.RelayIcon
 168  							}
 169  						}
 170  					}
 171  				}
 172  				break
 173  			}
 174  		}
 175  	}
 176  
 177  	// Restricted writes applies when ACL mode is not managed/curating but also not none
 178  	// (e.g., follows mode restricts writes to followed pubkeys)
 179  	restrictedWrites := s.Config.ACLMode != "managed" && s.Config.ACLMode != "curating" && s.Config.ACLMode != "none"
 180  
 181  	info = &relayinfo.T{
 182  		Name:        name,
 183  		Description: description,
 184  		PubKey:      relayPubkey,
 185  		Nips:        supportedNIPs,
 186  		Software:    version.URL,
 187  		Version:     strings.TrimPrefix(version.V, "v"),
 188  		Limitation: relayinfo.Limits{
 189  			AuthRequired:     s.Config.AuthRequired || s.Config.ACLMode != "none",
 190  			RestrictedWrites: restrictedWrites,
 191  			PaymentRequired:  s.Config.MonthlyPriceSats > 0,
 192  		},
 193  		Icon: icon,
 194  	}
 195  
 196  	// Build addresses list from config and Tor service
 197  	var addresses []string
 198  
 199  	// Add configured relay addresses
 200  	if len(s.Config.RelayAddresses) > 0 {
 201  		addresses = append(addresses, s.Config.RelayAddresses...)
 202  	}
 203  
 204  	// Add addresses from all transports (Tor .onion, etc.)
 205  	if s.transportMgr != nil {
 206  		addresses = append(addresses, s.transportMgr.Addresses()...)
 207  	}
 208  
 209  	// Build graph query config if enabled
 210  	var graphConfig *GraphQueryConfig
 211  	if s.graphExecutor != nil && s.Config.GraphQueriesEnabled {
 212  		graphEnabled, maxDepth, maxResults, _ := s.Config.GetGraphConfigValues()
 213  		if graphEnabled {
 214  			graphConfig = &GraphQueryConfig{
 215  				Enabled:    true,
 216  				MaxDepth:   maxDepth,
 217  				MaxResults: maxResults,
 218  				Methods:    []string{"follows", "followers", "mentions", "thread"},
 219  			}
 220  		}
 221  	}
 222  
 223  	// Return extended info if we have addresses, graph query support, custom theme, or blossom
 224  	theme := s.Config.Theme
 225  	if theme != "auto" && theme != "light" && theme != "dark" {
 226  		theme = "auto"
 227  	}
 228  	// Blossom is only available if the server is actually initialized (requires Badger backend)
 229  	blossomEnabled := s.blossomServer != nil
 230  	dbType := s.Config.DBType
 231  	if len(addresses) > 0 || graphConfig != nil || theme != "auto" || blossomEnabled || dbType != "badger" {
 232  		extInfo := &ExtendedRelayInfo{
 233  			T:              info,
 234  			Addresses:      addresses,
 235  			GraphQuery:     graphConfig,
 236  			Theme:          theme,
 237  			BlossomEnabled: blossomEnabled,
 238  			DBType:         dbType,
 239  		}
 240  		if err := json.NewEncoder(w).Encode(extInfo); chk.E(err) {
 241  		}
 242  	} else {
 243  		if err := json.NewEncoder(w).Encode(info); chk.E(err) {
 244  		}
 245  	}
 246  }
 247