delete.go raw

   1  package neo4j
   2  
   3  import (
   4  	"context"
   5  	"fmt"
   6  	"time"
   7  
   8  	"next.orly.dev/pkg/nostr/encoders/event"
   9  	"next.orly.dev/pkg/nostr/encoders/hex"
  10  	"next.orly.dev/pkg/database/indexes/types"
  11  )
  12  
  13  // DeleteEvent deletes an event by its ID
  14  func (n *N) DeleteEvent(c context.Context, eid []byte) error {
  15  	idStr := hex.Enc(eid)
  16  
  17  	cypher := "MATCH (e:Event {id: $id}) DETACH DELETE e"
  18  	params := map[string]any{"id": idStr}
  19  
  20  	_, err := n.ExecuteWrite(c, cypher, params)
  21  	if err != nil {
  22  		return fmt.Errorf("failed to delete event: %w", err)
  23  	}
  24  
  25  	return nil
  26  }
  27  
  28  // DeleteEventBySerial deletes an event by its serial number
  29  func (n *N) DeleteEventBySerial(c context.Context, ser *types.Uint40, ev *event.E) error {
  30  	serial := ser.Get()
  31  
  32  	cypher := "MATCH (e:Event {serial: $serial}) DETACH DELETE e"
  33  	params := map[string]any{"serial": int64(serial)}
  34  
  35  	_, err := n.ExecuteWrite(c, cypher, params)
  36  	if err != nil {
  37  		return fmt.Errorf("failed to delete event: %w", err)
  38  	}
  39  
  40  	return nil
  41  }
  42  
  43  // DeleteExpired deletes expired events based on NIP-40 expiration tags
  44  // Events with an expiration property > 0 and <= current time are deleted
  45  func (n *N) DeleteExpired() {
  46  	ctx := context.Background()
  47  	now := time.Now().Unix()
  48  
  49  	// Query for expired events (expiration > 0 means it has an expiration, and <= now means it's expired)
  50  	cypher := `
  51  MATCH (e:Event)
  52  WHERE e.expiration > 0 AND e.expiration <= $now
  53  RETURN e.serial AS serial, e.id AS id
  54  LIMIT 1000`
  55  
  56  	params := map[string]any{"now": now}
  57  
  58  	result, err := n.ExecuteRead(ctx, cypher, params)
  59  	if err != nil {
  60  		n.Logger.Warningf("failed to query expired events: %v", err)
  61  		return
  62  	}
  63  
  64  	// Collect serials to delete
  65  	var deleteCount int
  66  	for result.Next(ctx) {
  67  		record := result.Record()
  68  		if record == nil {
  69  			continue
  70  		}
  71  
  72  		idRaw, found := record.Get("id")
  73  		if !found {
  74  			continue
  75  		}
  76  
  77  		idStr, ok := idRaw.(string)
  78  		if !ok {
  79  			continue
  80  		}
  81  
  82  		// Delete the expired event
  83  		deleteCypher := "MATCH (e:Event {id: $id}) DETACH DELETE e"
  84  		deleteParams := map[string]any{"id": idStr}
  85  
  86  		if _, err := n.ExecuteWrite(ctx, deleteCypher, deleteParams); err != nil {
  87  			n.Logger.Warningf("failed to delete expired event %s: %v", safePrefix(idStr, 16), err)
  88  			continue
  89  		}
  90  
  91  		deleteCount++
  92  	}
  93  
  94  	if deleteCount > 0 {
  95  		n.Logger.Infof("deleted %d expired events", deleteCount)
  96  	}
  97  }
  98  
  99  // ProcessDelete processes a kind 5 deletion event
 100  func (n *N) ProcessDelete(ev *event.E, admins [][]byte) error {
 101  	// Deletion events (kind 5) can delete events by the same author
 102  	// or by relay admins
 103  
 104  	// Check if this is a kind 5 event
 105  	if ev.Kind != 5 {
 106  		return fmt.Errorf("not a deletion event")
 107  	}
 108  
 109  	// Get all 'e' tags (event IDs to delete)
 110  	eTags := ev.Tags.GetAll([]byte{'e'})
 111  	if len(eTags) == 0 {
 112  		return nil // Nothing to delete
 113  	}
 114  
 115  	ctx := context.Background()
 116  	isAdmin := false
 117  
 118  	// Check if author is an admin
 119  	for _, adminPk := range admins {
 120  		if string(ev.Pubkey) == string(adminPk) {
 121  			isAdmin = true
 122  			break
 123  		}
 124  	}
 125  
 126  	// For each event ID in e-tags, delete it if allowed
 127  	for _, eTag := range eTags {
 128  		if len(eTag.T) < 2 {
 129  			continue
 130  		}
 131  
 132  		// Use ValueHex() to correctly handle both binary and hex storage formats
 133  		eventIDStr := string(eTag.ValueHex())
 134  		eventID, err := hex.Dec(eventIDStr)
 135  		if err != nil {
 136  			continue
 137  		}
 138  
 139  		// Fetch the event to check authorship
 140  		cypher := "MATCH (e:Event {id: $id}) RETURN e.pubkey AS pubkey"
 141  		params := map[string]any{"id": eventIDStr}
 142  
 143  		result, err := n.ExecuteRead(ctx, cypher, params)
 144  		if err != nil {
 145  			continue
 146  		}
 147  
 148  		if result.Next(ctx) {
 149  			record := result.Record()
 150  			if record != nil {
 151  				pubkeyValue, found := record.Get("pubkey")
 152  				if found {
 153  					if pubkeyStr, ok := pubkeyValue.(string); ok {
 154  						pubkey, err := hex.Dec(pubkeyStr)
 155  						if err != nil {
 156  							continue
 157  						}
 158  
 159  						// Check if deletion is allowed (same author or admin)
 160  						canDelete := isAdmin || string(ev.Pubkey) == string(pubkey)
 161  						if canDelete {
 162  							// Delete the event
 163  							if err := n.DeleteEvent(ctx, eventID); err != nil {
 164  								n.Logger.Warningf("failed to delete event %s: %v", eventIDStr, err)
 165  							}
 166  						}
 167  					}
 168  				}
 169  			}
 170  		}
 171  	}
 172  
 173  	return nil
 174  }
 175  
 176  // CheckForDeleted checks if an event has been deleted
 177  func (n *N) CheckForDeleted(ev *event.E, admins [][]byte) error {
 178  	// Query for kind 5 events that reference this event via Tag nodes
 179  	ctx := context.Background()
 180  	idStr := hex.Enc(ev.ID[:])
 181  
 182  	// Build cypher query to find deletion events
 183  	// Traverses through Tag nodes: Event-[:TAGGED_WITH]->Tag-[:REFERENCES]->Event
 184  	cypher := `
 185  MATCH (target:Event {id: $targetId})
 186  MATCH (delete:Event {kind: 5})-[:TAGGED_WITH]->(t:Tag {type: 'e'})-[:REFERENCES]->(target)
 187  WHERE delete.pubkey = $pubkey OR delete.pubkey IN $admins
 188  RETURN delete.id AS id
 189  LIMIT 1`
 190  
 191  	adminPubkeys := make([]string, len(admins))
 192  	for i, admin := range admins {
 193  		adminPubkeys[i] = hex.Enc(admin)
 194  	}
 195  
 196  	params := map[string]any{
 197  		"targetId": idStr,
 198  		"pubkey":   hex.Enc(ev.Pubkey[:]),
 199  		"admins":   adminPubkeys,
 200  	}
 201  
 202  	result, err := n.ExecuteRead(ctx, cypher, params)
 203  	if err != nil {
 204  		return nil // Not deleted
 205  	}
 206  
 207  	if result.Next(ctx) {
 208  		return fmt.Errorf("event has been deleted")
 209  	}
 210  
 211  	return nil
 212  }
 213