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

View File

@@ -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"

View File

@@ -15,9 +15,7 @@ func main () {
// other stuff here
relay := khatru.NewRelay()
relay.QueryEvents = append(relay.QueryEvents,
handleWeatherQuery,
)
relay.QueryStored = handleWeatherQuery
// other stuff here
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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
})
}
```

View File

@@ -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)

View File

@@ -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

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"`:
```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.

View File

@@ -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)
}
```

View File

@@ -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"