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