summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-13 20:36:12 -0800
committerbndw <ben@bdw.to>2026-02-13 20:36:12 -0800
commit89b8948195f24df127b7ae656ab3f60bd1b49ac7 (patch)
tree2c6fc91e053039e4ad53ee0a72038b1b99f8c50c
parent656748ea286ff7eac6cbe1b241ad31212892ba61 (diff)
refactor: simplify deletion handling (remove NIP-09 processing)
Remove deletion processing logic in favor of simpler approach: - Remove deletions table from schema - Delete deletions.go and deletions_test.go - Remove ProcessDeletion from EventStore interface - Kind 5 events now stored like any other event (no special handling) - Update storage test to expect 2 tables instead of 3 - All 41 tests passing
-rw-r--r--README.md1
-rw-r--r--internal/handler/grpc/server.go11
-rw-r--r--internal/handler/websocket/handler.go8
-rw-r--r--internal/storage/deletions.go70
-rw-r--r--internal/storage/deletions_test.go184
-rw-r--r--internal/storage/storage.go12
-rw-r--r--internal/storage/storage_test.go6
7 files changed, 3 insertions, 289 deletions
diff --git a/README.md b/README.md
index d2fbd41..61fa0d4 100644
--- a/README.md
+++ b/README.md
@@ -100,7 +100,6 @@ See [proto/nostr/v1/nostr.proto](proto/nostr/v1/nostr.proto) for the full API.
100 - **WebSocket** (NIP-01 - standard Nostr protocol) 100 - **WebSocket** (NIP-01 - standard Nostr protocol)
101- ✅ Subscribe/streaming (real-time event delivery) 101- ✅ Subscribe/streaming (real-time event delivery)
102- ✅ Subscription management (filter matching, fan-out) 102- ✅ Subscription management (filter matching, fan-out)
103- ✅ **NIP-09** - Event deletion (authors can delete their own events)
104- ✅ **NIP-11** - Relay info document (GET with `Accept: application/nostr+json`) 103- ✅ **NIP-11** - Relay info document (GET with `Accept: application/nostr+json`)
105 104
106**Compatible with:** 105**Compatible with:**
diff --git a/internal/handler/grpc/server.go b/internal/handler/grpc/server.go
index b1ffd96..b65b527 100644
--- a/internal/handler/grpc/server.go
+++ b/internal/handler/grpc/server.go
@@ -13,7 +13,6 @@ import (
13type EventStore interface { 13type EventStore interface {
14 StoreEvent(context.Context, *storage.EventData) error 14 StoreEvent(context.Context, *storage.EventData) error
15 QueryEvents(context.Context, []*pb.Filter, *storage.QueryOptions) ([]*pb.Event, error) 15 QueryEvents(context.Context, []*pb.Filter, *storage.QueryOptions) ([]*pb.Event, error)
16 ProcessDeletion(context.Context, *pb.Event) error
17} 16}
18 17
19type Server struct { 18type Server struct {
@@ -76,16 +75,6 @@ func (s *Server) PublishEvent(ctx context.Context, req *pb.PublishEventRequest)
76 return nil, fmt.Errorf("failed to store event: %w", err) 75 return nil, fmt.Errorf("failed to store event: %w", err)
77 } 76 }
78 77
79 if req.Event.Kind == 5 {
80 if err := s.store.ProcessDeletion(ctx, req.Event); err != nil {
81 return &pb.PublishEventResponse{
82 Accepted: false,
83 Message: fmt.Sprintf("deletion processing failed: %v", err),
84 CanonicalJson: canonicalJSON,
85 }, nil
86 }
87 }
88
89 s.subs.MatchAndFan(req.Event) 78 s.subs.MatchAndFan(req.Event)
90 79
91 return &pb.PublishEventResponse{ 80 return &pb.PublishEventResponse{
diff --git a/internal/handler/websocket/handler.go b/internal/handler/websocket/handler.go
index 4a7db0d..38d4fa6 100644
--- a/internal/handler/websocket/handler.go
+++ b/internal/handler/websocket/handler.go
@@ -17,7 +17,6 @@ import (
17type EventStore interface { 17type EventStore interface {
18 StoreEvent(context.Context, *storage.EventData) error 18 StoreEvent(context.Context, *storage.EventData) error
19 QueryEvents(context.Context, []*pb.Filter, *storage.QueryOptions) ([]*pb.Event, error) 19 QueryEvents(context.Context, []*pb.Filter, *storage.QueryOptions) ([]*pb.Event, error)
20 ProcessDeletion(context.Context, *pb.Event) error
21} 20}
22 21
23type Handler struct { 22type Handler struct {
@@ -131,13 +130,6 @@ func (h *Handler) handleEvent(ctx context.Context, conn *websocket.Conn, raw []j
131 return nil 130 return nil
132 } 131 }
133 132
134 if pbEvent.Kind == 5 {
135 if err := h.store.ProcessDeletion(ctx, pbEvent); err != nil {
136 h.sendOK(ctx, conn, event.ID, false, fmt.Sprintf("deletion failed: %v", err))
137 return nil
138 }
139 }
140
141 h.subs.MatchAndFan(pbEvent) 133 h.subs.MatchAndFan(pbEvent)
142 134
143 h.sendOK(ctx, conn, event.ID, true, "") 135 h.sendOK(ctx, conn, event.ID, true, "")
diff --git a/internal/storage/deletions.go b/internal/storage/deletions.go
deleted file mode 100644
index 1a07e3c..0000000
--- a/internal/storage/deletions.go
+++ /dev/null
@@ -1,70 +0,0 @@
1package storage
2
3import (
4 "context"
5 "fmt"
6
7 pb "northwest.io/nostr-grpc/api/nostr/v1"
8)
9
10const KindDeletion = 5
11
12func (s *Storage) ProcessDeletion(ctx context.Context, deletionEvent *pb.Event) error {
13 if deletionEvent.Kind != KindDeletion {
14 return fmt.Errorf("not a deletion event")
15 }
16
17 var eventIDsToDelete []string
18 for _, tag := range deletionEvent.Tags {
19 if len(tag.Values) >= 2 && tag.Values[0] == "e" {
20 eventIDsToDelete = append(eventIDsToDelete, tag.Values[1])
21 }
22 }
23
24 if len(eventIDsToDelete) == 0 {
25 return nil
26 }
27
28 tx, err := s.db.BeginTx(ctx, nil)
29 if err != nil {
30 return fmt.Errorf("failed to begin transaction: %w", err)
31 }
32 defer tx.Rollback()
33
34 for _, eventID := range eventIDsToDelete {
35 var eventPubkey string
36 err := tx.QueryRowContext(ctx,
37 "SELECT pubkey FROM events WHERE id = ? AND deleted = 0",
38 eventID,
39 ).Scan(&eventPubkey)
40
41 if err != nil {
42 continue
43 }
44
45 if eventPubkey != deletionEvent.Pubkey {
46 continue
47 }
48
49 _, err = tx.ExecContext(ctx,
50 "UPDATE events SET deleted = 1 WHERE id = ?",
51 eventID,
52 )
53 if err != nil {
54 return fmt.Errorf("failed to mark event as deleted: %w", err)
55 }
56
57 _, err = tx.ExecContext(ctx,
58 "INSERT OR IGNORE INTO deletions (deleted_event_id, deletion_event_id, pubkey, created_at) VALUES (?, ?, ?, ?)",
59 eventID,
60 deletionEvent.Id,
61 deletionEvent.Pubkey,
62 deletionEvent.CreatedAt,
63 )
64 if err != nil {
65 return fmt.Errorf("failed to record deletion: %w", err)
66 }
67 }
68
69 return tx.Commit()
70}
diff --git a/internal/storage/deletions_test.go b/internal/storage/deletions_test.go
deleted file mode 100644
index 47e6547..0000000
--- a/internal/storage/deletions_test.go
+++ /dev/null
@@ -1,184 +0,0 @@
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 event := &pb.Event{
22 Id: "event123",
23 Pubkey: "alice",
24 CreatedAt: time.Now().Unix(),
25 Kind: 1,
26 Tags: []*pb.Tag{},
27 Content: "to be deleted",
28 Sig: "sig1",
29 }
30
31 err = store.StoreEvent(ctx, &EventData{
32 Event: event,
33 CanonicalJSON: []byte(`[0,"alice",1234567890,1,[],"to be deleted"]`),
34 })
35 if err != nil {
36 t.Fatalf("failed to store event: %v", err)
37 }
38
39 retrieved, err := store.GetEvent(ctx, "event123")
40 if err != nil {
41 t.Fatalf("event should exist before deletion: %v", err)
42 }
43 if retrieved.Id != "event123" {
44 t.Errorf("expected event123, got %s", retrieved.Id)
45 }
46
47 deletionEvent := &pb.Event{
48 Id: "deletion123",
49 Pubkey: "alice",
50 CreatedAt: time.Now().Unix(),
51 Kind: KindDeletion,
52 Tags: []*pb.Tag{
53 {Values: []string{"e", "event123"}},
54 },
55 Content: "deleting my event",
56 Sig: "sig2",
57 }
58
59 err = store.ProcessDeletion(ctx, deletionEvent)
60 if err != nil {
61 t.Fatalf("failed to process deletion: %v", err)
62 }
63
64 _, err = store.GetEvent(ctx, "event123")
65 if err != ErrEventNotFound {
66 t.Errorf("event should be deleted, got error: %v", err)
67 }
68}
69
70func TestProcessDeletionWrongAuthor(t *testing.T) {
71 store, err := New(":memory:")
72 if err != nil {
73 t.Fatalf("failed to create storage: %v", err)
74 }
75 defer store.Close()
76
77 ctx := context.Background()
78
79 event := &pb.Event{
80 Id: "event456",
81 Pubkey: "alice",
82 CreatedAt: time.Now().Unix(),
83 Kind: 1,
84 Tags: []*pb.Tag{},
85 Content: "alice's event",
86 Sig: "sig1",
87 }
88
89 err = store.StoreEvent(ctx, &EventData{
90 Event: event,
91 CanonicalJSON: []byte(`[0,"alice",1234567890,1,[],"alice's event"]`),
92 })
93 if err != nil {
94 t.Fatalf("failed to store event: %v", err)
95 }
96
97 deletionEvent := &pb.Event{
98 Id: "deletion456",
99 Pubkey: "bob",
100 CreatedAt: time.Now().Unix(),
101 Kind: KindDeletion,
102 Tags: []*pb.Tag{
103 {Values: []string{"e", "event456"}},
104 },
105 Content: "trying to delete alice's event",
106 Sig: "sig2",
107 }
108
109 err = store.ProcessDeletion(ctx, deletionEvent)
110 if err != nil {
111 t.Fatalf("process deletion should succeed but not delete: %v", err)
112 }
113
114 retrieved, err := store.GetEvent(ctx, "event456")
115 if err != nil {
116 t.Fatalf("event should still exist: %v", err)
117 }
118 if retrieved.Id != "event456" {
119 t.Errorf("expected event456, got %s", retrieved.Id)
120 }
121}
122
123func TestProcessDeletionMultipleEvents(t *testing.T) {
124 store, err := New(":memory:")
125 if err != nil {
126 t.Fatalf("failed to create storage: %v", err)
127 }
128 defer store.Close()
129
130 ctx := context.Background()
131
132 for i := 1; i <= 3; i++ {
133 event := &pb.Event{
134 Id: fmt.Sprintf("event%d", i),
135 Pubkey: "alice",
136 CreatedAt: time.Now().Unix(),
137 Kind: 1,
138 Tags: []*pb.Tag{},
139 Content: fmt.Sprintf("event %d", i),
140 Sig: fmt.Sprintf("sig%d", i),
141 }
142
143 err = store.StoreEvent(ctx, &EventData{
144 Event: event,
145 CanonicalJSON: []byte(fmt.Sprintf(`[0,"alice",1234567890,1,[],"event %d"]`, i)),
146 })
147 if err != nil {
148 t.Fatalf("failed to store event: %v", err)
149 }
150 }
151
152 deletionEvent := &pb.Event{
153 Id: "deletion789",
154 Pubkey: "alice",
155 CreatedAt: time.Now().Unix(),
156 Kind: KindDeletion,
157 Tags: []*pb.Tag{
158 {Values: []string{"e", "event1"}},
159 {Values: []string{"e", "event2"}},
160 },
161 Content: "deleting multiple events",
162 Sig: "sig_del",
163 }
164
165 err = store.ProcessDeletion(ctx, deletionEvent)
166 if err != nil {
167 t.Fatalf("failed to process deletion: %v", err)
168 }
169
170 _, err = store.GetEvent(ctx, "event1")
171 if err != ErrEventNotFound {
172 t.Error("event1 should be deleted")
173 }
174
175 _, err = store.GetEvent(ctx, "event2")
176 if err != ErrEventNotFound {
177 t.Error("event2 should be deleted")
178 }
179
180 _, err = store.GetEvent(ctx, "event3")
181 if err != nil {
182 t.Error("event3 should still exist")
183 }
184}
diff --git a/internal/storage/storage.go b/internal/storage/storage.go
index 806acac..d00d7bf 100644
--- a/internal/storage/storage.go
+++ b/internal/storage/storage.go
@@ -99,18 +99,6 @@ func (s *Storage) initSchema(ctx context.Context) error {
99 ON events(tags) 99 ON events(tags)
100 WHERE deleted = 0; 100 WHERE deleted = 0;
101 101
102 -- Deletion events (NIP-09)
103 CREATE TABLE IF NOT EXISTS deletions (
104 deleted_event_id TEXT PRIMARY KEY, -- ID of event being deleted
105 deletion_event_id TEXT NOT NULL, -- ID of deletion event
106 pubkey TEXT NOT NULL, -- Who requested deletion
107 created_at INTEGER NOT NULL,
108 FOREIGN KEY (deleted_event_id) REFERENCES events(id)
109 ) STRICT;
110
111 CREATE INDEX IF NOT EXISTS idx_deleted_event
112 ON deletions(deleted_event_id);
113
114 -- Replaceable events tracking (NIP-16, NIP-33) 102 -- Replaceable events tracking (NIP-16, NIP-33)
115 CREATE TABLE IF NOT EXISTS replaceable_events ( 103 CREATE TABLE IF NOT EXISTS replaceable_events (
116 kind INTEGER NOT NULL, 104 kind INTEGER NOT NULL,
diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go
index f2fe401..89369f4 100644
--- a/internal/storage/storage_test.go
+++ b/internal/storage/storage_test.go
@@ -19,14 +19,14 @@ func TestNew(t *testing.T) {
19 19
20 // Verify schema was created by checking if tables exist 20 // Verify schema was created by checking if tables exist
21 var count int 21 var count int
22 query := `SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN ('events', 'deletions', 'replaceable_events')` 22 query := `SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN ('events', 'replaceable_events')`
23 err = store.DB().QueryRow(query).Scan(&count) 23 err = store.DB().QueryRow(query).Scan(&count)
24 if err != nil { 24 if err != nil {
25 t.Fatalf("failed to query tables: %v", err) 25 t.Fatalf("failed to query tables: %v", err)
26 } 26 }
27 27
28 if count != 3 { 28 if count != 2 {
29 t.Errorf("expected 3 main tables, got %d", count) 29 t.Errorf("expected 2 main tables, got %d", count)
30 } 30 }
31} 31}
32 32