Compare commits

...

4 Commits

Author SHA1 Message Date
fiatjaf
33545587b6 make it so ephemeral events respond with ok:false if no one is listening. 2025-04-14 09:24:34 -03:00
Kay
214371f8bd refactor(adding): check kind range with proper function. 2025-04-13 09:05:23 -03:00
fiatjaf
fbb40f3b74 use .Find() instead of .GetFirst() everywhere. 2025-04-04 23:07:18 -03:00
fiatjaf
d97a2f1cf2 initialScan() 2025-04-04 17:55:16 -03:00
13 changed files with 135 additions and 91 deletions

132
adding.go
View File

@@ -11,34 +11,46 @@ import (
// 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.
func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) (skipBroadcast bool, writeError error) { func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) (skipBroadcast bool, writeError error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if evt == nil { if evt == nil {
return false, errors.New("error: event is nil") return false, errors.New("error: event is nil")
} }
if nostr.IsEphemeralKind(evt.Kind) {
return false, rl.handleEphemeral(ctx, evt)
} else {
return rl.handleNormal(ctx, evt)
}
}
func (rl *Relay) handleNormal(ctx context.Context, evt *nostr.Event) (skipBroadcast bool, writeError error) {
for _, reject := range rl.RejectEvent { for _, reject := range rl.RejectEvent {
if reject, msg := reject(ctx, evt); reject { if reject, msg := reject(ctx, evt); reject {
if msg == "" { if msg == "" {
return false, errors.New("blocked: no reason") return true, errors.New("blocked: no reason")
} else { } else {
return false, errors.New(nostr.NormalizeOKMessage(msg, "blocked")) return true, errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
} }
} }
} }
if 20000 <= evt.Kind && evt.Kind < 30000 { // will store
// do not store ephemeral events // regular kinds are just saved directly
for _, oee := range rl.OnEphemeralEvent { if nostr.IsRegularKind(evt.Kind) {
oee(ctx, evt) for _, store := range rl.StoreEvent {
if err := store(ctx, evt); err != nil {
switch err {
case eventstore.ErrDupEvent:
return true, nil
default:
return false, fmt.Errorf("%s", nostr.NormalizeOKMessage(err.Error(), "error"))
}
}
} }
} else { } else {
// will store // otherwise it's a replaceable -- so we'll use the replacer functions if we have any
// regular kinds are just saved directly if len(rl.ReplaceEvent) > 0 {
if nostr.IsRegularKind(evt.Kind) { for _, repl := range rl.ReplaceEvent {
for _, store := range rl.StoreEvent { if err := repl(ctx, evt); err != nil {
if err := store(ctx, evt); err != nil {
switch err { switch err {
case eventstore.ErrDupEvent: case eventstore.ErrDupEvent:
return true, nil return true, nil
@@ -48,68 +60,54 @@ func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) (skipBroadcast
} }
} }
} else { } else {
// otherwise it's a replaceable -- so we'll use the replacer functions if we have any // otherwise do it the manual way
if len(rl.ReplaceEvent) > 0 { filter := nostr.Filter{Limit: 1, Kinds: []int{evt.Kind}, Authors: []string{evt.PubKey}}
for _, repl := range rl.ReplaceEvent { if nostr.IsAddressableKind(evt.Kind) {
if err := repl(ctx, evt); err != nil { // when addressable, add the "d" tag to the filter
switch err { filter.Tags = nostr.TagMap{"d": []string{evt.Tags.GetD()}}
}
// now we fetch old events and delete them
shouldStore := true
for _, query := range rl.QueryEvents {
ch, err := query(ctx, filter)
if err != nil {
continue
}
for previous := range ch {
if isOlder(previous, evt) {
for _, del := range rl.DeleteEvent {
del(ctx, previous)
}
} else {
// we found a more recent event, so we won't delete it and also will not store this new one
shouldStore = false
}
}
}
// store
if shouldStore {
for _, store := range rl.StoreEvent {
if saveErr := store(ctx, evt); saveErr != nil {
switch saveErr {
case eventstore.ErrDupEvent: case eventstore.ErrDupEvent:
return true, nil return true, nil
default: default:
return false, fmt.Errorf("%s", nostr.NormalizeOKMessage(err.Error(), "error")) return false, fmt.Errorf("%s", nostr.NormalizeOKMessage(saveErr.Error(), "error"))
}
}
}
} else {
// otherwise do it the manual way
filter := nostr.Filter{Limit: 1, Kinds: []int{evt.Kind}, Authors: []string{evt.PubKey}}
if nostr.IsAddressableKind(evt.Kind) {
// when addressable, add the "d" tag to the filter
filter.Tags = nostr.TagMap{"d": []string{evt.Tags.GetD()}}
}
// now we fetch old events and delete them
shouldStore := true
for _, query := range rl.QueryEvents {
ch, err := query(ctx, filter)
if err != nil {
continue
}
for previous := range ch {
if isOlder(previous, evt) {
for _, del := range rl.DeleteEvent {
del(ctx, previous)
}
} else {
// we found a more recent event, so we won't delete it and also will not store this new one
shouldStore = false
}
}
}
// store
if shouldStore {
for _, store := range rl.StoreEvent {
if saveErr := store(ctx, evt); saveErr != nil {
switch saveErr {
case eventstore.ErrDupEvent:
return true, nil
default:
return false, fmt.Errorf("%s", nostr.NormalizeOKMessage(saveErr.Error(), "error"))
}
} }
} }
} }
} }
} }
for _, ons := range rl.OnEventSaved {
ons(ctx, evt)
}
// track event expiration if applicable
rl.expirationManager.trackEvent(evt)
} }
for _, ons := range rl.OnEventSaved {
ons(ctx, evt)
}
// track event expiration if applicable
rl.expirationManager.trackEvent(evt)
return false, nil return false, nil
} }

