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