khatru: upgrade docs a little more.

This commit is contained in:
fiatjaf
2025-05-02 23:52:49 -03:00
parent 1ece6d0eab
commit f0b3da78ef
12 changed files with 78 additions and 87 deletions
+19 -25
View File
@@ -40,31 +40,25 @@ func main() {
store := make(map[nostr.ID]*nostr.Event, 120) store := make(map[nostr.ID]*nostr.Event, 120)
// set up the basic relay functions // set up the basic relay functions
relay.StoreEvent = append(relay.StoreEvent, relay.StoreEvent = func(ctx context.Context, event *nostr.Event) error {
func(ctx context.Context, event *nostr.Event) error { store[event.ID] = event
store[event.ID] = event return nil
return nil }
}, relay.QueryStored = func(ctx context.Context, filter nostr.Filter) iter.Seq[*nostr.Event] {
) return func(yield func(*nostr.Event) bool) {
relay.QueryEvents = append(relay.QueryEvents, for _, evt := range store {
func(ctx context.Context, filter nostr.Filter) (iter.Seq[*nostr.Event], error) { if filter.Matches(evt) {
return func(yield func(*nostr.Event) bool) { if !yield(evt) {
for _, evt := range store { break
if filter.Matches(evt) {
if !yield(evt) {
break
}
} }
} }
}, nil }
}, }
) }
relay.DeleteEvent = append(relay.DeleteEvent, relay.DeleteEvent = func(ctx context.Context, id nostr.ID) error {
func(ctx context.Context, id nostr.ID) error { delete(store, id)
delete(store, id) return nil
return nil }
},
)
// there are many other configurable things you can set // there are many other configurable things you can set
relay.RejectEvent = append(relay.RejectEvent, relay.RejectEvent = append(relay.RejectEvent,
@@ -88,8 +82,8 @@ func main() {
// define your own policies // define your own policies
func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
if pubkey, isAuthed := khatru.GetAuthed(ctx); isAuthed { if authed, is := khatru.GetAuthed(ctx); is {
log.Printf("request from %s\n", pubkey) log.Printf("request from %s\n", authed)
return false, "" return false, ""
} }
return true, "auth-required: only authenticated users can read from this relay" return true, "auth-required: only authenticated users can read from this relay"
+1 -3
View File
@@ -15,9 +15,7 @@ func main () {
// other stuff here // other stuff here
relay := khatru.NewRelay() relay := khatru.NewRelay()
relay.QueryEvents = append(relay.QueryEvents, relay.QueryStored = handleWeatherQuery
handleWeatherQuery,
)
// other stuff here // other stuff here
} }
+2 -2
View File
@@ -11,8 +11,8 @@ func main () {
// other stuff here // other stuff here
relay := khatru.NewRelay() relay := khatru.NewRelay()
relay.StoreEvent = append(relay.StoreEvent, handleEvent) relay.StoreEvent = handleEvent
relay.QueryEvents = append(relay.QueryEvents, handleQuery) relay.QueryStored = handleQuery
// other stuff here // other stuff here
} }
+4 -4
View File
@@ -25,9 +25,9 @@ func main () {
panic(err) panic(err)
} }
relay.StoreEvent = append(relay.StoreEvent, normal.SaveEvent, search.SaveEvent) relay.StoreEvent = policies.SeqStore(normal.SaveEvent, search.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, normal.QueryEvents, search.QueryEvents) relay.QueryStored = policies.SeqQuery(normal.QueryEvents, search.QueryEvents)
relay.DeleteEvent = append(relay.DeleteEvent, normal.DeleteEvent, search.DeleteEvent) relay.DeleteEvent = policies.SeqDelete(normal.DeleteEvent, search.DeleteEvent)
// other stuff here // 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: 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 ```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] { relay.QueryStored = func (ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event] {
if len(filter.Search) > 0 { if len(filter.Search) > 0 {
return search.QueryEvents(ctx, filter) return search.QueryEvents(ctx, filter)
+19 -16
View File
@@ -15,24 +15,27 @@ It makes sense to give the user the option to authenticate right after they esta
```go ```go
relay := khatru.NewRelay() relay := khatru.NewRelay()
relay.OnConnect = append(relay.OnConnect, func(ctx context.Context) { relay.OnConnect = policies.SeqConnect(
khatru.RequestAuth(ctx) 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. 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 ## 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 ```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" 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" return true, "auth-required: publishing this event requires authentication"
}) }
``` ```
## Reading the auth status of a client ## 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. 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 ```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) authenticatedUser, isAuthenticated := khatru.GetAuthed(ctx)
}) }
``` ```
## Telling an authenticated user they're still not allowed to do something ## 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 ```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 := khatru.GetAuthed(ctx) authenticatedUser, _ := khatru.GetAuthed(ctx)
if slices.Contain(authorizedUsers, authenticatedUser) { if slices.Contains(authorizedUsers, authenticatedUser) {
return false return false
} else { } else {
return true, "restricted: you're not a member of the privileged group that can read that stuff" return true, "restricted: you're not a member of the privileged group that can read that stuff"
} }
}) }
``` ```
## Reacting to a successful authentication ## 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. 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 ```go
relay.OnConnect = append(relay.OnConnect, relay.OnConnect = policies.SeqConnect(
khatru.RequestAuth, khatru.RequestAuth,
func(ctx context.Context) { func(ctx context.Context) {
go func(ctx context.Context) { go func(ctx context.Context) {
+8 -8
View File
@@ -22,18 +22,18 @@ func main() {
bl.Store = blossom.EventStoreBlobIndexWrapper{Store: blobdb, ServiceURL: bl.ServiceURL} bl.Store = blossom.EventStoreBlobIndexWrapper{Store: blobdb, ServiceURL: bl.ServiceURL}
// implement the required storage functions // 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 // store the blob data somewhere
return nil 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 // load and return the blob data
return nil, nil 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 // delete the blob data
return nil return nil
}) }
http.ListenAndServe(":3334", relay) http.ListenAndServe(":3334", relay)
} }
@@ -59,7 +59,7 @@ var allowedUsers = map[string]bool{
"pubkey2": true, "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 // check file size
if size > maxFileSize { if size > maxFileSize {
return true, "file too large", 413 return true, "file too large", 413
@@ -71,7 +71,7 @@ bl.RejectUpload = append(bl.RejectUpload, func(ctx context.Context, auth *nostr.
} }
return false, "", 0 return false, "", 0
}) }
``` ```
There are other `Reject*` hooks you can also implement, but this is the most important one. There are other `Reject*` hooks you can also implement, but this is the most important one.
+7 -7
View File
@@ -50,11 +50,11 @@ func main() {
## Using two at a time ## 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 ```go
relay.StoreEvent = append(relay.StoreEvent, db1.SaveEvent, db2.SaveEvent) relay.StoreEvent = policies.SeqStore(db1.SaveEvent, db2.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, db1.QueryEvents, db2.QueryEvents) relay.QueryStored = policies.SeqQuery(db1.QueryEvents, db2.QueryEvents)
``` ```
But that will duplicate events on both and then return duplicated events on each query. 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`: For example, maybe you want kind 1 events in `db1` and kind 30023 events in `db30023`:
```go ```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 { switch evt.Kind {
case nostr.Kind(1): case nostr.Kind(1):
return db1.SaveEvent(evt) 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: default:
return nil 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 { for _, kind := range filter.Kinds {
switch nostr.Kind(kind) { switch nostr.Kind(kind) {
case nostr.Kind(1): 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 return nil
}) }
``` ```
+6 -8
View File
@@ -19,15 +19,13 @@ var allowedPubkeys = make([]nostr.PubKey, 0, 10)
func main () { func main () {
relay := khatru.NewRelay() relay := khatru.NewRelay()
relay.ManagementAPI.RejectAPICall = append(relay.ManagementAPI.RejectAPICall, relay.ManagementAPI.RejectAPICall = func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string) {
func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string) { authed, _ := khatru.GetAuthed(ctx)
user := khatru.GetAuthed(ctx) if user != owner {
if user != owner { return true, "go away, intruder"
return true, "go away, intruder"
}
return false, ""
} }
) return false, ""
}
relay.ManagementAPI.AllowPubKey = func(ctx context.Context, pubkey nostr.PubKey, reason string) error { relay.ManagementAPI.AllowPubKey = func(ctx context.Context, pubkey nostr.PubKey, reason string) error {
allowedPubkeys = append(allowedPubkeys, pubkey) allowedPubkeys = append(allowedPubkeys, pubkey)
+1 -4
View File
@@ -21,10 +21,7 @@ groupsRelay, _ := khatru29.Init(relay29.Options{Domain: "example.com", DB: group
publicStore := slicestore.SliceStore{} publicStore := slicestore.SliceStore{}
publicStore.Init() publicStore.Init()
publicRelay := khatru.NewRelay() publicRelay := khatru.NewRelay()
publicRelay.StoreEvent = append(publicRelay.StoreEvent, publicStore.SaveEvent) publicRelay.UseEventStore(publicStore)
publicRelay.QueryEvents = append(publicRelay.QueryEvents, publicStore.QueryEvents)
publicRelay.CountEvents = append(publicRelay.CountEvents, publicStore.CountEvents)
publicRelay.DeleteEvent = append(publicRelay.DeleteEvent, publicStore.DeleteEvent)
// ... // ...
// a higher-level relay that just routes between the two above // a higher-level relay that just routes between the two above
+7 -3
View File
@@ -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"`: 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 ```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] firstHexChar := event.PubKey.Hex()[0:1]
if firstHexChar == "a" || firstHexChar == "b" || firstHexChar == "c" { if firstHexChar == "a" || firstHexChar == "b" || firstHexChar == "c" {
return false, "" // allow return false, "" // allow
} }
return true, "you're not allowed in this shard" return true, "you're not allowed in this shard"
}) }
``` ```
We can also make use of some default policies that come bundled with Khatru: 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 ```go
import "fiatjaf.com/nostr/khatru/policies" // implied 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. 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.
+2 -5
View File
@@ -47,11 +47,8 @@ It allows you to create a fully-functional relay in 7 lines of code:
func main() { func main() {
relay := khatru.NewRelay() relay := khatru.NewRelay()
db := badger.BadgerBackend{Path: "/tmp/khatru-badgern-tmp"} db := badger.BadgerBackend{Path: "/tmp/khatru-badgern-tmp"}
db.Init() db.Init()
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent) relay.UseEventStore(db)
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
relay.ReplaceEvent = append(relay.ReplaceEvent, db.ReplaceEvent)
http.ListenAndServe(":3334", relay) http.ListenAndServe(":3334", relay)
} }
``` ```
+2 -2
View File
@@ -66,8 +66,8 @@ func main() {
// define your own policies // define your own policies
func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
if pubkey, isAuthed := khatru.GetAuthed(ctx); !isAuthed { if authed, is := khatru.GetAuthed(ctx); !is {
log.Printf("request from %s\n", pubkey) log.Printf("request from %s\n", authed)
return false, "" return false, ""
} }
return true, "auth-required: only authenticated users can read from this relay" return true, "auth-required: only authenticated users can read from this relay"