Files
next.orly.dev/pkg/neo4j/graph-refs.go
woikos 047cdf3472
Some checks failed
Go / build-and-release (push) Has been cancelled
Add curation ACL mode and complete graph query implementation (v0.47.0)
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>
2026-01-05 21:42:17 +01:00

164 lines
4.3 KiB
Go

package neo4j
import (
"context"
"fmt"
"strings"
)
// AddInboundRefsToResult collects inbound references (events that reference discovered items)
// for events at a specific depth in the result.
//
// For example, if you have a follows graph result and want to find all kind-7 reactions
// to posts by users at depth 1, this collects those reactions and adds them to result.InboundRefs.
//
// Parameters:
// - result: The graph result to augment with ref data
// - depth: The depth at which to collect refs (0 = all depths)
// - kinds: Event kinds to collect (e.g., [7] for reactions, [6] for reposts)
func (n *N) AddInboundRefsToResult(result *GraphResult, depth int, kinds []uint16) error {
ctx := context.Background()
// Get pubkeys to find refs for
var pubkeys []string
if depth == 0 {
pubkeys = result.GetAllPubkeys()
} else {
pubkeys = result.GetPubkeysAtDepth(depth)
}
if len(pubkeys) == 0 {
n.Logger.Debugf("AddInboundRefsToResult: no pubkeys at depth %d", depth)
return nil
}
// Convert kinds to int64 for Neo4j
kindsInt := make([]int64, len(kinds))
for i, k := range kinds {
kindsInt[i] = int64(k)
}
// Query for events by these pubkeys and their inbound references
// This finds: (ref:Event)-[:REFERENCES]->(authored:Event)<-[:AUTHORED_BY]-(u:NostrUser)
// where the referencing event has the specified kinds
cypher := `
UNWIND $pubkeys AS pk
MATCH (u:NostrUser {pubkey: pk})<-[:AUTHORED_BY]-(authored:Event)
WHERE authored.kind IN [1, 30023]
MATCH (ref:Event)-[:REFERENCES]->(authored)
WHERE ref.kind IN $kinds
RETURN authored.id AS target_id, ref.id AS ref_id, ref.kind AS ref_kind
`
params := map[string]any{
"pubkeys": pubkeys,
"kinds": kindsInt,
}
queryResult, err := n.ExecuteRead(ctx, cypher, params)
if err != nil {
return fmt.Errorf("failed to query inbound refs: %w", err)
}
refCount := 0
for queryResult.Next(ctx) {
record := queryResult.Record()
targetID, ok := record.Values[0].(string)
if !ok || targetID == "" {
continue
}
refID, ok := record.Values[1].(string)
if !ok || refID == "" {
continue
}
refKind, ok := record.Values[2].(int64)
if !ok {
continue
}
result.AddInboundRef(uint16(refKind), strings.ToLower(targetID), strings.ToLower(refID))
refCount++
}
n.Logger.Debugf("AddInboundRefsToResult: collected %d refs for %d pubkeys", refCount, len(pubkeys))
return nil
}
// AddOutboundRefsToResult collects outbound references (events referenced by discovered items).
//
// For example, find all events that posts by users at depth 1 reference (quoted posts, replied-to posts).
func (n *N) AddOutboundRefsToResult(result *GraphResult, depth int, kinds []uint16) error {
ctx := context.Background()
// Get pubkeys to find refs for
var pubkeys []string
if depth == 0 {
pubkeys = result.GetAllPubkeys()
} else {
pubkeys = result.GetPubkeysAtDepth(depth)
}
if len(pubkeys) == 0 {
n.Logger.Debugf("AddOutboundRefsToResult: no pubkeys at depth %d", depth)
return nil
}
// Convert kinds to int64 for Neo4j
kindsInt := make([]int64, len(kinds))
for i, k := range kinds {
kindsInt[i] = int64(k)
}
// Query for events by these pubkeys and their outbound references
// This finds: (authored:Event)-[:REFERENCES]->(ref:Event)
// where the authored event has the specified kinds
cypher := `
UNWIND $pubkeys AS pk
MATCH (u:NostrUser {pubkey: pk})<-[:AUTHORED_BY]-(authored:Event)
WHERE authored.kind IN $kinds
MATCH (authored)-[:REFERENCES]->(ref:Event)
RETURN authored.id AS source_id, ref.id AS ref_id, authored.kind AS source_kind
`
params := map[string]any{
"pubkeys": pubkeys,
"kinds": kindsInt,
}
queryResult, err := n.ExecuteRead(ctx, cypher, params)
if err != nil {
return fmt.Errorf("failed to query outbound refs: %w", err)
}
refCount := 0
for queryResult.Next(ctx) {
record := queryResult.Record()
sourceID, ok := record.Values[0].(string)
if !ok || sourceID == "" {
continue
}
refID, ok := record.Values[1].(string)
if !ok || refID == "" {
continue
}
sourceKind, ok := record.Values[2].(int64)
if !ok {
continue
}
result.AddOutboundRef(uint16(sourceKind), strings.ToLower(sourceID), strings.ToLower(refID))
refCount++
}
n.Logger.Debugf("AddOutboundRefsToResult: collected %d refs from %d pubkeys", refCount, len(pubkeys))
return nil
}