Some checks failed
Go / build-and-release (push) Has been cancelled
Curation Mode: - Three-tier publisher classification: Trusted, Blacklisted, Unclassified - Per-pubkey rate limiting (default 50/day) for unclassified users - IP flood protection (default 500/day) with automatic banning - Event kind allow-listing via categories, ranges, and custom kinds - Query filtering hides blacklisted pubkey events (admin/owner exempt) - Web UI for managing trusted/blacklisted pubkeys and configuration - NIP-86 API endpoints for all curation management operations Graph Query Extension: - Complete reference aggregation for Badger and Neo4j backends - E-tag graph backfill migration (v8) runs automatically on startup - Configuration options: ORLY_GRAPH_QUERIES_ENABLED, MAX_DEPTH, etc. - NIP-11 advertisement of graph query capabilities Files modified: - app/handle-nip86-curating.go: NIP-86 curation API handlers (new) - app/web/src/CurationView.svelte: Curation management UI (new) - app/web/src/kindCategories.js: Kind category definitions (new) - pkg/acl/curating.go: Curating ACL implementation (new) - pkg/database/curating-acl.go: Database layer for curation (new) - pkg/neo4j/graph-refs.go: Neo4j ref collection (new) - pkg/database/migrations.go: E-tag graph backfill migration - pkg/protocol/graph/executor.go: Reference aggregation support - app/handle-event.go: Curation config event processing - app/handle-req.go: Blacklist filtering for queries - docs/GRAPH_QUERIES_REMAINING_PLAN.md: Updated completion status 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
206 lines
6.1 KiB
Go
206 lines
6.1 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
|
|
"lol.mleku.dev/chk"
|
|
"lol.mleku.dev/log"
|
|
"next.orly.dev/pkg/acl"
|
|
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
|
|
"git.mleku.dev/mleku/nostr/encoders/hex"
|
|
"git.mleku.dev/mleku/nostr/relayinfo"
|
|
"next.orly.dev/pkg/version"
|
|
)
|
|
|
|
// GraphQueryConfig describes graph query capabilities for NIP-11 advertisement.
|
|
type GraphQueryConfig struct {
|
|
Enabled bool `json:"enabled"`
|
|
MaxDepth int `json:"max_depth"`
|
|
MaxResults int `json:"max_results"`
|
|
Methods []string `json:"methods"`
|
|
}
|
|
|
|
// ExtendedRelayInfo extends the standard NIP-11 relay info with additional fields.
|
|
// The Addresses field contains alternative WebSocket URLs for the relay (e.g., .onion).
|
|
type ExtendedRelayInfo struct {
|
|
*relayinfo.T
|
|
Addresses []string `json:"addresses,omitempty"`
|
|
GraphQuery *GraphQueryConfig `json:"graph_query,omitempty"`
|
|
}
|
|
|
|
// HandleRelayInfo generates and returns a relay information document in JSON
|
|
// format based on the server's configuration and supported NIPs.
|
|
//
|
|
// # Parameters
|
|
//
|
|
// - w: HTTP response writer used to send the generated document.
|
|
//
|
|
// - r: HTTP request object containing incoming client request data.
|
|
//
|
|
// # Expected Behaviour
|
|
//
|
|
// The function constructs a relay information document using either the
|
|
// Informer interface implementation or predefined server configuration. It
|
|
// returns this document as a JSON response to the client.
|
|
func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Vary", "Accept")
|
|
log.D.Ln("handling relay information document")
|
|
var info *relayinfo.T
|
|
nips := []relayinfo.NIP{
|
|
relayinfo.BasicProtocol,
|
|
relayinfo.Authentication,
|
|
relayinfo.EncryptedDirectMessage,
|
|
relayinfo.EventDeletion,
|
|
relayinfo.RelayInformationDocument,
|
|
relayinfo.GenericTagQueries,
|
|
// relayinfo.NostrMarketplace,
|
|
relayinfo.CountingResults,
|
|
relayinfo.EventTreatment,
|
|
relayinfo.CommandResults,
|
|
relayinfo.ParameterizedReplaceableEvents,
|
|
relayinfo.ExpirationTimestamp,
|
|
relayinfo.ProtectedEvents,
|
|
relayinfo.RelayListMetadata,
|
|
relayinfo.SearchCapability,
|
|
}
|
|
// Add NIP-43 if enabled
|
|
if s.Config.NIP43Enabled {
|
|
nips = append(nips, relayinfo.RelayAccessMetadata)
|
|
}
|
|
supportedNIPs := relayinfo.GetList(nips...)
|
|
if s.Config.ACLMode != "none" {
|
|
nipsACL := []relayinfo.NIP{
|
|
relayinfo.BasicProtocol,
|
|
relayinfo.Authentication,
|
|
relayinfo.EncryptedDirectMessage,
|
|
relayinfo.EventDeletion,
|
|
relayinfo.RelayInformationDocument,
|
|
relayinfo.GenericTagQueries,
|
|
// relayinfo.NostrMarketplace,
|
|
relayinfo.CountingResults,
|
|
relayinfo.EventTreatment,
|
|
relayinfo.CommandResults,
|
|
relayinfo.ParameterizedReplaceableEvents,
|
|
relayinfo.ExpirationTimestamp,
|
|
relayinfo.ProtectedEvents,
|
|
relayinfo.RelayListMetadata,
|
|
relayinfo.SearchCapability,
|
|
}
|
|
// Add NIP-43 if enabled
|
|
if s.Config.NIP43Enabled {
|
|
nipsACL = append(nipsACL, relayinfo.RelayAccessMetadata)
|
|
}
|
|
supportedNIPs = relayinfo.GetList(nipsACL...)
|
|
}
|
|
sort.Sort(supportedNIPs)
|
|
log.I.Ln("supported NIPs", supportedNIPs)
|
|
// Get relay identity pubkey as hex
|
|
var relayPubkey string
|
|
if skb, err := s.DB.GetRelayIdentitySecret(); err == nil && len(skb) == 32 {
|
|
var sign *p8k.Signer
|
|
var sigErr error
|
|
if sign, sigErr = p8k.New(); sigErr == nil {
|
|
if err := sign.InitSec(skb); err == nil {
|
|
relayPubkey = hex.Enc(sign.Pub())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default relay info
|
|
name := s.Config.AppName
|
|
description := version.Description + " dashboard: " + s.DashboardURL(r)
|
|
icon := "https://i.nostr.build/6wGXAn7Zaw9mHxFg.png"
|
|
|
|
// Override with managed ACL config if in managed mode
|
|
if s.Config.ACLMode == "managed" {
|
|
// Get managed ACL instance
|
|
for _, aclInstance := range acl.Registry.ACL {
|
|
if aclInstance.Type() == "managed" {
|
|
if managed, ok := aclInstance.(*acl.Managed); ok {
|
|
managedACL := managed.GetManagedACL()
|
|
if managedACL != nil {
|
|
if config, err := managedACL.GetRelayConfig(); err == nil {
|
|
if config.RelayName != "" {
|
|
name = config.RelayName
|
|
}
|
|
if config.RelayDescription != "" {
|
|
description = config.RelayDescription
|
|
}
|
|
if config.RelayIcon != "" {
|
|
icon = config.RelayIcon
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restricted writes applies when ACL mode is not managed/curating but also not none
|
|
// (e.g., follows mode restricts writes to followed pubkeys)
|
|
restrictedWrites := s.Config.ACLMode != "managed" && s.Config.ACLMode != "curating" && s.Config.ACLMode != "none"
|
|
|
|
info = &relayinfo.T{
|
|
Name: name,
|
|
Description: description,
|
|
PubKey: relayPubkey,
|
|
Nips: supportedNIPs,
|
|
Software: version.URL,
|
|
Version: strings.TrimPrefix(version.V, "v"),
|
|
Limitation: relayinfo.Limits{
|
|
AuthRequired: s.Config.AuthRequired || s.Config.ACLMode != "none",
|
|
RestrictedWrites: restrictedWrites,
|
|
PaymentRequired: s.Config.MonthlyPriceSats > 0,
|
|
},
|
|
Icon: icon,
|
|
}
|
|
|
|
// Build addresses list from config and Tor service
|
|
var addresses []string
|
|
|
|
// Add configured relay addresses
|
|
if len(s.Config.RelayAddresses) > 0 {
|
|
addresses = append(addresses, s.Config.RelayAddresses...)
|
|
}
|
|
|
|
// Add Tor hidden service address if available
|
|
if s.torService != nil {
|
|
if onionAddr := s.torService.OnionWSAddress(); onionAddr != "" {
|
|
addresses = append(addresses, onionAddr)
|
|
}
|
|
}
|
|
|
|
// Build graph query config if enabled
|
|
var graphConfig *GraphQueryConfig
|
|
if s.graphExecutor != nil && s.Config.GraphQueriesEnabled {
|
|
graphEnabled, maxDepth, maxResults, _ := s.Config.GetGraphConfigValues()
|
|
if graphEnabled {
|
|
graphConfig = &GraphQueryConfig{
|
|
Enabled: true,
|
|
MaxDepth: maxDepth,
|
|
MaxResults: maxResults,
|
|
Methods: []string{"follows", "followers", "mentions", "thread"},
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return extended info if we have addresses or graph query support, otherwise standard info
|
|
if len(addresses) > 0 || graphConfig != nil {
|
|
extInfo := &ExtendedRelayInfo{
|
|
T: info,
|
|
Addresses: addresses,
|
|
GraphQuery: graphConfig,
|
|
}
|
|
if err := json.NewEncoder(w).Encode(extInfo); chk.E(err) {
|
|
}
|
|
} else {
|
|
if err := json.NewEncoder(w).Encode(info); chk.E(err) {
|
|
}
|
|
}
|
|
}
|