From b87bc0ede4c50dc3f8c6973b116f4f226dfd74d9 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 31 Oct 2025 16:36:39 -0300 Subject: [PATCH] khatru: store and broadcast kind:5 deletion events. by insistence of @staab --- khatru/adding.go | 3 ++- khatru/deleting.go | 56 +++++++++++++++++++++++++------------------- khatru/expiration.go | 18 +++++++------- khatru/handlers.go | 10 +++++--- khatru/relay_test.go | 29 ++++++++++++++++++++++- 5 files changed, 79 insertions(+), 37 deletions(-) diff --git a/khatru/adding.go b/khatru/adding.go index c0e3dfd..c352cee 100644 --- a/khatru/adding.go +++ b/khatru/adding.go @@ -7,6 +7,7 @@ import ( "fiatjaf.com/nostr" "fiatjaf.com/nostr/eventstore" + "fiatjaf.com/nostr/nip40" ) // AddEvent sends an event through then normal add pipeline, as if it was received from a websocket. @@ -62,7 +63,7 @@ func (rl *Relay) handleNormal(ctx context.Context, evt nostr.Event) (skipBroadca // track event expiration if applicable if rl.expirationManager != nil { - rl.expirationManager.trackEvent(evt) + rl.expirationManager.trackEvent(evt.ID, nip40.GetExpiration(evt.Tags)) } return false, nil diff --git a/khatru/deleting.go b/khatru/deleting.go index 08e8405..7ac9ca0 100644 --- a/khatru/deleting.go +++ b/khatru/deleting.go @@ -2,6 +2,7 @@ package khatru import ( "context" + "errors" "fmt" "strconv" "strings" @@ -9,8 +10,19 @@ import ( "fiatjaf.com/nostr" ) +var ( + ErrNothingToDelete = errors.New("blocked: nothing to delete") + ErrNotAuthor = errors.New("blocked: you are not the author of this event") +) + +// event deletion -- nip09 func (rl *Relay) handleDeleteRequest(ctx context.Context, evt nostr.Event) error { - // event deletion -- nip09 + if nil == rl.QueryStored || nil == rl.DeleteEvent { + // if we don't have a way to query or to delete that means we won't delete anything + return ErrNothingToDelete + } + + haveDeletedSomething := false for _, tag := range evt.Tags { if len(tag) >= 2 { var f nostr.Filter @@ -49,31 +61,23 @@ func (rl *Relay) handleDeleteRequest(ctx context.Context, evt nostr.Event) error ctx := context.WithValue(ctx, internalCallKey, struct{}{}) - if nil != rl.QueryStored { - for target := range rl.QueryStored(ctx, f) { - // got the event, now check if the user can delete it - acceptDeletion := target.PubKey == evt.PubKey - var msg string - if !acceptDeletion { - msg = "you are not the author of this event" + for target := range rl.QueryStored(ctx, f) { + // got the event, now check if the user can delete it + if target.PubKey == evt.PubKey { + // delete it + if err := rl.DeleteEvent(ctx, target.ID); err != nil { + return err } - if acceptDeletion { - // delete it - if nil != rl.DeleteEvent { - if err := rl.DeleteEvent(ctx, target.ID); err != nil { - return err - } - } - - // if it was tracked to be expired that is not needed anymore - if rl.expirationManager != nil { - rl.expirationManager.removeEvent(target.ID) - } - } else { - // fail and stop here - return fmt.Errorf("blocked: %s", msg) + // if it was tracked to be expired that is not needed anymore + if rl.expirationManager != nil { + rl.expirationManager.removeEvent(target.ID) } + + haveDeletedSomething = true + } else { + // fail and stop here + return ErrNotAuthor } // don't try to query this same event again @@ -82,5 +86,9 @@ func (rl *Relay) handleDeleteRequest(ctx context.Context, evt nostr.Event) error } } - return nil + if haveDeletedSomething { + return nil + } + + return ErrNothingToDelete } diff --git a/khatru/expiration.go b/khatru/expiration.go index 0bd3983..997e259 100644 --- a/khatru/expiration.go +++ b/khatru/expiration.go @@ -113,15 +113,17 @@ func (em *expirationManager) checkExpiredEvents(ctx context.Context) { } } -func (em *expirationManager) trackEvent(evt nostr.Event) { - if expiresAt := nip40.GetExpiration(evt.Tags); expiresAt != -1 { - em.mu.Lock() - heap.Push(&em.events, expiringEvent{ - id: evt.ID, - expiresAt: expiresAt, - }) - em.mu.Unlock() +func (em *expirationManager) trackEvent(id nostr.ID, expiration nostr.Timestamp) { + if expiration <= 0 { + return } + + em.mu.Lock() + heap.Push(&em.events, expiringEvent{ + id: id, + expiresAt: expiration, + }) + em.mu.Unlock() } func (em *expirationManager) removeEvent(id nostr.ID) { diff --git a/khatru/handlers.go b/khatru/handlers.go index 87357ff..9e69040 100644 --- a/khatru/handlers.go +++ b/khatru/handlers.go @@ -208,9 +208,13 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) { var writeErr error var skipBroadcast bool - if env.Event.Kind == 5 { - // this always returns "blocked: " whenever it returns an error - writeErr = srl.handleDeleteRequest(ctx, env.Event) + if env.Event.Kind == nostr.KindDeletion { + // store the delete event first + skipBroadcast, writeErr = srl.handleNormal(ctx, env.Event) + if writeErr == nil { + // this always returns "blocked: " whenever it returns an error + writeErr = srl.handleDeleteRequest(ctx, env.Event) + } } else if env.Event.Kind.IsEphemeral() { // this will also always return a prefixed reason writeErr = srl.handleEphemeral(ctx, env.Event) diff --git a/khatru/relay_test.go b/khatru/relay_test.go index 9a60bf0..7693d8a 100644 --- a/khatru/relay_test.go +++ b/khatru/relay_test.go @@ -160,11 +160,38 @@ func TestBasicRelayFunctionality(t *testing.T) { if gotEvent { t.Error("should not have received deleted event") } - return + goto checkDeleteStored case <-ctx.Done(): t.Fatal("timeout waiting for EOSE") } } + + checkDeleteStored: + // verify that the delete event itself is stored + subDelete, err := client2.Subscribe(ctx, nostr.Filter{ + IDs: []nostr.ID{delEvent.ID}, + }, nostr.SubscriptionOptions{}) + if err != nil { + t.Fatalf("failed to subscribe to delete event: %v", err) + } + defer subDelete.Unsub() + + gotDeleteEvent := false + for { + select { + case evt := <-subDelete.Events: + if evt.ID == delEvent.ID { + gotDeleteEvent = true + } + case <-subDelete.EndOfStoredEvents: + if !gotDeleteEvent { + t.Error("should have received the delete event") + } + return + case <-ctx.Done(): + t.Fatal("timeout waiting for EOSE on delete event") + } + } }) // test 4: teplaceable events