delete.go raw
1 package neo4j
2
3 import (
4 "context"
5 "fmt"
6 "time"
7
8 "next.orly.dev/pkg/nostr/encoders/event"
9 "next.orly.dev/pkg/nostr/encoders/hex"
10 "next.orly.dev/pkg/database/indexes/types"
11 )
12
13 // DeleteEvent deletes an event by its ID
14 func (n *N) DeleteEvent(c context.Context, eid []byte) error {
15 idStr := hex.Enc(eid)
16
17 cypher := "MATCH (e:Event {id: $id}) DETACH DELETE e"
18 params := map[string]any{"id": idStr}
19
20 _, err := n.ExecuteWrite(c, cypher, params)
21 if err != nil {
22 return fmt.Errorf("failed to delete event: %w", err)
23 }
24
25 return nil
26 }
27
28 // DeleteEventBySerial deletes an event by its serial number
29 func (n *N) DeleteEventBySerial(c context.Context, ser *types.Uint40, ev *event.E) error {
30 serial := ser.Get()
31
32 cypher := "MATCH (e:Event {serial: $serial}) DETACH DELETE e"
33 params := map[string]any{"serial": int64(serial)}
34
35 _, err := n.ExecuteWrite(c, cypher, params)
36 if err != nil {
37 return fmt.Errorf("failed to delete event: %w", err)
38 }
39
40 return nil
41 }
42
43 // DeleteExpired deletes expired events based on NIP-40 expiration tags
44 // Events with an expiration property > 0 and <= current time are deleted
45 func (n *N) DeleteExpired() {
46 ctx := context.Background()
47 now := time.Now().Unix()
48
49 // Query for expired events (expiration > 0 means it has an expiration, and <= now means it's expired)
50 cypher := `
51 MATCH (e:Event)
52 WHERE e.expiration > 0 AND e.expiration <= $now
53 RETURN e.serial AS serial, e.id AS id
54 LIMIT 1000`
55
56 params := map[string]any{"now": now}
57
58 result, err := n.ExecuteRead(ctx, cypher, params)
59 if err != nil {
60 n.Logger.Warningf("failed to query expired events: %v", err)
61 return
62 }
63
64 // Collect serials to delete
65 var deleteCount int
66 for result.Next(ctx) {
67 record := result.Record()
68 if record == nil {
69 continue
70 }
71
72 idRaw, found := record.Get("id")
73 if !found {
74 continue
75 }
76
77 idStr, ok := idRaw.(string)
78 if !ok {
79 continue
80 }
81
82 // Delete the expired event
83 deleteCypher := "MATCH (e:Event {id: $id}) DETACH DELETE e"
84 deleteParams := map[string]any{"id": idStr}
85
86 if _, err := n.ExecuteWrite(ctx, deleteCypher, deleteParams); err != nil {
87 n.Logger.Warningf("failed to delete expired event %s: %v", safePrefix(idStr, 16), err)
88 continue
89 }
90
91 deleteCount++
92 }
93
94 if deleteCount > 0 {
95 n.Logger.Infof("deleted %d expired events", deleteCount)
96 }
97 }
98
99 // ProcessDelete processes a kind 5 deletion event
100 func (n *N) ProcessDelete(ev *event.E, admins [][]byte) error {
101 // Deletion events (kind 5) can delete events by the same author
102 // or by relay admins
103
104 // Check if this is a kind 5 event
105 if ev.Kind != 5 {
106 return fmt.Errorf("not a deletion event")
107 }
108
109 // Get all 'e' tags (event IDs to delete)
110 eTags := ev.Tags.GetAll([]byte{'e'})
111 if len(eTags) == 0 {
112 return nil // Nothing to delete
113 }
114
115 ctx := context.Background()
116 isAdmin := false
117
118 // Check if author is an admin
119 for _, adminPk := range admins {
120 if string(ev.Pubkey) == string(adminPk) {
121 isAdmin = true
122 break
123 }
124 }
125
126 // For each event ID in e-tags, delete it if allowed
127 for _, eTag := range eTags {
128 if len(eTag.T) < 2 {
129 continue
130 }
131
132 // Use ValueHex() to correctly handle both binary and hex storage formats
133 eventIDStr := string(eTag.ValueHex())
134 eventID, err := hex.Dec(eventIDStr)
135 if err != nil {
136 continue
137 }
138
139 // Fetch the event to check authorship
140 cypher := "MATCH (e:Event {id: $id}) RETURN e.pubkey AS pubkey"
141 params := map[string]any{"id": eventIDStr}
142
143 result, err := n.ExecuteRead(ctx, cypher, params)
144 if err != nil {
145 continue
146 }
147
148 if result.Next(ctx) {
149 record := result.Record()
150 if record != nil {
151 pubkeyValue, found := record.Get("pubkey")
152 if found {
153 if pubkeyStr, ok := pubkeyValue.(string); ok {
154 pubkey, err := hex.Dec(pubkeyStr)
155 if err != nil {
156 continue
157 }
158
159 // Check if deletion is allowed (same author or admin)
160 canDelete := isAdmin || string(ev.Pubkey) == string(pubkey)
161 if canDelete {
162 // Delete the event
163 if err := n.DeleteEvent(ctx, eventID); err != nil {
164 n.Logger.Warningf("failed to delete event %s: %v", eventIDStr, err)
165 }
166 }
167 }
168 }
169 }
170 }
171 }
172
173 return nil
174 }
175
176 // CheckForDeleted checks if an event has been deleted
177 func (n *N) CheckForDeleted(ev *event.E, admins [][]byte) error {
178 // Query for kind 5 events that reference this event via Tag nodes
179 ctx := context.Background()
180 idStr := hex.Enc(ev.ID[:])
181
182 // Build cypher query to find deletion events
183 // Traverses through Tag nodes: Event-[:TAGGED_WITH]->Tag-[:REFERENCES]->Event
184 cypher := `
185 MATCH (target:Event {id: $targetId})
186 MATCH (delete:Event {kind: 5})-[:TAGGED_WITH]->(t:Tag {type: 'e'})-[:REFERENCES]->(target)
187 WHERE delete.pubkey = $pubkey OR delete.pubkey IN $admins
188 RETURN delete.id AS id
189 LIMIT 1`
190
191 adminPubkeys := make([]string, len(admins))
192 for i, admin := range admins {
193 adminPubkeys[i] = hex.Enc(admin)
194 }
195
196 params := map[string]any{
197 "targetId": idStr,
198 "pubkey": hex.Enc(ev.Pubkey[:]),
199 "admins": adminPubkeys,
200 }
201
202 result, err := n.ExecuteRead(ctx, cypher, params)
203 if err != nil {
204 return nil // Not deleted
205 }
206
207 if result.Next(ctx) {
208 return fmt.Errorf("event has been deleted")
209 }
210
211 return nil
212 }
213