khatru: store and broadcast kind:5 deletion events.
by insistence of @staab
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/eventstore"
|
"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.
|
// 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
|
// track event expiration if applicable
|
||||||
if rl.expirationManager != nil {
|
if rl.expirationManager != nil {
|
||||||
rl.expirationManager.trackEvent(evt)
|
rl.expirationManager.trackEvent(evt.ID, nip40.GetExpiration(evt.Tags))
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package khatru
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -9,8 +10,19 @@ import (
|
|||||||
"fiatjaf.com/nostr"
|
"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 {
|
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 {
|
for _, tag := range evt.Tags {
|
||||||
if len(tag) >= 2 {
|
if len(tag) >= 2 {
|
||||||
var f nostr.Filter
|
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{}{})
|
ctx := context.WithValue(ctx, internalCallKey, struct{}{})
|
||||||
|
|
||||||
if nil != rl.QueryStored {
|
for target := range rl.QueryStored(ctx, f) {
|
||||||
for target := range rl.QueryStored(ctx, f) {
|
// got the event, now check if the user can delete it
|
||||||
// got the event, now check if the user can delete it
|
if target.PubKey == evt.PubKey {
|
||||||
acceptDeletion := target.PubKey == evt.PubKey
|
// delete it
|
||||||
var msg string
|
if err := rl.DeleteEvent(ctx, target.ID); err != nil {
|
||||||
if !acceptDeletion {
|
return err
|
||||||
msg = "you are not the author of this event"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if acceptDeletion {
|
// if it was tracked to be expired that is not needed anymore
|
||||||
// delete it
|
if rl.expirationManager != nil {
|
||||||
if nil != rl.DeleteEvent {
|
rl.expirationManager.removeEvent(target.ID)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
haveDeletedSomething = true
|
||||||
|
} else {
|
||||||
|
// fail and stop here
|
||||||
|
return ErrNotAuthor
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't try to query this same event again
|
// 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,15 +113,17 @@ func (em *expirationManager) checkExpiredEvents(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (em *expirationManager) trackEvent(evt nostr.Event) {
|
func (em *expirationManager) trackEvent(id nostr.ID, expiration nostr.Timestamp) {
|
||||||
if expiresAt := nip40.GetExpiration(evt.Tags); expiresAt != -1 {
|
if expiration <= 0 {
|
||||||
em.mu.Lock()
|
return
|
||||||
heap.Push(&em.events, expiringEvent{
|
|
||||||
id: evt.ID,
|
|
||||||
expiresAt: expiresAt,
|
|
||||||
})
|
|
||||||
em.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
em.mu.Lock()
|
||||||
|
heap.Push(&em.events, expiringEvent{
|
||||||
|
id: id,
|
||||||
|
expiresAt: expiration,
|
||||||
|
})
|
||||||
|
em.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (em *expirationManager) removeEvent(id nostr.ID) {
|
func (em *expirationManager) removeEvent(id nostr.ID) {
|
||||||
|
|||||||
@@ -208,9 +208,13 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
var writeErr error
|
var writeErr error
|
||||||
var skipBroadcast bool
|
var skipBroadcast bool
|
||||||
|
|
||||||
if env.Event.Kind == 5 {
|
if env.Event.Kind == nostr.KindDeletion {
|
||||||
// this always returns "blocked: " whenever it returns an error
|
// store the delete event first
|
||||||
writeErr = srl.handleDeleteRequest(ctx, env.Event)
|
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() {
|
} else if env.Event.Kind.IsEphemeral() {
|
||||||
// this will also always return a prefixed reason
|
// this will also always return a prefixed reason
|
||||||
writeErr = srl.handleEphemeral(ctx, env.Event)
|
writeErr = srl.handleEphemeral(ctx, env.Event)
|
||||||
|
|||||||
@@ -160,11 +160,38 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
|||||||
if gotEvent {
|
if gotEvent {
|
||||||
t.Error("should not have received deleted event")
|
t.Error("should not have received deleted event")
|
||||||
}
|
}
|
||||||
return
|
goto checkDeleteStored
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
t.Fatal("timeout waiting for EOSE")
|
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
|
// test 4: teplaceable events
|
||||||
|
|||||||
Reference in New Issue
Block a user