From f0b3da78ef3adc7fcf79d251518df4be95a30884 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 2 May 2025 23:52:49 -0300 Subject: [PATCH] khatru: upgrade docs a little more. --- khatru/README.md | 44 ++++++++++++--------------- khatru/docs/cookbook/custom-stores.md | 4 +-- khatru/docs/cookbook/google-drive.md | 4 +-- khatru/docs/cookbook/search.md | 8 ++--- khatru/docs/core/auth.md | 35 +++++++++++---------- khatru/docs/core/blossom.md | 16 +++++----- khatru/docs/core/eventstore.md | 14 ++++----- khatru/docs/core/management.md | 14 ++++----- khatru/docs/core/routing.md | 5 +-- khatru/docs/getting-started/index.md | 10 ++++-- khatru/docs/index.md | 7 ++--- khatru/examples/readme-demo/main.go | 4 +-- 12 files changed, 78 insertions(+), 87 deletions(-) diff --git a/khatru/README.md b/khatru/README.md index ebce9c7..1ae8221 100644 --- a/khatru/README.md +++ b/khatru/README.md @@ -40,31 +40,25 @@ func main() { store := make(map[nostr.ID]*nostr.Event, 120) // set up the basic relay functions - relay.StoreEvent = append(relay.StoreEvent, - func(ctx context.Context, event *nostr.Event) error { - store[event.ID] = event - return nil - }, - ) - relay.QueryEvents = append(relay.QueryEvents, - func(ctx context.Context, filter nostr.Filter) (iter.Seq[*nostr.Event], error) { - return func(yield func(*nostr.Event) bool) { - for _, evt := range store { - if filter.Matches(evt) { - if !yield(evt) { - break - } + relay.StoreEvent = func(ctx context.Context, event *nostr.Event) error { + store[event.ID] = event + return nil + } + relay.QueryStored = func(ctx context.Context, filter nostr.Filter) iter.Seq[*nostr.Event] { + return func(yield func(*nostr.Event) bool) { + for _, evt := range store { + if filter.Matches(evt) { + if !yield(evt) { + break } } - }, nil - }, - ) - relay.DeleteEvent = append(relay.DeleteEvent, - func(ctx context.Context, id nostr.ID) error { - delete(store, id) - return nil - }, - ) + } + } + } + relay.DeleteEvent = func(ctx context.Context, id nostr.ID) error { + delete(store, id) + return nil + } // there are many other configurable things you can set relay.RejectEvent = append(relay.RejectEvent, @@ -88,8 +82,8 @@ func main() { // define your own policies func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { - if pubkey, isAuthed := khatru.GetAuthed(ctx); isAuthed { - log.Printf("request from %s\n", pubkey) + if authed, is := khatru.GetAuthed(ctx); is { + log.Printf("request from %s\n", authed) return false, "" } return true, "auth-required: only authenticated users can read from this relay" diff --git a/khatru/docs/cookbook/custom-stores.md b/khatru/docs/cookbook/custom-stores.md index 3ae9620..a8efb29 100644 --- a/khatru/docs/cookbook/custom-stores.md +++ b/khatru/docs/cookbook/custom-stores.md @@ -15,9 +15,7 @@ func main () { // other stuff here relay := khatru.NewRelay() - relay.QueryEvents = append(relay.QueryEvents, - handleWeatherQuery, - ) + relay.QueryStored = handleWeatherQuery // other stuff here } diff --git a/khatru/docs/cookbook/google-drive.md b/khatru/docs/cookbook/google-drive.md index e931907..9e70ae7 100644 --- a/khatru/docs/cookbook/google-drive.md +++ b/khatru/docs/cookbook/google-drive.md @@ -11,8 +11,8 @@ func main () { // other stuff here relay := khatru.NewRelay() - relay.StoreEvent = append(relay.StoreEvent, handleEvent) - relay.QueryEvents = append(relay.QueryEvents, handleQuery) + relay.StoreEvent = handleEvent + relay.QueryStored = handleQuery // other stuff here } diff --git a/khatru/docs/cookbook/search.md b/khatru/docs/cookbook/search.md index a999202..0b17f38 100644 --- a/khatru/docs/cookbook/search.md +++ b/khatru/docs/cookbook/search.md @@ -25,9 +25,9 @@ func main () { panic(err) } - relay.StoreEvent = append(relay.StoreEvent, normal.SaveEvent, search.SaveEvent) - relay.QueryEvents = append(relay.QueryEvents, normal.QueryEvents, search.QueryEvents) - relay.DeleteEvent = append(relay.DeleteEvent, normal.DeleteEvent, search.DeleteEvent) + relay.StoreEvent = policies.SeqStore(normal.SaveEvent, search.SaveEvent) + relay.QueryStored = policies.SeqQuery(normal.QueryEvents, search.QueryEvents) + relay.DeleteEvent = policies.SeqDelete(normal.DeleteEvent, search.DeleteEvent) // other stuff here } @@ -38,7 +38,7 @@ Note that in this case we're using the [LMDB](https://pkg.go.dev/fiatjaf.com/nos Other adapters, like [SQLite](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/sqlite3), implement search functionality on their own, so if you don't want to use that you would have to have a middleware between, like: ```go - relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent, search.SaveEvent) + relay.StoreEvent = policies.SeqStore(db.SaveEvent, search.SaveEvent) relay.QueryStored = func (ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event] { if len(filter.Search) > 0 { return search.QueryEvents(ctx, filter) diff --git a/khatru/docs/core/auth.md b/khatru/docs/core/auth.md index e8198c4..bf96981 100644 --- a/khatru/docs/core/auth.md +++ b/khatru/docs/core/auth.md @@ -15,24 +15,27 @@ It makes sense to give the user the option to authenticate right after they esta ```go relay := khatru.NewRelay() -relay.OnConnect = append(relay.OnConnect, func(ctx context.Context) { - khatru.RequestAuth(ctx) -}) +relay.OnConnect = policies.SeqConnect( + khatru.RequestAuth, + func(ctx context.Context) { + khatru.RequestAuth(ctx) + }, +) ``` This will send a NIP-42 `AUTH` challenge message to the client so it will have the option to authenticate itself whenever it wants to. ## Signaling to the client that a specific query requires an authenticated user -If on `RejectFilter` or `RejectEvent` you prefix the message with `auth-required: `, that will automatically send an `AUTH` message before a `CLOSED` or `OK` with that prefix, such that the client will immediately be able to know it must authenticate to proceed and will already have the challenge required for that, so they can immediately replay the request. +If on `OnRequest` or `OnEvent` you prefix the message with `auth-required: `, that will automatically send an `AUTH` message before a `CLOSED` or `OK` with that prefix, such that the client will immediately be able to know it must authenticate to proceed and will already have the challenge required for that, so they can immediately replay the request. ```go -relay.RejectFilter = append(relay.RejectFilter, func(ctx context.Context, filter nostr.Filter) (bool, string) { +relay.OnRequest = func(ctx context.Context, filter nostr.Filter) (bool, string) { return true, "auth-required: this query requires you to be authenticated" -}) -relay.RejectEvent = append(relay.RejectEvent, func(ctx context.Context, event *nostr.Event) (bool, string) { +} +relay.OnEvent = func(ctx context.Context, event *nostr.Event) (bool, string) { return true, "auth-required: publishing this event requires authentication" -}) +} ``` ## Reading the auth status of a client @@ -40,25 +43,25 @@ relay.RejectEvent = append(relay.RejectEvent, func(ctx context.Context, event *n After a client is authenticated and opens a new subscription with `REQ` or sends a new event with `EVENT`, you'll be able to read the public key they're authenticated with. ```go -relay.RejectFilter = append(relay.RejectFilter, func(ctx context.Context, filter nostr.Filter) (bool, string) { +relay.OnRequest = func(ctx context.Context, filter nostr.Filter) (bool, string) { authenticatedUser, isAuthenticated := khatru.GetAuthed(ctx) -}) +} ``` ## Telling an authenticated user they're still not allowed to do something -If the user is authenticated but still not allowed (because some specific filters or events are only accessible to some specific users) you can reply on `RejectFilter` or `RejectEvent` with a message prefixed with `"restricted: "` to make that clear to clients. +If the user is authenticated but still not allowed (because some specific filters or events are only accessible to some specific users) you can reply on `OnRequest` or `OnEvent` with a message prefixed with `"restricted: "` to make that clear to clients. ```go -relay.RejectFilter = append(relay.RejectFilter, func(ctx context.Context, filter nostr.Filter) (bool, string) { - authenticatedUser := khatru.GetAuthed(ctx) +relay.OnRequest = func(ctx context.Context, filter nostr.Filter) (bool, string) { + authenticatedUser, _ := khatru.GetAuthed(ctx) - if slices.Contain(authorizedUsers, authenticatedUser) { + if slices.Contains(authorizedUsers, authenticatedUser) { return false } else { return true, "restricted: you're not a member of the privileged group that can read that stuff" } -}) +} ``` ## Reacting to a successful authentication @@ -68,7 +71,7 @@ Each `khatru.WebSocket` object has an `.Authed` channel that is closed whenever You can use that to emulate a listener for these events in case you want to keep track of who is authenticating in real time and not only check it when they request for something. ```go - relay.OnConnect = append(relay.OnConnect, + relay.OnConnect = policies.SeqConnect( khatru.RequestAuth, func(ctx context.Context) { go func(ctx context.Context) { diff --git a/khatru/docs/core/blossom.md b/khatru/docs/core/blossom.md index 2d24758..7a90f43 100644 --- a/khatru/docs/core/blossom.md +++ b/khatru/docs/core/blossom.md @@ -22,18 +22,18 @@ func main() { bl.Store = blossom.EventStoreBlobIndexWrapper{Store: blobdb, ServiceURL: bl.ServiceURL} // implement the required storage functions - bl.StoreBlob = append(bl.StoreBlob, func(ctx context.Context, sha256 string, body []byte) error { + bl.StoreBlob = func(ctx context.Context, sha256 string, body []byte) error { // store the blob data somewhere return nil - }) - bl.LoadBlob = append(bl.LoadBlob, func(ctx context.Context, sha256 string) (io.ReadSeeker, error) { + } + bl.LoadBlob = func(ctx context.Context, sha256 string) (io.ReadSeeker, error) { // load and return the blob data return nil, nil - }) - bl.DeleteBlob = append(bl.DeleteBlob, func(ctx context.Context, sha256 string) error { + } + bl.DeleteBlob = func(ctx context.Context, sha256 string) error { // delete the blob data return nil - }) + } http.ListenAndServe(":3334", relay) } @@ -59,7 +59,7 @@ var allowedUsers = map[string]bool{ "pubkey2": true, } -bl.RejectUpload = append(bl.RejectUpload, func(ctx context.Context, auth *nostr.Event, size int, ext string) (bool, string, int) { +bl.RejectUpload = func(ctx context.Context, auth *nostr.Event, size int, ext string) (bool, string, int) { // check file size if size > maxFileSize { return true, "file too large", 413 @@ -71,7 +71,7 @@ bl.RejectUpload = append(bl.RejectUpload, func(ctx context.Context, auth *nostr. } return false, "", 0 -}) +} ``` There are other `Reject*` hooks you can also implement, but this is the most important one. diff --git a/khatru/docs/core/eventstore.md b/khatru/docs/core/eventstore.md index a5bf20f..7cdbd58 100644 --- a/khatru/docs/core/eventstore.md +++ b/khatru/docs/core/eventstore.md @@ -50,11 +50,11 @@ func main() { ## Using two at a time -If you want to use two different adapters at the same time that's easy. Just add both to the corresponding slices: +If you want to use two different adapters at the same time that's easy. Just use the `policies.Seq*` functions: ```go - relay.StoreEvent = append(relay.StoreEvent, db1.SaveEvent, db2.SaveEvent) - relay.QueryEvents = append(relay.QueryEvents, db1.QueryEvents, db2.QueryEvents) + relay.StoreEvent = policies.SeqStore(db1.SaveEvent, db2.SaveEvent) + relay.QueryStored = policies.SeqQuery(db1.QueryEvents, db2.QueryEvents) ``` But that will duplicate events on both and then return duplicated events on each query. @@ -66,7 +66,7 @@ You can do a kind of sharding, for example, by storing some events in one store For example, maybe you want kind 1 events in `db1` and kind 30023 events in `db30023`: ```go - relay.StoreEvent = append(relay.StoreEvent, func (ctx context.Context, evt *nostr.Event) error { + relay.StoreEvent = func (ctx context.Context, evt *nostr.Event) error { switch evt.Kind { case nostr.Kind(1): return db1.SaveEvent(evt) @@ -75,8 +75,8 @@ For example, maybe you want kind 1 events in `db1` and kind 30023 events in `db3 default: return nil } - }) - relay.QueryEvents = append(relay.QueryEvents, func (ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event] { + } + relay.QueryStored = func (ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event] { for _, kind := range filter.Kinds { switch nostr.Kind(kind) { case nostr.Kind(1): @@ -92,5 +92,5 @@ For example, maybe you want kind 1 events in `db1` and kind 30023 events in `db3 } } return nil - }) + } ``` diff --git a/khatru/docs/core/management.md b/khatru/docs/core/management.md index 7f944f8..a02e114 100644 --- a/khatru/docs/core/management.md +++ b/khatru/docs/core/management.md @@ -19,15 +19,13 @@ var allowedPubkeys = make([]nostr.PubKey, 0, 10) func main () { relay := khatru.NewRelay() - relay.ManagementAPI.RejectAPICall = append(relay.ManagementAPI.RejectAPICall, - func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string) { - user := khatru.GetAuthed(ctx) - if user != owner { - return true, "go away, intruder" - } - return false, "" + relay.ManagementAPI.RejectAPICall = func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string) { + authed, _ := khatru.GetAuthed(ctx) + if user != owner { + return true, "go away, intruder" } - ) + return false, "" + } relay.ManagementAPI.AllowPubKey = func(ctx context.Context, pubkey nostr.PubKey, reason string) error { allowedPubkeys = append(allowedPubkeys, pubkey) diff --git a/khatru/docs/core/routing.md b/khatru/docs/core/routing.md index 95b7bfa..57d6b3d 100644 --- a/khatru/docs/core/routing.md +++ b/khatru/docs/core/routing.md @@ -21,10 +21,7 @@ groupsRelay, _ := khatru29.Init(relay29.Options{Domain: "example.com", DB: group publicStore := slicestore.SliceStore{} publicStore.Init() publicRelay := khatru.NewRelay() -publicRelay.StoreEvent = append(publicRelay.StoreEvent, publicStore.SaveEvent) -publicRelay.QueryEvents = append(publicRelay.QueryEvents, publicStore.QueryEvents) -publicRelay.CountEvents = append(publicRelay.CountEvents, publicStore.CountEvents) -publicRelay.DeleteEvent = append(publicRelay.DeleteEvent, publicStore.DeleteEvent) +publicRelay.UseEventStore(publicStore) // ... // a higher-level relay that just routes between the two above diff --git a/khatru/docs/getting-started/index.md b/khatru/docs/getting-started/index.md index 7cee8b4..748a0a4 100644 --- a/khatru/docs/getting-started/index.md +++ b/khatru/docs/getting-started/index.md @@ -47,13 +47,13 @@ These are lists of functions that will be called in order every time an `EVENT` The next step is adding some protection, because maybe we don't want to allow _anyone_ to write to our relay. Maybe we want to only allow people that have a pubkey starting with `"a"`, `"b"` or `"c"`: ```go -relay.RejectEvent = append(relay.RejectEvent, func (ctx context.Context, event *nostr.Event) (reject bool, msg string) { +relay.OnEvent = func (ctx context.Context, event *nostr.Event) (reject bool, msg string) { firstHexChar := event.PubKey.Hex()[0:1] if firstHexChar == "a" || firstHexChar == "b" || firstHexChar == "c" { return false, "" // allow } return true, "you're not allowed in this shard" -}) +} ``` We can also make use of some default policies that come bundled with Khatru: @@ -61,7 +61,11 @@ We can also make use of some default policies that come bundled with Khatru: ```go import "fiatjaf.com/nostr/khatru/policies" // implied -relay.RejectEvent = append(relay.RejectEvent, policies.PreventLargeTags(120), policies.PreventTimestampsInThePast(time.Hour * 2), policies.PreventTimestampsInTheFuture(time.Minute * 30)) +relay.OnEvent = policies.SeqEvent( + policies.PreventLargeTags(120), + policies.PreventTimestampsInThePast(time.Hour * 2), + policies.PreventTimestampsInTheFuture(time.Minute * 30), +) ``` There are many other ways to customize the relay behavior. Take a look at the [`Relay` struct docs](https://pkg.go.dev/fiatjaf.com/nostr/khatru#Relay) for more, or read the pages on the sidebar. diff --git a/khatru/docs/index.md b/khatru/docs/index.md index b01cfa4..16eba43 100644 --- a/khatru/docs/index.md +++ b/khatru/docs/index.md @@ -47,11 +47,8 @@ It allows you to create a fully-functional relay in 7 lines of code: func main() { relay := khatru.NewRelay() db := badger.BadgerBackend{Path: "/tmp/khatru-badgern-tmp"} - db.Init() - relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent) - relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents) - relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent) - relay.ReplaceEvent = append(relay.ReplaceEvent, db.ReplaceEvent) + db.Init() + relay.UseEventStore(db) http.ListenAndServe(":3334", relay) } ``` diff --git a/khatru/examples/readme-demo/main.go b/khatru/examples/readme-demo/main.go index 8eee4c3..13ab596 100644 --- a/khatru/examples/readme-demo/main.go +++ b/khatru/examples/readme-demo/main.go @@ -66,8 +66,8 @@ func main() { // define your own policies func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { - if pubkey, isAuthed := khatru.GetAuthed(ctx); !isAuthed { - log.Printf("request from %s\n", pubkey) + if authed, is := khatru.GetAuthed(ctx); !is { + log.Printf("request from %s\n", authed) return false, "" } return true, "auth-required: only authenticated users can read from this relay"