From 656748ea286ff7eac6cbe1b241ad31212892ba61 Mon Sep 17 00:00:00 2001 From: bndw Date: Fri, 13 Feb 2026 19:12:28 -0800 Subject: 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 --- internal/storage/deletions.go | 70 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 internal/storage/deletions.go (limited to 'internal/storage/deletions.go') 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 @@ +package storage + +import ( + "context" + "fmt" + + pb "northwest.io/nostr-grpc/api/nostr/v1" +) + +const KindDeletion = 5 + +func (s *Storage) ProcessDeletion(ctx context.Context, deletionEvent *pb.Event) error { + if deletionEvent.Kind != KindDeletion { + return fmt.Errorf("not a deletion event") + } + + var eventIDsToDelete []string + for _, tag := range deletionEvent.Tags { + if len(tag.Values) >= 2 && tag.Values[0] == "e" { + eventIDsToDelete = append(eventIDsToDelete, tag.Values[1]) + } + } + + if len(eventIDsToDelete) == 0 { + return nil + } + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Rollback() + + for _, eventID := range eventIDsToDelete { + var eventPubkey string + err := tx.QueryRowContext(ctx, + "SELECT pubkey FROM events WHERE id = ? AND deleted = 0", + eventID, + ).Scan(&eventPubkey) + + if err != nil { + continue + } + + if eventPubkey != deletionEvent.Pubkey { + continue + } + + _, err = tx.ExecContext(ctx, + "UPDATE events SET deleted = 1 WHERE id = ?", + eventID, + ) + if err != nil { + return fmt.Errorf("failed to mark event as deleted: %w", err) + } + + _, err = tx.ExecContext(ctx, + "INSERT OR IGNORE INTO deletions (deleted_event_id, deletion_event_id, pubkey, created_at) VALUES (?, ?, ?, ?)", + eventID, + deletionEvent.Id, + deletionEvent.Pubkey, + deletionEvent.CreatedAt, + ) + if err != nil { + return fmt.Errorf("failed to record deletion: %w", err) + } + } + + return tx.Commit() +} -- cgit v1.2.3