From dfa19ff0776be0850ad7b86ca579601431349593 Mon Sep 17 00:00:00 2001 From: bndw Date: Fri, 13 Feb 2026 20:38:59 -0800 Subject: feat: implement NIP-09 with hard delete Implement event deletion (NIP-09) using hard delete approach: - Kind 5 events trigger deletion but are not stored themselves - ProcessDeletion hard deletes referenced events (DELETE FROM events) - Only authors can delete their own events (pubkey verification) - Support multiple event IDs in single deletion request - No deletions table needed (simpler schema) - Added 4 deletion tests covering various scenarios - All 45 tests passing --- internal/storage/deletions.go | 67 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 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..c16b96b --- /dev/null +++ b/internal/storage/deletions.go @@ -0,0 +1,67 @@ +package storage + +import ( + "context" + "fmt" + + pb "northwest.io/nostr-grpc/api/nostr/v1" +) + +const KindDeletion = 5 + +// ProcessDeletion handles kind 5 deletion events by hard deleting the referenced events. +// Only events authored by the same pubkey as the deletion event can be deleted. +func (s *Storage) ProcessDeletion(ctx context.Context, deletionEvent *pb.Event) error { + if deletionEvent.Kind != KindDeletion { + return fmt.Errorf("not a deletion event") + } + + // Extract event IDs to delete from "e" tags + 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() + + // Hard delete each referenced event (only if authored by same pubkey) + for _, eventID := range eventIDsToDelete { + // Verify the event exists and was authored by the same pubkey + var eventPubkey string + err := tx.QueryRowContext(ctx, + "SELECT pubkey FROM events WHERE id = ?", + eventID, + ).Scan(&eventPubkey) + + if err != nil { + // Event doesn't exist, skip + continue + } + + // Only delete if pubkeys match + if eventPubkey != deletionEvent.Pubkey { + continue + } + + // Hard delete the event + _, err = tx.ExecContext(ctx, + "DELETE FROM events WHERE id = ?", + eventID, + ) + if err != nil { + return fmt.Errorf("failed to delete event: %w", err) + } + } + + return tx.Commit() +} -- cgit v1.2.3