summaryrefslogtreecommitdiffstats
path: root/internal/storage
diff options
context:
space:
mode:
Diffstat (limited to 'internal/storage')
-rw-r--r--internal/storage/deletions.go67
-rw-r--r--internal/storage/deletions_test.go224
2 files changed, 291 insertions, 0 deletions
diff --git a/internal/storage/deletions.go b/internal/storage/deletions.go
new file mode 100644
index 0000000..c16b96b
--- /dev/null
+++ b/internal/storage/deletions.go
@@ -0,0 +1,67 @@
1package storage
2
3import (
4 "context"
5 "fmt"
6
7 pb "northwest.io/nostr-grpc/api/nostr/v1"
8)
9
10const KindDeletion = 5
11
12// ProcessDeletion handles kind 5 deletion events by hard deleting the referenced events.
13// Only events authored by the same pubkey as the deletion event can be deleted.
14func (s *Storage) ProcessDeletion(ctx context.Context, deletionEvent *pb.Event) error {
15 if deletionEvent.Kind != KindDeletion {
16 return fmt.Errorf("not a deletion event")
17 }
18
19 // Extract event IDs to delete from "e" tags
20 var eventIDsToDelete []string
21 for _, tag := range deletionEvent.Tags {
22 if len(tag.Values) >= 2 && tag.Values[0] == "e" {
23 eventIDsToDelete = append(eventIDsToDelete, tag.Values[1])
24 }
25 }
26
27 if len(eventIDsToDelete) == 0 {
28 return nil
29 }
30
31 tx, err := s.db.BeginTx(ctx, nil)
32 if err != nil {
33 return fmt.Errorf("failed to begin transaction: %w", err)
34 }
35 defer tx.Rollback()
36
37 // Hard delete each referenced event (only if authored by same pubkey)
38 for _, eventID := range eventIDsToDelete {
39 // Verify the event exists and was authored by the same pubkey
40 var eventPubkey string
41 err := tx.QueryRowContext(ctx,
42 "SELECT pubkey FROM events WHERE id = ?",
43 eventID,
44 ).Scan(&eventPubkey)
45
46 if err != nil {
47 // Event doesn't exist, skip
48 continue
49 }
50
51 // Only delete if pubkeys match
52 if eventPubkey != deletionEvent.Pubkey {
53 continue
54 }
55
56 // Hard delete the event
57 _, err = tx.ExecContext(ctx,
58 "DELETE FROM events WHERE id = ?",
59 eventID,
60 )
61 if err != nil {
62 return fmt.Errorf("failed to delete event: %w", err)
63 }
64 }
65
66 return tx.Commit()
67}
diff --git a/internal/storage/deletions_test.go b/internal/storage/deletions_test.go
new file mode 100644
index 0000000..71fa477
--- /dev/null
+++ b/internal/storage/deletions_test.go
@@ -0,0 +1,224 @@
1package storage
2
3import (
4 "context"
5 "fmt"
6 "testing"
7 "time"
8
9 pb "northwest.io/nostr-grpc/api/nostr/v1"
10)
11
12func TestProcessDeletion(t *testing.T) {
13 store, err := New(":memory:")
14 if err != nil {
15 t.Fatalf("failed to create storage: %v", err)
16 }
17 defer store.Close()
18
19 ctx := context.Background()
20
21 // Store an event
22 event := &pb.Event{
23 Id: "event123",
24 Pubkey: "alice",
25 CreatedAt: time.Now().Unix(),
26 Kind: 1,
27 Tags: []*pb.Tag{},
28 Content: "to be deleted",
29 Sig: "sig1",
30 }
31
32 err = store.StoreEvent(ctx, &EventData{
33 Event: event,
34 CanonicalJSON: []byte(`[0,"alice",1234567890,1,[],"to be deleted"]`),
35 })
36 if err != nil {
37 t.Fatalf("failed to store event: %v", err)
38 }
39
40 // Verify event exists
41 retrieved, err := store.GetEvent(ctx, "event123")
42 if err != nil {
43 t.Fatalf("event should exist before deletion: %v", err)
44 }
45 if retrieved.Id != "event123" {
46 t.Errorf("expected event123, got %s", retrieved.Id)
47 }
48
49 // Process deletion
50 deletionEvent := &pb.Event{
51 Id: "deletion123",
52 Pubkey: "alice",
53 CreatedAt: time.Now().Unix(),
54 Kind: KindDeletion,
55 Tags: []*pb.Tag{
56 {Values: []string{"e", "event123"}},
57 },
58 Content: "deleting my event",
59 Sig: "sig2",
60 }
61
62 err = store.ProcessDeletion(ctx, deletionEvent)
63 if err != nil {
64 t.Fatalf("failed to process deletion: %v", err)
65 }
66
67 // Verify event was hard deleted
68 _, err = store.GetEvent(ctx, "event123")
69 if err != ErrEventNotFound {
70 t.Errorf("event should be deleted, got error: %v", err)
71 }
72}
73
74func TestProcessDeletionWrongAuthor(t *testing.T) {
75 store, err := New(":memory:")
76 if err != nil {
77 t.Fatalf("failed to create storage: %v", err)
78 }
79 defer store.Close()
80
81 ctx := context.Background()
82
83 // Store an event from alice
84 event := &pb.Event{
85 Id: "event456",
86 Pubkey: "alice",
87 CreatedAt: time.Now().Unix(),
88 Kind: 1,
89 Tags: []*pb.Tag{},
90 Content: "alice's event",
91 Sig: "sig1",
92 }
93
94 err = store.StoreEvent(ctx, &EventData{
95 Event: event,
96 CanonicalJSON: []byte(`[0,"alice",1234567890,1,[],"alice's event"]`),
97 })
98 if err != nil {
99 t.Fatalf("failed to store event: %v", err)
100 }
101
102 // Try to delete with bob's pubkey
103 deletionEvent := &pb.Event{
104 Id: "deletion456",
105 Pubkey: "bob",
106 CreatedAt: time.Now().Unix(),
107 Kind: KindDeletion,
108 Tags: []*pb.Tag{
109 {Values: []string{"e", "event456"}},
110 },
111 Content: "trying to delete alice's event",
112 Sig: "sig2",
113 }
114
115 err = store.ProcessDeletion(ctx, deletionEvent)
116 if err != nil {
117 t.Fatalf("process deletion should succeed but not delete: %v", err)
118 }
119
120 // Verify event still exists (bob can't delete alice's events)
121 retrieved, err := store.GetEvent(ctx, "event456")
122 if err != nil {
123 t.Fatalf("event should still exist: %v", err)
124 }
125 if retrieved.Id != "event456" {
126 t.Errorf("expected event456, got %s", retrieved.Id)
127 }
128}
129
130func TestProcessDeletionMultipleEvents(t *testing.T) {
131 store, err := New(":memory:")
132 if err != nil {
133 t.Fatalf("failed to create storage: %v", err)
134 }
135 defer store.Close()
136
137 ctx := context.Background()
138
139 // Store 3 events from alice
140 for i := 1; i <= 3; i++ {
141 event := &pb.Event{
142 Id: fmt.Sprintf("event%d", i),
143 Pubkey: "alice",
144 CreatedAt: time.Now().Unix(),
145 Kind: 1,
146 Tags: []*pb.Tag{},
147 Content: fmt.Sprintf("event %d", i),
148 Sig: fmt.Sprintf("sig%d", i),
149 }
150
151 err = store.StoreEvent(ctx, &EventData{
152 Event: event,
153 CanonicalJSON: []byte(fmt.Sprintf(`[0,"alice",1234567890,1,[],"event %d"]`, i)),
154 })
155 if err != nil {
156 t.Fatalf("failed to store event: %v", err)
157 }
158 }
159
160 // Delete event1 and event2
161 deletionEvent := &pb.Event{
162 Id: "deletion789",
163 Pubkey: "alice",
164 CreatedAt: time.Now().Unix(),
165 Kind: KindDeletion,
166 Tags: []*pb.Tag{
167 {Values: []string{"e", "event1"}},
168 {Values: []string{"e", "event2"}},
169 },
170 Content: "deleting multiple events",
171 Sig: "sig_del",
172 }
173
174 err = store.ProcessDeletion(ctx, deletionEvent)
175 if err != nil {
176 t.Fatalf("failed to process deletion: %v", err)
177 }
178
179 // Verify event1 and event2 are deleted
180 _, err = store.GetEvent(ctx, "event1")
181 if err != ErrEventNotFound {
182 t.Error("event1 should be deleted")
183 }
184
185 _, err = store.GetEvent(ctx, "event2")
186 if err != ErrEventNotFound {
187 t.Error("event2 should be deleted")
188 }
189
190 // Verify event3 still exists
191 _, err = store.GetEvent(ctx, "event3")
192 if err != nil {
193 t.Error("event3 should still exist")
194 }
195}
196
197func TestProcessDeletionNonExistentEvent(t *testing.T) {
198 store, err := New(":memory:")
199 if err != nil {
200 t.Fatalf("failed to create storage: %v", err)
201 }
202 defer store.Close()
203
204 ctx := context.Background()
205
206 // Try to delete an event that doesn't exist
207 deletionEvent := &pb.Event{
208 Id: "deletion999",
209 Pubkey: "alice",
210 CreatedAt: time.Now().Unix(),
211 Kind: KindDeletion,
212 Tags: []*pb.Tag{
213 {Values: []string{"e", "nonexistent"}},
214 },
215 Content: "deleting nonexistent event",
216 Sig: "sig_del",
217 }
218
219 // Should succeed without error (no-op)
220 err = store.ProcessDeletion(ctx, deletionEvent)
221 if err != nil {
222 t.Fatalf("process deletion should succeed: %v", err)
223 }
224}