diff --git a/pkg/neo4j/query-events.go b/pkg/neo4j/query-events.go index c6487a8..cca64dc 100644 --- a/pkg/neo4j/query-events.go +++ b/pkg/neo4j/query-events.go @@ -3,12 +3,15 @@ package neo4j import ( "context" "fmt" + "sort" + "strconv" "strings" "time" "git.mleku.dev/mleku/nostr/encoders/event" "git.mleku.dev/mleku/nostr/encoders/filter" "git.mleku.dev/mleku/nostr/encoders/hex" + "git.mleku.dev/mleku/nostr/encoders/kind" "git.mleku.dev/mleku/nostr/encoders/tag" "lol.mleku.dev/log" "next.orly.dev/pkg/database/indexes/types" @@ -41,11 +44,81 @@ func (n *N) QueryEventsWithOptions( } // Parse response - evs, err = n.parseEventsFromResult(result) + allEvents, err := n.parseEventsFromResult(result) if err != nil { return nil, fmt.Errorf("failed to parse events: %w", err) } + // Filter replaceable events to only return the latest version + // unless showAllVersions is true + if showAllVersions { + return allEvents, nil + } + + // Separate events by type and filter replaceables + replaceableEvents := make(map[string]*event.E) // key: pubkey:kind + paramReplaceableEvents := make(map[string]map[string]*event.E) // key: pubkey:kind -> d-tag -> event + var regularEvents event.S + + for _, ev := range allEvents { + if kind.IsReplaceable(ev.Kind) { + // For replaceable events, keep only the latest per pubkey:kind + key := hex.Enc(ev.Pubkey) + ":" + strconv.Itoa(int(ev.Kind)) + existing, exists := replaceableEvents[key] + if !exists || ev.CreatedAt > existing.CreatedAt { + replaceableEvents[key] = ev + } + } else if kind.IsParameterizedReplaceable(ev.Kind) { + // For parameterized replaceable events, keep only the latest per pubkey:kind:d-tag + key := hex.Enc(ev.Pubkey) + ":" + strconv.Itoa(int(ev.Kind)) + + // Get the 'd' tag value + dTag := ev.Tags.GetFirst([]byte("d")) + var dValue string + if dTag != nil && dTag.Len() > 1 { + dValue = string(dTag.Value()) + } + + // Initialize inner map if needed + if _, exists := paramReplaceableEvents[key]; !exists { + paramReplaceableEvents[key] = make(map[string]*event.E) + } + + // Keep only the newest version + existing, exists := paramReplaceableEvents[key][dValue] + if !exists || ev.CreatedAt > existing.CreatedAt { + paramReplaceableEvents[key][dValue] = ev + } + } else { + regularEvents = append(regularEvents, ev) + } + } + + // Combine results + evs = make(event.S, 0, len(replaceableEvents)+len(paramReplaceableEvents)+len(regularEvents)) + + for _, ev := range replaceableEvents { + evs = append(evs, ev) + } + + for _, innerMap := range paramReplaceableEvents { + for _, ev := range innerMap { + evs = append(evs, ev) + } + } + + evs = append(evs, regularEvents...) + + // Re-sort by timestamp (newest first) + sort.Slice(evs, func(i, j int) bool { + return evs[i].CreatedAt > evs[j].CreatedAt + }) + + // Re-apply limit after filtering + if f.Limit != nil && len(evs) > int(*f.Limit) { + evs = evs[:*f.Limit] + } + return evs, nil } diff --git a/pkg/neo4j/save-event.go b/pkg/neo4j/save-event.go index bf8356e..c019a07 100644 --- a/pkg/neo4j/save-event.go +++ b/pkg/neo4j/save-event.go @@ -56,6 +56,15 @@ func (n *N) SaveEvent(c context.Context, ev *event.E) (exists bool, err error) { return true, nil // Event already exists } + // For parameterized replaceable events (kinds 30000-39999), delete older versions + // before saving the new one. This ensures Neo4j only stores the latest version. + if ev.Kind >= 30000 && ev.Kind < 40000 { + if err := n.deleteOlderParameterizedReplaceable(c, ev); err != nil { + n.Logger.Warningf("failed to delete older replaceable events: %v", err) + // Continue with save - older events will be filtered at query time + } + } + // Get next serial number serial, err := n.getNextSerial() if err != nil { @@ -444,3 +453,37 @@ ORDER BY e.created_at DESC` return wouldReplace, serials, nil } + +// deleteOlderParameterizedReplaceable deletes older versions of parameterized replaceable events +// (kinds 30000-39999) that have the same pubkey, kind, and d-tag value. +// This is called before saving a new event to ensure only the latest version is stored. +func (n *N) deleteOlderParameterizedReplaceable(c context.Context, ev *event.E) error { + authorPubkey := hex.Enc(ev.Pubkey[:]) + + // Get the d-tag value + dTag := ev.Tags.GetFirst([]byte{'d'}) + dValue := "" + if dTag != nil && len(dTag.T) >= 2 { + dValue = string(dTag.T[1]) + } + + // Delete older events with same pubkey, kind, and d-tag + // Only delete if the existing event is older than the new one + cypher := ` +MATCH (e:Event {kind: $kind, pubkey: $pubkey})-[:TAGGED_WITH]->(t:Tag {type: 'd', value: $dValue}) +WHERE e.created_at < $createdAt +DETACH DELETE e` + + params := map[string]any{ + "pubkey": authorPubkey, + "kind": int64(ev.Kind), + "dValue": dValue, + "createdAt": ev.CreatedAt, + } + + if _, err := n.ExecuteWrite(c, cypher, params); err != nil { + return fmt.Errorf("failed to delete older replaceable events: %w", err) + } + + return nil +} diff --git a/pkg/version/version b/pkg/version/version index 476935b..7526600 100644 --- a/pkg/version/version +++ b/pkg/version/version @@ -1 +1 @@ -v0.52.4 +v0.52.5