graph-mentions.go raw

   1  package neo4j
   2  
   3  import (
   4  	"context"
   5  	"fmt"
   6  	"strings"
   7  
   8  	"next.orly.dev/pkg/nostr/encoders/hex"
   9  	"next.orly.dev/pkg/protocol/graph"
  10  )
  11  
  12  // FindMentions finds events that mention a pubkey via p-tags.
  13  // This returns events grouped by depth, where depth represents how the events relate:
  14  //   - Depth 1: Events that directly mention the seed pubkey
  15  //   - Depth 2+: Not typically used for mentions (reserved for future expansion)
  16  //
  17  // The kinds parameter filters which event kinds to include (e.g., [1] for notes only,
  18  // [1,7] for notes and reactions, etc.)
  19  //
  20  // Uses Neo4j MENTIONS relationships created by SaveEvent when processing p-tags.
  21  func (n *N) FindMentions(pubkey []byte, kinds []uint16) (graph.GraphResultI, error) {
  22  	result := NewGraphResult()
  23  
  24  	if len(pubkey) != 32 {
  25  		return result, fmt.Errorf("invalid pubkey length: expected 32, got %d", len(pubkey))
  26  	}
  27  
  28  	pubkeyHex := strings.ToLower(hex.Enc(pubkey))
  29  	ctx := context.Background()
  30  
  31  	// Build kinds filter if specified
  32  	var kindsFilter string
  33  	params := map[string]any{
  34  		"pubkey": pubkeyHex,
  35  	}
  36  
  37  	if len(kinds) > 0 {
  38  		// Convert uint16 slice to int64 slice for Neo4j
  39  		kindsInt := make([]int64, len(kinds))
  40  		for i, k := range kinds {
  41  			kindsInt[i] = int64(k)
  42  		}
  43  		params["kinds"] = kindsInt
  44  		kindsFilter = "AND e.kind IN $kinds"
  45  	}
  46  
  47  	// Query for events that mention this pubkey
  48  	// The MENTIONS relationship is created by SaveEvent when processing p-tags
  49  	cypher := fmt.Sprintf(`
  50  		MATCH (e:Event)-[:MENTIONS]->(u:NostrUser {pubkey: $pubkey})
  51  		WHERE true %s
  52  		RETURN e.id AS event_id
  53  		ORDER BY e.created_at DESC
  54  	`, kindsFilter)
  55  
  56  	queryResult, err := n.ExecuteRead(ctx, cypher, params)
  57  	if err != nil {
  58  		return result, fmt.Errorf("failed to query mentions: %w", err)
  59  	}
  60  
  61  	// Add all found events at depth 1
  62  	for queryResult.Next(ctx) {
  63  		record := queryResult.Record()
  64  		eventID, ok := record.Values[0].(string)
  65  		if !ok || eventID == "" {
  66  			continue
  67  		}
  68  
  69  		// Normalize to lowercase for consistency
  70  		eventID = strings.ToLower(eventID)
  71  		result.AddEventAtDepth(eventID, 1)
  72  	}
  73  
  74  	n.Logger.Debugf("FindMentions: found %d events mentioning pubkey %s", result.TotalEvents, safePrefix(pubkeyHex, 16))
  75  
  76  	return result, nil
  77  }
  78  
  79  // FindMentionsFromHex is a convenience wrapper that accepts hex-encoded pubkey.
  80  func (n *N) FindMentionsFromHex(pubkeyHex string, kinds []uint16) (*GraphResult, error) {
  81  	pubkey, err := hex.Dec(pubkeyHex)
  82  	if err != nil {
  83  		return nil, err
  84  	}
  85  	result, err := n.FindMentions(pubkey, kinds)
  86  	if err != nil {
  87  		return nil, err
  88  	}
  89  	return result.(*GraphResult), nil
  90  }
  91  
  92  // FindMentionsByPubkeys returns events that mention any of the given pubkeys.
  93  // Useful for finding mentions across a set of followed accounts.
  94  func (n *N) FindMentionsByPubkeys(pubkeys []string, kinds []uint16) (*GraphResult, error) {
  95  	result := NewGraphResult()
  96  
  97  	if len(pubkeys) == 0 {
  98  		return result, nil
  99  	}
 100  
 101  	ctx := context.Background()
 102  
 103  	// Build kinds filter if specified
 104  	var kindsFilter string
 105  	params := map[string]any{
 106  		"pubkeys": pubkeys,
 107  	}
 108  
 109  	if len(kinds) > 0 {
 110  		kindsInt := make([]int64, len(kinds))
 111  		for i, k := range kinds {
 112  			kindsInt[i] = int64(k)
 113  		}
 114  		params["kinds"] = kindsInt
 115  		kindsFilter = "AND e.kind IN $kinds"
 116  	}
 117  
 118  	// Query for events that mention any of the pubkeys
 119  	cypher := fmt.Sprintf(`
 120  		MATCH (e:Event)-[:MENTIONS]->(u:NostrUser)
 121  		WHERE u.pubkey IN $pubkeys %s
 122  		RETURN DISTINCT e.id AS event_id
 123  		ORDER BY e.created_at DESC
 124  	`, kindsFilter)
 125  
 126  	queryResult, err := n.ExecuteRead(ctx, cypher, params)
 127  	if err != nil {
 128  		return result, fmt.Errorf("failed to query mentions: %w", err)
 129  	}
 130  
 131  	for queryResult.Next(ctx) {
 132  		record := queryResult.Record()
 133  		eventID, ok := record.Values[0].(string)
 134  		if !ok || eventID == "" {
 135  			continue
 136  		}
 137  
 138  		eventID = strings.ToLower(eventID)
 139  		result.AddEventAtDepth(eventID, 1)
 140  	}
 141  
 142  	return result, nil
 143  }
 144