Files
next.orly.dev/pkg/neo4j/delete.go
mleku 96bdf5cba2
Some checks failed
Go / build-and-release (push) Has been cancelled
Implement Tag-based e/p model for Neo4j backend (v0.36.0)
- Add unified Tag-based model where e/p tags create intermediate Tag nodes
  with REFERENCES relationships to Event/NostrUser nodes
- Update save-event.go: addPTagsInBatches and addETagsInBatches now create
  Tag nodes with TAGGED_WITH and REFERENCES relationships
- Update delete.go: CheckForDeleted uses Tag traversal for kind 5 detection
- Add v3 migration in migrations.go to convert existing direct REFERENCES
  and MENTIONS relationships to the new Tag-based model
- Create comprehensive test file tag_model_test.go with 15+ test functions
  covering Tag model, filter queries, migrations, and deletion detection
- Update save-event_test.go to verify new Tag-based relationship patterns
- Update WOT_SPEC.md with Tag-Based References documentation section
- Update CLAUDE.md and README.md with Neo4j Tag-based model documentation
- Bump version to v0.36.0

This change enables #e and #p filter queries to work correctly by storing
all tags (including e/p) through intermediate Tag nodes.

Files modified:
- pkg/neo4j/save-event.go: Tag-based e/p relationship creation
- pkg/neo4j/delete.go: Tag traversal for deletion detection
- pkg/neo4j/migrations.go: v3 migration for existing data
- pkg/neo4j/tag_model_test.go: New comprehensive test file
- pkg/neo4j/save-event_test.go: Updated for new model
- pkg/neo4j/WOT_SPEC.md: Tag-Based References documentation
- pkg/neo4j/README.md: Architecture and example queries
- CLAUDE.md: Repository documentation update
- pkg/version/version: Bump to v0.36.0

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 09:22:05 +01:00

213 lines
5.1 KiB
Go

package neo4j
import (
"context"
"fmt"
"time"
"git.mleku.dev/mleku/nostr/encoders/event"
"git.mleku.dev/mleku/nostr/encoders/hex"
"next.orly.dev/pkg/database/indexes/types"
)
// DeleteEvent deletes an event by its ID
func (n *N) DeleteEvent(c context.Context, eid []byte) error {
idStr := hex.Enc(eid)
cypher := "MATCH (e:Event {id: $id}) DETACH DELETE e"
params := map[string]any{"id": idStr}
_, err := n.ExecuteWrite(c, cypher, params)
if err != nil {
return fmt.Errorf("failed to delete event: %w", err)
}
return nil
}
// DeleteEventBySerial deletes an event by its serial number
func (n *N) DeleteEventBySerial(c context.Context, ser *types.Uint40, ev *event.E) error {
serial := ser.Get()
cypher := "MATCH (e:Event {serial: $serial}) DETACH DELETE e"
params := map[string]any{"serial": int64(serial)}
_, err := n.ExecuteWrite(c, cypher, params)
if err != nil {
return fmt.Errorf("failed to delete event: %w", err)
}
return nil
}
// DeleteExpired deletes expired events based on NIP-40 expiration tags
// Events with an expiration property > 0 and <= current time are deleted
func (n *N) DeleteExpired() {
ctx := context.Background()
now := time.Now().Unix()
// Query for expired events (expiration > 0 means it has an expiration, and <= now means it's expired)
cypher := `
MATCH (e:Event)
WHERE e.expiration > 0 AND e.expiration <= $now
RETURN e.serial AS serial, e.id AS id
LIMIT 1000`
params := map[string]any{"now": now}
result, err := n.ExecuteRead(ctx, cypher, params)
if err != nil {
n.Logger.Warningf("failed to query expired events: %v", err)
return
}
// Collect serials to delete
var deleteCount int
for result.Next(ctx) {
record := result.Record()
if record == nil {
continue
}
idRaw, found := record.Get("id")
if !found {
continue
}
idStr, ok := idRaw.(string)
if !ok {
continue
}
// Delete the expired event
deleteCypher := "MATCH (e:Event {id: $id}) DETACH DELETE e"
deleteParams := map[string]any{"id": idStr}
if _, err := n.ExecuteWrite(ctx, deleteCypher, deleteParams); err != nil {
n.Logger.Warningf("failed to delete expired event %s: %v", safePrefix(idStr, 16), err)
continue
}
deleteCount++
}
if deleteCount > 0 {
n.Logger.Infof("deleted %d expired events", deleteCount)
}
}
// ProcessDelete processes a kind 5 deletion event
func (n *N) ProcessDelete(ev *event.E, admins [][]byte) error {
// Deletion events (kind 5) can delete events by the same author
// or by relay admins
// Check if this is a kind 5 event
if ev.Kind != 5 {
return fmt.Errorf("not a deletion event")
}
// Get all 'e' tags (event IDs to delete)
eTags := ev.Tags.GetAll([]byte{'e'})
if len(eTags) == 0 {
return nil // Nothing to delete
}
ctx := context.Background()
isAdmin := false
// Check if author is an admin
for _, adminPk := range admins {
if string(ev.Pubkey) == string(adminPk) {
isAdmin = true
break
}
}
// For each event ID in e-tags, delete it if allowed
for _, eTag := range eTags {
if len(eTag.T) < 2 {
continue
}
// Use ValueHex() to correctly handle both binary and hex storage formats
eventIDStr := string(eTag.ValueHex())
eventID, err := hex.Dec(eventIDStr)
if err != nil {
continue
}
// Fetch the event to check authorship
cypher := "MATCH (e:Event {id: $id}) RETURN e.pubkey AS pubkey"
params := map[string]any{"id": eventIDStr}
result, err := n.ExecuteRead(ctx, cypher, params)
if err != nil {
continue
}
if result.Next(ctx) {
record := result.Record()
if record != nil {
pubkeyValue, found := record.Get("pubkey")
if found {
if pubkeyStr, ok := pubkeyValue.(string); ok {
pubkey, err := hex.Dec(pubkeyStr)
if err != nil {
continue
}
// Check if deletion is allowed (same author or admin)
canDelete := isAdmin || string(ev.Pubkey) == string(pubkey)
if canDelete {
// Delete the event
if err := n.DeleteEvent(ctx, eventID); err != nil {
n.Logger.Warningf("failed to delete event %s: %v", eventIDStr, err)
}
}
}
}
}
}
}
return nil
}
// CheckForDeleted checks if an event has been deleted
func (n *N) CheckForDeleted(ev *event.E, admins [][]byte) error {
// Query for kind 5 events that reference this event via Tag nodes
ctx := context.Background()
idStr := hex.Enc(ev.ID[:])
// Build cypher query to find deletion events
// Traverses through Tag nodes: Event-[:TAGGED_WITH]->Tag-[:REFERENCES]->Event
cypher := `
MATCH (target:Event {id: $targetId})
MATCH (delete:Event {kind: 5})-[:TAGGED_WITH]->(t:Tag {type: 'e'})-[:REFERENCES]->(target)
WHERE delete.pubkey = $pubkey OR delete.pubkey IN $admins
RETURN delete.id AS id
LIMIT 1`
adminPubkeys := make([]string, len(admins))
for i, admin := range admins {
adminPubkeys[i] = hex.Enc(admin)
}
params := map[string]any{
"targetId": idStr,
"pubkey": hex.Enc(ev.Pubkey[:]),
"admins": adminPubkeys,
}
result, err := n.ExecuteRead(ctx, cypher, params)
if err != nil {
return nil // Not deleted
}
if result.Next(ctx) {
return fmt.Errorf("event has been deleted")
}
return nil
}