summaryrefslogtreecommitdiffstats
path: root/internal/storage/deletions.go
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-13 19:12:28 -0800
committerbndw <ben@bdw.to>2026-02-13 19:12:28 -0800
commit656748ea286ff7eac6cbe1b241ad31212892ba61 (patch)
treee9685b4a585809463bdf51a4d1ecb7f7c5efaf70 /internal/storage/deletions.go
parent83876eae868bd1e4fb6b9a823a6e8173919f290d (diff)
feat: implement NIP-09 (deletions) and NIP-11 (relay info)
NIP-11 (Relay Information Document): - Serves relay metadata at GET / with Accept: application/nostr+json - Returns name, description, supported NIPs, limitations - CORS headers for browser compatibility NIP-09 (Event Deletion): - Kind 5 events delete events referenced in 'e' tags - Only authors can delete their own events - Soft delete (marks deleted=1) - Records deletion in deletions table - Works across all protocols (gRPC, Connect, WebSocket) Fixed deletions schema: - deleted_event_id as PRIMARY KEY (not deletion_event_id) - Allows one deletion event to delete multiple events 3 new tests, 44 total tests passing Supported NIPs now: 1, 9, 11
Diffstat (limited to 'internal/storage/deletions.go')
-rw-r--r--internal/storage/deletions.go70
1 files changed, 70 insertions, 0 deletions
diff --git a/internal/storage/deletions.go b/internal/storage/deletions.go
new file mode 100644
index 0000000..1a07e3c
--- /dev/null
+++ b/internal/storage/deletions.go
@@ -0,0 +1,70 @@
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}