graph-refs.go raw

   1  package neo4j
   2  
   3  import (
   4  	"context"
   5  	"fmt"
   6  	"strings"
   7  )
   8  
   9  // AddInboundRefsToResult collects inbound references (events that reference discovered items)
  10  // for events at a specific depth in the result.
  11  //
  12  // For example, if you have a follows graph result and want to find all kind-7 reactions
  13  // to posts by users at depth 1, this collects those reactions and adds them to result.InboundRefs.
  14  //
  15  // Parameters:
  16  // - result: The graph result to augment with ref data
  17  // - depth: The depth at which to collect refs (0 = all depths)
  18  // - kinds: Event kinds to collect (e.g., [7] for reactions, [6] for reposts)
  19  func (n *N) AddInboundRefsToResult(result *GraphResult, depth int, kinds []uint16) error {
  20  	ctx := context.Background()
  21  
  22  	// Get pubkeys to find refs for
  23  	var pubkeys []string
  24  	if depth == 0 {
  25  		pubkeys = result.GetAllPubkeys()
  26  	} else {
  27  		pubkeys = result.GetPubkeysAtDepth(depth)
  28  	}
  29  
  30  	if len(pubkeys) == 0 {
  31  		n.Logger.Debugf("AddInboundRefsToResult: no pubkeys at depth %d", depth)
  32  		return nil
  33  	}
  34  
  35  	// Convert kinds to int64 for Neo4j
  36  	kindsInt := make([]int64, len(kinds))
  37  	for i, k := range kinds {
  38  		kindsInt[i] = int64(k)
  39  	}
  40  
  41  	// Query for events by these pubkeys and their inbound references
  42  	// This finds: (ref:Event)-[:REFERENCES]->(authored:Event)<-[:AUTHORED_BY]-(u:NostrUser)
  43  	// where the referencing event has the specified kinds
  44  	cypher := `
  45  		UNWIND $pubkeys AS pk
  46  		MATCH (u:NostrUser {pubkey: pk})<-[:AUTHORED_BY]-(authored:Event)
  47  		WHERE authored.kind IN [1, 30023]
  48  		MATCH (ref:Event)-[:REFERENCES]->(authored)
  49  		WHERE ref.kind IN $kinds
  50  		RETURN authored.id AS target_id, ref.id AS ref_id, ref.kind AS ref_kind
  51  	`
  52  
  53  	params := map[string]any{
  54  		"pubkeys": pubkeys,
  55  		"kinds":   kindsInt,
  56  	}
  57  
  58  	queryResult, err := n.ExecuteRead(ctx, cypher, params)
  59  	if err != nil {
  60  		return fmt.Errorf("failed to query inbound refs: %w", err)
  61  	}
  62  
  63  	refCount := 0
  64  	for queryResult.Next(ctx) {
  65  		record := queryResult.Record()
  66  
  67  		targetID, ok := record.Values[0].(string)
  68  		if !ok || targetID == "" {
  69  			continue
  70  		}
  71  
  72  		refID, ok := record.Values[1].(string)
  73  		if !ok || refID == "" {
  74  			continue
  75  		}
  76  
  77  		refKind, ok := record.Values[2].(int64)
  78  		if !ok {
  79  			continue
  80  		}
  81  
  82  		result.AddInboundRef(uint16(refKind), strings.ToLower(targetID), strings.ToLower(refID))
  83  		refCount++
  84  	}
  85  
  86  	n.Logger.Debugf("AddInboundRefsToResult: collected %d refs for %d pubkeys", refCount, len(pubkeys))
  87  
  88  	return nil
  89  }
  90  
  91  // AddOutboundRefsToResult collects outbound references (events referenced by discovered items).
  92  //
  93  // For example, find all events that posts by users at depth 1 reference (quoted posts, replied-to posts).
  94  func (n *N) AddOutboundRefsToResult(result *GraphResult, depth int, kinds []uint16) error {
  95  	ctx := context.Background()
  96  
  97  	// Get pubkeys to find refs for
  98  	var pubkeys []string
  99  	if depth == 0 {
 100  		pubkeys = result.GetAllPubkeys()
 101  	} else {
 102  		pubkeys = result.GetPubkeysAtDepth(depth)
 103  	}
 104  
 105  	if len(pubkeys) == 0 {
 106  		n.Logger.Debugf("AddOutboundRefsToResult: no pubkeys at depth %d", depth)
 107  		return nil
 108  	}
 109  
 110  	// Convert kinds to int64 for Neo4j
 111  	kindsInt := make([]int64, len(kinds))
 112  	for i, k := range kinds {
 113  		kindsInt[i] = int64(k)
 114  	}
 115  
 116  	// Query for events by these pubkeys and their outbound references
 117  	// This finds: (authored:Event)-[:REFERENCES]->(ref:Event)
 118  	// where the authored event has the specified kinds
 119  	cypher := `
 120  		UNWIND $pubkeys AS pk
 121  		MATCH (u:NostrUser {pubkey: pk})<-[:AUTHORED_BY]-(authored:Event)
 122  		WHERE authored.kind IN $kinds
 123  		MATCH (authored)-[:REFERENCES]->(ref:Event)
 124  		RETURN authored.id AS source_id, ref.id AS ref_id, authored.kind AS source_kind
 125  	`
 126  
 127  	params := map[string]any{
 128  		"pubkeys": pubkeys,
 129  		"kinds":   kindsInt,
 130  	}
 131  
 132  	queryResult, err := n.ExecuteRead(ctx, cypher, params)
 133  	if err != nil {
 134  		return fmt.Errorf("failed to query outbound refs: %w", err)
 135  	}
 136  
 137  	refCount := 0
 138  	for queryResult.Next(ctx) {
 139  		record := queryResult.Record()
 140  
 141  		sourceID, ok := record.Values[0].(string)
 142  		if !ok || sourceID == "" {
 143  			continue
 144  		}
 145  
 146  		refID, ok := record.Values[1].(string)
 147  		if !ok || refID == "" {
 148  			continue
 149  		}
 150  
 151  		sourceKind, ok := record.Values[2].(int64)
 152  		if !ok {
 153  			continue
 154  		}
 155  
 156  		result.AddOutboundRef(uint16(sourceKind), strings.ToLower(sourceID), strings.ToLower(refID))
 157  		refCount++
 158  	}
 159  
 160  	n.Logger.Debugf("AddOutboundRefsToResult: collected %d refs from %d pubkeys", refCount, len(pubkeys))
 161  
 162  	return nil
 163  }
 164