View File

@@ -25,7 +25,7 @@ func (bs BlossomServer) handleUploadCheck(w http.ResponseWriter, r *http.Request
blossomError(w, "missing \"Authorization\" header", 401) blossomError(w, "missing \"Authorization\" header", 401)
return return
} }
if auth.Tags.GetFirst([]string{"t", "upload"}) == nil { if auth.Tags.FindWithValue("t", "upload") == nil {
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403) blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
return return
} }
@@ -59,7 +59,7 @@ func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) {
blossomError(w, "missing \"Authorization\" header", 401) blossomError(w, "missing \"Authorization\" header", 401)
return return
} }
if auth.Tags.GetFirst([]string{"t", "upload"}) == nil { if auth.Tags.FindWithValue("t", "upload") == nil {
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403) blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
return return
} }
@@ -163,13 +163,13 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) {
// if there is one, we check if it has the extra requirements // if there is one, we check if it has the extra requirements
if auth != nil { if auth != nil {
if auth.Tags.GetFirst([]string{"t", "get"}) == nil { if auth.Tags.FindWithValue("t", "get") == nil {
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403) blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
return return
} }
if auth.Tags.GetFirst([]string{"x", hhash}) == nil && if auth.Tags.FindWithValue("x", hhash) == nil &&
auth.Tags.GetFirst([]string{"server", bs.ServiceURL}) == nil { auth.Tags.FindWithValue("server", bs.ServiceURL) == nil {
blossomError(w, "invalid \"Authorization\" event \"x\" or \"server\" tag", 403) blossomError(w, "invalid \"Authorization\" event \"x\" or \"server\" tag", 403)
return return
} }
@@ -239,7 +239,7 @@ func (bs BlossomServer) handleList(w http.ResponseWriter, r *http.Request) {
// if there is one, we check if it has the extra requirements // if there is one, we check if it has the extra requirements
if auth != nil { if auth != nil {
if auth.Tags.GetFirst([]string{"t", "list"}) == nil { if auth.Tags.FindWithValue("t", "list") == nil {
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403) blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
return return
} }
@@ -283,7 +283,7 @@ func (bs BlossomServer) handleDelete(w http.ResponseWriter, r *http.Request) {
} }
if auth != nil { if auth != nil {
if auth.Tags.GetFirst([]string{"t", "delete"}) == nil { if auth.Tags.FindWithValue("t", "delete") == nil {
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403) blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
return return
} }
@@ -296,8 +296,8 @@ func (bs BlossomServer) handleDelete(w http.ResponseWriter, r *http.Request) {
return return
} }
hhash = hhash[1:] hhash = hhash[1:]
if auth.Tags.GetFirst([]string{"x", hhash}) == nil && if auth.Tags.FindWithValue("x", hhash) == nil &&
auth.Tags.GetFirst([]string{"server", bs.ServiceURL}) == nil { auth.Tags.FindWithValue("server", bs.ServiceURL) == nil {
blossomError(w, "invalid \"Authorization\" event \"x\" or \"server\" tag", 403) blossomError(w, "invalid \"Authorization\" event \"x\" or \"server\" tag", 403)
return return
} }

View File

@@ -6,6 +6,6 @@ import (
// BroadcastEvent emits an event to all listeners whose filters' match, skipping all filters and actions // BroadcastEvent emits an event to all listeners whose filters' match, skipping all filters and actions
// it also doesn't attempt to store the event or trigger any reactions or callbacks // it also doesn't attempt to store the event or trigger any reactions or callbacks
func (rl *Relay) BroadcastEvent(evt *nostr.Event) { func (rl *Relay) BroadcastEvent(evt *nostr.Event) int {
rl.notifyListeners(evt) return rl.notifyListeners(evt)
} }

View File

@@ -47,7 +47,7 @@ router.Route().
return true return true
case event.Kind <= 12 && event.Kind >= 9: case event.Kind <= 12 && event.Kind >= 9:
return true return true
case event.Tags.GetFirst([]string{"h", ""}) != nil: case event.Tags.Find("h") != nil:
return true return true
default: default:
return false return false

26
ephemeral.go Normal file
View File

@@ -0,0 +1,26 @@
package khatru
import (
"context"
"errors"
"github.com/nbd-wtf/go-nostr"
)
func (rl *Relay) handleEphemeral(ctx context.Context, evt *nostr.Event) error {
for _, reject := range rl.RejectEvent {
if reject, msg := reject(ctx, evt); reject {
if msg == "" {
return errors.New("blocked: no reason")
} else {
return errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
}
}
}
for _, oee := range rl.OnEphemeralEvent {
oee(ctx, evt)
}
return nil
}

View File

@@ -13,7 +13,7 @@ func main() {
relay := khatru.NewRelay() relay := khatru.NewRelay()
db := lmdb.LMDBBackend{Path: "/tmp/khatru-lmdb-tmp"} db := lmdb.LMDBBackend{Path: "/tmp/khatru-lmdb-tmp"}
os.MkdirAll(db.Path, 0755) os.MkdirAll(db.Path, 0o755)
if err := db.Init(); err != nil { if err := db.Init(); err != nil {
panic(err) panic(err)
} }

View File

@@ -16,7 +16,7 @@ func main() {
relay := khatru.NewRelay() relay := khatru.NewRelay()
db := lmdb.LMDBBackend{Path: "/tmp/exclusive"} db := lmdb.LMDBBackend{Path: "/tmp/exclusive"}
os.MkdirAll(db.Path, 0755) os.MkdirAll(db.Path, 0o755)
if err := db.Init(); err != nil { if err := db.Init(); err != nil {
panic(err) panic(err)
} }

View File

@@ -52,7 +52,7 @@ func main() {
return slices.Contains(filter.Kinds, 1) && slices.Contains(filter.Tags["t"], "spam") return slices.Contains(filter.Kinds, 1) && slices.Contains(filter.Tags["t"], "spam")
}). }).
Event(func(event *nostr.Event) bool { Event(func(event *nostr.Event) bool {
return event.Kind == 1 && event.Tags.GetFirst([]string{"t", "spam"}) != nil return event.Kind == 1 && event.Tags.FindWithValue("t", "spam") != nil
}). }).
Relay(r2) Relay(r2)

View File

@@ -73,6 +73,7 @@ func (em *expirationManager) initialScan(ctx context.Context) {
defer em.mu.Unlock() defer em.mu.Unlock()
// query all events // query all events
ctx = context.WithValue(ctx, internalCallKey, struct{}{})
for _, query := range em.relay.QueryEvents { for _, query := range em.relay.QueryEvents {
ch, err := query(ctx, nostr.Filter{}) ch, err := query(ctx, nostr.Filter{})
if err != nil { if err != nil {

View File

@@ -6,6 +6,7 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"net/http" "net/http"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -141,6 +142,7 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
continue continue
} }
// this is safe because ReadMessage() will always create a new slice
message := unsafe.String(unsafe.SliceData(msgb), len(msgb)) message := unsafe.String(unsafe.SliceData(msgb), len(msgb))
// parse messages sequentially otherwise sonic breaks // parse messages sequentially otherwise sonic breaks
@@ -215,9 +217,12 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
if env.Event.Kind == 5 { if env.Event.Kind == 5 {
// this always returns "blocked: " whenever it returns an error // this always returns "blocked: " whenever it returns an error
writeErr = srl.handleDeleteRequest(ctx, &env.Event) writeErr = srl.handleDeleteRequest(ctx, &env.Event)
} else if nostr.IsEphemeralKind(env.Event.Kind) {
// this will also always return a prefixed reason
writeErr = srl.handleEphemeral(ctx, &env.Event)
} else { } else {
// this will also always return a prefixed reason // this will also always return a prefixed reason
skipBroadcast, writeErr = srl.AddEvent(ctx, &env.Event) skipBroadcast, writeErr = srl.handleNormal(ctx, &env.Event)
} }
var reason string var reason string
@@ -227,9 +232,20 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
ovw(ctx, &env.Event) ovw(ctx, &env.Event)
} }
if !skipBroadcast { if !skipBroadcast {
srl.notifyListeners(&env.Event) n := srl.notifyListeners(&env.Event)
// the number of notified listeners matters in ephemeral events
if nostr.IsEphemeralKind(env.Event.Kind) {
if n == 0 {
ok = false
reason = "mute: no one was listening for this"
} else {
reason = "broadcasted to " + strconv.Itoa(n) + " listeners"
}
}
} }
} else { } else {
ok = false
reason = writeErr.Error() reason = writeErr.Error()
if strings.HasPrefix(reason, "auth-required:") { if strings.HasPrefix(reason, "auth-required:") {
RequestAuth(ctx) RequestAuth(ctx)
@@ -244,14 +260,13 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
var total int64 var total int64
var hll *hyperloglog.HyperLogLog var hll *hyperloglog.HyperLogLog
uneligibleForHLL := false
srl := rl srl := rl
if rl.getSubRelayFromFilter != nil { if rl.getSubRelayFromFilter != nil {
srl = rl.getSubRelayFromFilter(env.Filter) srl = rl.getSubRelayFromFilter(env.Filter)
} }
if offset := nip45.HyperLogLogEventPubkeyOffsetForFilter(env.Filter); offset != -1 && !uneligibleForHLL { if offset := nip45.HyperLogLogEventPubkeyOffsetForFilter(env.Filter); offset != -1 {
total, hll = srl.handleCountRequestWithHLL(ctx, ws, env.Filter, offset) total, hll = srl.handleCountRequestWithHLL(ctx, ws, env.Filter, offset)
} else { } else {
total = srl.handleCountRequest(ctx, ws, env.Filter) total = srl.handleCountRequest(ctx, ws, env.Filter)

View File

@@ -132,15 +132,20 @@ func (rl *Relay) removeClientAndListeners(ws *WebSocket) {
delete(rl.clients, ws) delete(rl.clients, ws)
} }
func (rl *Relay) notifyListeners(event *nostr.Event) { // returns how many listeners were notified
func (rl *Relay) notifyListeners(event *nostr.Event) int {
count := 0
listenersloop:
for _, listener := range rl.listeners { for _, listener := range rl.listeners {
if listener.filter.Matches(event) { if listener.filter.Matches(event) {
for _, pb := range rl.PreventBroadcast { for _, pb := range rl.PreventBroadcast {
if pb(listener.ws, event) { if pb(listener.ws, event) {
return continue listenersloop
} }
} }
listener.ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &listener.id, Event: *event}) listener.ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &listener.id, Event: *event})
count++
} }
} }
return count
} }

View File

@@ -86,10 +86,10 @@ func (rl *Relay) HandleNIP86(w http.ResponseWriter, r *http.Request) {
goto respond goto respond
} }
if uTag := evt.Tags.GetFirst([]string{"u", ""}); uTag == nil || rl.getBaseURL(r) != (*uTag)[1] { if uTag := evt.Tags.Find("u"); uTag == nil || rl.getBaseURL(r) != uTag[1] {
resp.Error = "invalid 'u' tag" resp.Error = "invalid 'u' tag"
goto respond goto respond
} else if pht := evt.Tags.GetFirst([]string{"payload", hex.EncodeToString(payloadHash[:])}); pht == nil { } else if pht := evt.Tags.FindWithValue("payload", hex.EncodeToString(payloadHash[:])); pht == nil {
resp.Error = "invalid auth event payload hash" resp.Error = "invalid auth event payload hash"
goto respond goto respond
} else if evt.CreatedAt < nostr.Now()-30 { } else if evt.CreatedAt < nostr.Now()-30 {

View File

@@ -2,7 +2,6 @@ package policies
import ( import (
"context" "context"
"slices" "slices"
"github.com/fiatjaf/khatru" "github.com/fiatjaf/khatru"