partial docs update.

This commit is contained in:
fiatjaf
2025-04-21 12:12:11 -03:00
parent 59bddab471
commit aaf0740513
9 changed files with 65 additions and 81 deletions

View File

@@ -1,8 +1,8 @@
# khatru, a relay framework [![docs badge](https://img.shields.io/badge/docs-reference-blue)](https://pkg.go.dev/github.com/fiatjaf/khatru#Relay)
# khatru, a relay framework [![docs badge](https://img.shields.io/badge/docs-reference-blue)](https://pkg.go.dev/fiatjaf.com/nostr/khatru#Relay)
[![Run Tests](https://github.com/fiatjaf/khatru/actions/workflows/test.yml/badge.svg)](https://github.com/fiatjaf/khatru/actions/workflows/test.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/fiatjaf/khatru.svg)](https://pkg.go.dev/github.com/fiatjaf/khatru)
[![Go Report Card](https://goreportcard.com/badge/github.com/fiatjaf/khatru)](https://goreportcard.com/report/github.com/fiatjaf/khatru)
[![Go Reference](https://pkg.go.dev/badge/fiatjaf.com/nostr/khatru.svg)](https://pkg.go.dev/fiatjaf.com/nostr/khatru)
[![Go Report Card](https://goreportcard.com/badge/fiatjaf.com/nostr/khatru)](https://goreportcard.com/report/fiatjaf.com/nostr/khatru)
Khatru makes it easy to write very very custom relays:
@@ -22,8 +22,8 @@ import (
"log"
"net/http"
"github.com/fiatjaf/khatru"
"github.com/nbd-wtf/go-nostr"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/khatru"
)
func main() {
@@ -32,12 +32,12 @@ func main() {
// set up some basic properties (will be returned on the NIP-11 endpoint)
relay.Info.Name = "my relay"
relay.Info.PubKey = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
relay.Info.PubKey = nostr.MustPubKeyFromHex("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
relay.Info.Description = "this is my custom relay"
relay.Info.Icon = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fliquipedia.net%2Fcommons%2Fimages%2F3%2F35%2FSCProbe.jpg&f=1&nofb=1&ipt=0cbbfef25bce41da63d910e86c3c343e6c3b9d63194ca9755351bb7c2efa3359&ipo=images"
// you must bring your own storage scheme -- if you want to have any
store := make(map[string]*nostr.Event, 120)
store := make(map[nostr.ID]*nostr.Event, 120)
// set up the basic relay functions
relay.StoreEvent = append(relay.StoreEvent,
@@ -47,22 +47,21 @@ func main() {
},
)
relay.QueryEvents = append(relay.QueryEvents,
func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
ch := make(chan *nostr.Event)
go func() {
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) {
ch <- evt
if !yield(evt) {
break
}
}
close(ch)
}()
return ch, nil
}
}, nil
},
)
relay.DeleteEvent = append(relay.DeleteEvent,
func(ctx context.Context, event *nostr.Event) error {
delete(store, event.ID)
func(ctx context.Context, id nostr.ID) error {
delete(store, id)
return nil
},
)
@@ -75,7 +74,7 @@ func main() {
// define your own policies
policies.PreventLargeTags(100),
func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
if event.PubKey == "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" {
if event.PubKey == nostr.MustPubKeyFromHex("fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52") {
return true, "we don't allow this person to write here"
}
return false, "" // anyone else can
@@ -89,7 +88,7 @@ func main() {
// define your own policies
func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
if pubkey := khatru.GetAuthed(ctx); pubkey != "" {
if pubkey, isAuthed := khatru.GetAuthed(ctx); isAuthed {
log.Printf("request from %s\n", pubkey)
return false, ""
}
@@ -123,16 +122,12 @@ Fear no more. Using the https://fiatjaf.com/nostr/eventstore module you get a bu
panic(err)
}
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
relay.ReplaceEvent = append(relay.ReplaceEvent, db.ReplaceEvent)
relay.UseEventstore(db)
```
### But I don't want to write a bunch of custom policies!
Fear no more. We have a bunch of common policies written in the `github.com/fiatjaf/khatru/policies` package and also a handpicked selection of base sane defaults, which you can apply with:
Fear no more. We have a bunch of common policies written in the `fiatjaf.com/nostr/khatru/policies` package and also a handpicked selection of base sane defaults, which you can apply with:
```go
policies.ApplySaneDefaults(relay)

View File

@@ -39,7 +39,7 @@ func startPollingGame(relay *khatru.Relay) {
Content: "team A has scored!",
Tags: nostr.Tags{{"t", "this-game"}}
}
evt.Sign(global.RelayPrivateKey)
evt.Sign(global.RelaySecretKey)
// calling BroadcastEvent will send the event to everybody who has been listening for tag "t=[this-game]"
// there is no need to do any code to keep track of these clients or who is listening to what, khatru
// does that already in the background automatically
@@ -61,4 +61,3 @@ func startPollingGame(relay *khatru.Relay) {
func fetchGameStatus() (GameStatus, error) {
// implementation of calling some external API goes here
}
```

View File

@@ -21,23 +21,19 @@ func main () {
// other stuff here
}
func handleWeatherQuery(ctx context.Context, filter nostr.Filter) (ch chan *nostr.Event, err error) {
if filter.Kind != 10774 {
func handleWeatherQuery(ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event] {
if filter.Kind != nostr.Kind(10774) {
// this function only handles kind 10774, if the query is for something else we return
// a nil channel, which corresponds to no results
return nil, nil
return nil
}
file, err := os.Open("weatherdata.xml")
if err != nil {
return nil, fmt.Errorf("we have lost our file: %w", err)
return nil
}
// QueryEvents functions are expected to return a channel
ch := make(chan *nostr.Event)
// and they can do their query asynchronously, emitting events to the channel as they come
go func () {
return func(yield func(nostr.Event) bool) {
defer file.Close()
// we're going to do this for each tag in the filter
@@ -74,15 +70,15 @@ func handleWeatherQuery(ctx context.Context, filter nostr.Filter) (ch chan *nost
{"condition", record[3]},
}
}
evt.Sign(global.RelayPrivateKey)
evt.Sign(global.RelaySecretKey)
ch <- evt
}
}
}
}()
return ch, nil
}
}
```
Beware, the code above is inefficient and the entire approach is not very smart, it's meant just as an example.

View File

@@ -4,7 +4,7 @@ outline: deep
# Implementing NIP-50 `search` support
The [`nostr.Filter` type](https://pkg.go.dev/github.com/nbd-wtf/go-nostr#Filter) has a `Search` field, so you basically just has to handle that if it's present.
The [`nostr.Filter` type](https://pkg.go.dev/fiatjaf.com/nostr#Filter) has a `Search` field, so you basically just has to handle that if it's present.
It can be tricky to implement fulltext search properly though, so some [eventstores](../core/eventstore) implement it natively, such as [Bluge](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/bluge), [OpenSearch](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/opensearch) and [ElasticSearch](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/elasticsearch) (although for the last two you'll need an instance of these database servers running, while with Bluge it's embedded).
@@ -39,7 +39,7 @@ Other adapters, like [SQLite](https://pkg.go.dev/fiatjaf.com/nostr/eventstore/sq
```go
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent, search.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, func (ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
relay.QueryStored = func (ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event] {
if len(filter.Search) > 0 {
return search.QueryEvents(ctx, filter)
} else {

View File

@@ -41,7 +41,7 @@ After a client is authenticated and opens a new subscription with `REQ` or sends
```go
relay.RejectFilter = append(relay.RejectFilter, func(ctx context.Context, filter nostr.Filter) (bool, string) {
authenticatedUser := khatru.GetAuthed(ctx)
authenticatedUser, isAuthenticated := khatru.GetAuthed(ctx)
})
```
@@ -77,7 +77,7 @@ You can use that to emulate a listener for these events in case you want to keep
case <-ctx.Done():
fmt.Println("connection closed")
case <-conn.Authed:
fmt.Println("authenticated as", conn.AuthedPublicKey)
fmt.Println("authenticated as", conn.AuthedPubKey)
}
}(ctx)
},

View File

@@ -24,7 +24,7 @@ import (
"net/http"
"fiatjaf.com/nostr/eventstore/badger"
"github.com/fiatjaf/khatru"
"fiatjaf.com/nostr/khatru"
)
func main() {
@@ -35,11 +35,7 @@ func main() {
panic(err)
}
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
relay.ReplaceEvent = append(relay.ReplaceEvent, db.ReplaceEvent)
relay.UseEventstore(db)
fmt.Println("running on :3334")
http.ListenAndServe(":3334", relay)
@@ -58,7 +54,7 @@ If you want to use two different adapters at the same time that's easy. Just add
```go
relay.StoreEvent = append(relay.StoreEvent, db1.SaveEvent, db2.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, db1.QueryEvents, db2.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, db1.QueryEvents, db2.QueryEvents)
```
But that will duplicate events on both and then return duplicated events on each query.
@@ -72,28 +68,29 @@ For example, maybe you want kind 1 events in `db1` and kind 30023 events in `db3
```go
relay.StoreEvent = append(relay.StoreEvent, func (ctx context.Context, evt *nostr.Event) error {
switch evt.Kind {
case 1:
return db1.StoreEvent(ctx, evt)
case 30023:
return db30023.StoreEvent(ctx, evt)
case nostr.Kind(1):
return db1.SaveEvent(evt)
case nostr.Kind(30023):
return db30023.SaveEvent(evt)
default:
return nil
}
})
relay.QueryEvents = append(relay.QueryEvents, func (ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
relay.QueryEvents = append(relay.QueryEvents, func (ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event] {
for _, kind := range filter.Kinds {
switch kind {
case 1:
switch nostr.Kind(kind) {
case nostr.Kind(1):
filter1 := filter
filter1.Kinds = []int{1}
return db1.QueryEvents(ctx, filter1)
case 30023:
filter1.Kinds = []nostr.Kind{1}
return db1.QueryEvents(filter1)
case nostr.Kind(30023):
filter30023 := filter
filter30023.Kinds = []int{30023}
return db30023.QueryEvents(ctx, filter30023)
filter30023.Kinds = []nostr.Kind{30023}
return db30023.QueryEvents(filter30023)
default:
return nil, nil
return nil
}
}
return nil
})
```

View File

@@ -6,15 +6,15 @@ outline: deep
[NIP-86](https://nips.nostr.com/86) specifies a set of RPC methods for managing the boring aspects of relays, such as whitelisting or banning users, banning individual events, banning IPs and so on.
All [`khatru.Relay`](https://pkg.go.dev/github.com/fiatjaf/khatru#Relay) instances expose a field `ManagementAPI` with a [`RelayManagementAPI`](https://pkg.go.dev/github.com/fiatjaf/khatru#RelayManagementAPI) instance inside, which can be used for creating handlers for each of the RPC methods.
All [`khatru.Relay`](https://pkg.go.dev/fiatjaf.com/nostr/khatru#Relay) instances expose a field `ManagementAPI` with a [`RelayManagementAPI`](https://pkg.go.dev/fiatjaf.com/nostr/khatru#RelayManagementAPI) instance inside, which can be used for creating handlers for each of the RPC methods.
There is also a generic `RejectAPICall` which is a slice of functions that will be called before any RPC method, if they exist and, if any of them returns true, the request will be rejected.
The most basic implementation of a `RejectAPICall` handler would be one that checks the public key of the caller with a hardcoded public key of the relay owner:
```go
var owner = "<my-own-pubkey>"
var allowedPubkeys = make([]string, 0, 10)
var owner = nostr.MustPubKeyFromHex("<my-own-pubkey>")
var allowedPubkeys = make([]nostr.PubKey, 0, 10)
func main () {
relay := khatru.NewRelay()
@@ -29,16 +29,17 @@ func main () {
}
)
relay.ManagementAPI.AllowPubKey = func(ctx context.Context, pubkey string, reason string) error {
relay.ManagementAPI.AllowPubKey = func(ctx context.Context, pubkey nostr.PubKey, reason string) error {
allowedPubkeys = append(allowedPubkeys, pubkey)
return nil
}
relay.ManagementAPI.BanPubKey = func(ctx context.Context, pubkey string, reason string) error {
relay.ManagementAPI.BanPubKey = func(ctx context.Context, pubkey nostr.PubKey, reason string) error {
idx := slices.Index(allowedPubkeys, pubkey)
if idx == -1 {
return fmt.Errorf("pubkey already not allowed")
}
allowedPubkeys = slices.Delete(allowedPubkeys, idx, idx+1)
return nil
}
}
```
@@ -48,12 +49,12 @@ You can also not provide any `RejectAPICall` handler and do the approval specifi
In the following example any current member can include any other pubkey, and anyone who was added before is able to remove any pubkey that was added afterwards (not a very good idea, but serves as an example).
```go
var allowedPubkeys = []string{"<my-own-pubkey>"}
var allowedPubkeys = []nostr.PubKey{nostr.MustPubKeyFromHex("<my-own-pubkey>")}
func main () {
relay := khatru.NewRelay()
relay.ManagementAPI.AllowPubKey = func(ctx context.Context, pubkey string, reason string) error {
relay.ManagementAPI.AllowPubKey = func(ctx context.Context, pubkey nostr.PubKey, reason string) error {
caller := khatru.GetAuthed(ctx)
if slices.Contains(allowedPubkeys, caller) {
@@ -63,7 +64,7 @@ func main () {
return fmt.Errorf("you're not authorized")
}
relay.ManagementAPI.BanPubKey = func(ctx context.Context, pubkey string, reason string) error {
relay.ManagementAPI.BanPubKey = func(ctx context.Context, pubkey nostr.PubKey, reason string) error {
caller := khatru.GetAuthed(ctx)
callerIdx := slices.Index(allowedPubkeys, caller)
@@ -82,4 +83,3 @@ func main () {
return nil
}
}
```

View File

@@ -7,13 +7,13 @@ outline: deep
Download the library:
```bash
go get github.com/fiatjaf/khatru
go get fiatjaf.com/nostr/khatru
```
Include the library:
```go
import "github.com/fiatjaf/khatru"
import "fiatjaf.com/nostr/khatru"
```
Then in your `main()` function, instantiate a new `Relay`:
@@ -26,7 +26,7 @@ Optionally, set up basic info about the relay that will be returned according to
```go
relay.Info.Name = "my relay"
relay.Info.PubKey = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
relay.Info.PubKey = nostr.MustPubKeyFromHex("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
relay.Info.Description = "this is my custom relay"
relay.Info.Icon = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fliquipedia.net%2Fcommons%2Fimages%2F3%2F35%2FSCProbe.jpg&f=1&nofb=1&ipt=0cbbfef25bce41da63d910e86c3c343e6c3b9d63194ca9755351bb7c2efa3359&ipo=images"
```
@@ -39,10 +39,7 @@ if err := db.Init(); err != nil {
panic(err)
}
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)
relay.UseEventstore(db)
```
These are lists of functions that will be called in order every time an `EVENT` is received, or a `REQ` query is received. You can add more than one handler there, you can have a function that reads from some other server, but just in some cases, you can do anything.
@@ -51,7 +48,7 @@ The next step is adding some protection, because maybe we don't want to allow _a
```go
relay.RejectEvent = append(relay.RejectEvent, func (ctx context.Context, event *nostr.Event) (reject bool, msg string) {
firstHexChar := event.PubKey[0:1]
firstHexChar := event.PubKey.Hex()[0:1]
if firstHexChar == "a" || firstHexChar == "b" || firstHexChar == "c" {
return false, "" // allow
}
@@ -62,12 +59,12 @@ relay.RejectEvent = append(relay.RejectEvent, func (ctx context.Context, event *
We can also make use of some default policies that come bundled with Khatru:
```go
import "github.com/fiatjaf/khatru" // 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))
```
There are many other ways to customize the relay behavior. Take a look at the [`Relay` struct docs](https://pkg.go.dev/github.com/fiatjaf/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.
The last step is actually running the server. Our relay is actually an `http.Handler`, so it can just be ran directly with `http.ListenAndServe()` from the standard library:

View File

@@ -21,7 +21,7 @@ func NewBadgerHints(path string) (*BadgerHints, error) {
opts.Logger = nil
db, err := badger.Open(opts)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to open badger db: %w", err)
}
return &BadgerHints{db: db}, nil
}