khatru

package
v0.0.0-...-f50b7b0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 16, 2026 License: Unlicense Imports: 42 Imported by: 17

README

khatru, a relay framework docs badge

Run Tests Go Reference Go Report Card

Khatru makes it easy to write very very custom relays:

  • custom event or filter acceptance policies
  • custom AUTH handlers
  • custom storage and pluggable databases
  • custom webpages and other HTTP handlers

Here's a sample:

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"

	"fiatjaf.com/nostr"
	"fiatjaf.com/nostr/khatru"
)

func main() {
	// create the relay instance
	relay := khatru.NewRelay()

	// set up some basic properties (will be returned on the NIP-11 endpoint)
	relay.Info.Name = "my relay"
	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[nostr.ID]*nostr.Event, 120)

	// set up the basic relay functions
	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
					}
				}
			}
		}
	}
	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,
		// built-in policies
		policies.ValidateKind,
		policies.RejectUnprefixedNostrReferences,

		// define your own policies
		func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
			if event.PubKey == nostr.MustPubKeyFromHex("fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52") {
				return true, "we don't allow this person to write here"
			}
			return false, "" // anyone else can
		},
	)

	// you can request auth by rejecting an event or a request with the prefix "auth-required: "
	relay.RejectFilter = append(relay.RejectFilter,
		// built-in policies
		policies.NoComplexFilters,

		// define your own policies
		func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
			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"
			// (this will cause an AUTH message to be sent and then a CLOSED message such that clients can
			//  authenticate and then request again)
		},
	)
	// check the docs for more goodies!

	mux := relay.Router()
	// set up other http handlers
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("content-type", "text/html")
		fmt.Fprintf(w, `<b>welcome</b> to my relay!`)
	})

	// start the server
	fmt.Println("running on :3334")
	http.ListenAndServe(":3334", relay)
}
But I don't want to write my own database!

Fear no more. Using the fiatjaf.com/nostr/eventstore module you get a bunch of compatible databases out of the box and you can just plug them into your relay. For example, lmdb:

	db := lmdb.LMDBackend{Path: "/tmp/khatru-lmdb-tmp"}
	if err := db.Init(); err != nil {
		panic(err)
	}

	relay.UseEventstore(db, 500)
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 fiatjaf.com/nostr/khatru/policies package and also a handpicked selection of base sane defaults, which you can apply with:

	policies.ApplySaneDefaults(relay)

Contributions to this are very much welcomed.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNothingToDelete = errors.New("blocked: nothing to delete")
	ErrNotAuthor       = errors.New("blocked: you are not the author of this event")
)
View Source
var ErrSubscriptionClosedByClient = errors.New("subscription closed by client")

Functions

func GetAllAuthed

func GetAllAuthed(ctx context.Context) []nostr.PubKey

GetAllAuthed returns all authenticated public keys.

In a NIP-86 context it returns the single pubkey that authenticated for that method call.

func GetAuthed

func GetAuthed(ctx context.Context) (nostr.PubKey, bool)

GetAuthed returns the last pubkey to have authenticated. Returns false if no one has.

In a NIP-86 context it returns the single pubkey that have authenticated for that specific method call.

func GetIP

func GetIP(ctx context.Context) string

func GetIPFromRequest

func GetIPFromRequest(r *http.Request) string

func GetSubscriptionID

func GetSubscriptionID(ctx context.Context) string

func IsAuthed

func IsAuthed(ctx context.Context, pubkey nostr.PubKey) bool

IsAuthed checks if the given public key is among the multiple that may have potentially authenticated.

func IsInternalCall

func IsInternalCall(ctx context.Context) bool

IsInternalCall returns true when a call to QueryEvents, for example, is being made because of a deletion or expiration request.

func IsNegentropySession

func IsNegentropySession(ctx context.Context) bool

func RequestAuth

func RequestAuth(ctx context.Context)

func SendNotice

func SendNotice(ctx context.Context, msg string)

func SetNegentropy

func SetNegentropy(ctx context.Context) context.Context

Types

type ClientInfo

type ClientInfo struct {
	ID                string
	IP                string
	UserAgent         string
	Origin            string
	Authenticated     []nostr.PubKey
	SubscriptionCount int
}

type ClientSnapshot

type ClientSnapshot struct {
	ClientInfo
	Subscriptions []SubscriptionInfo
}

type NegentropySession

type NegentropySession struct {
	// contains filtered or unexported fields
}

type Relay

type Relay struct {

	// setting this variable overwrites the hackish workaround we do to try to figure out our own base URL.
	// it also ensures the relay stuff is served only from that path and not from any path possible.
	ServiceURL string

	// hooks that will be called at various times
	OnEvent                   func(ctx context.Context, event nostr.Event) (reject bool, msg string)
	StoreEvent                func(ctx context.Context, event nostr.Event) error
	ReplaceEvent              func(ctx context.Context, event nostr.Event) error
	DeleteEvent               func(ctx context.Context, id nostr.ID) error
	OnEventSaved              func(ctx context.Context, event nostr.Event)
	OnEventDeleted            func(ctx context.Context, deleted nostr.Event)
	OnEphemeralEvent          func(ctx context.Context, event nostr.Event)
	OnRequest                 func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
	OnCount                   func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
	QueryStored               func(ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event]
	Count                     func(ctx context.Context, filter nostr.Filter) (uint32, error)
	CountHLL                  func(ctx context.Context, filter nostr.Filter, offset int) (uint32, *hyperloglog.HyperLogLog, error)
	RejectConnection          func(r *http.Request) bool
	OnConnect                 func(ctx context.Context)
	OnDisconnect              func(ctx context.Context)
	OverwriteRelayInformation func(ctx context.Context, r *http.Request, info nip11.RelayInformationDocument) nip11.RelayInformationDocument
	PreventBroadcast          func(ws *WebSocket, filter nostr.Filter, event nostr.Event) bool

	// this can be ignored unless you know what you're doing
	ChallengePrefix string

	// setting up handlers here will enable these methods
	ManagementAPI RelayManagementAPI

	// editing info will affect the NIP-11 responses
	Info *nip11.RelayInformationDocument

	// Default logger, as set by NewServer, is a stdlib logger prefixed with "[khatru-relay] ",
	// outputting to stderr.
	Log *log.Logger

	// set this to true to support negentropy
	Negentropy bool

	// in case you call Server.Start
	Addr string

	// websocket options
	WriteWait               time.Duration // Time allowed to write a message to the peer.
	PongWait                time.Duration // Time allowed to read the next pong message from the peer.
	PingPeriod              time.Duration // Send pings to peer with this period. Must be less than pongWait.
	MaxMessageSize          int64         // Maximum message size allowed from peer.
	MaxAuthenticatedClients int
	// contains filtered or unexported fields
}

func NewRelay

func NewRelay() *Relay

func (*Relay) AddEvent

func (rl *Relay) AddEvent(ctx context.Context, evt nostr.Event) (skipBroadcast bool, writeError error)

AddEvent sends an event through then normal add pipeline, as if it was received from a websocket.

func (*Relay) BroadcastEvent

func (rl *Relay) BroadcastEvent(evt nostr.Event) int

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

func (*Relay) DisableExpirationManager

func (rl *Relay) DisableExpirationManager()

func (*Relay) ForceBroadcastEvent

func (rl *Relay) ForceBroadcastEvent(evt nostr.Event) int

ForceBroadcastEvent is like BroadcastEvent, but it skips the PreventBroadcast hook.

func (*Relay) GetClientSnapshot

func (rl *Relay) GetClientSnapshot(id string) (ClientSnapshot, bool)

func (*Relay) GetListeningFilters

func (rl *Relay) GetListeningFilters() []nostr.Filter

func (*Relay) HandleNIP11

func (rl *Relay) HandleNIP11(w http.ResponseWriter, r *http.Request)

func (*Relay) HandleNIP86

func (rl *Relay) HandleNIP86(w http.ResponseWriter, r *http.Request)

func (*Relay) HandleWebsocket

func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request)

func (*Relay) ListClients

func (rl *Relay) ListClients() []ClientInfo

func (*Relay) Router

func (rl *Relay) Router() *http.ServeMux

func (*Relay) ServeHTTP

func (rl *Relay) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler interface.

func (*Relay) SetRouter

func (rl *Relay) SetRouter(mux *http.ServeMux)

func (*Relay) StartExpirationManager

func (rl *Relay) StartExpirationManager(
	queryStored func(ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event],
	deleteEvent func(ctx context.Context, id nostr.ID) error,
	onDeleteCallback func(ctx context.Context, evt nostr.Event),
)

func (*Relay) Stats

func (rl *Relay) Stats() (clients, listeners int)

Stats returns the current number of connected clients and open listeners.

func (*Relay) UseEventstore

func (rl *Relay) UseEventstore(store eventstore.Store, maxQueryLimit int)

UseEventstore hooks up an eventstore.Store into the relay in the default way. It should be used in 85% of the cases, when you don't want to do any complicated scheme with your event storage.

maxQueryLimit is the default max limit to be enforced when querying events, to prevent users for downloading way too much, setting it to something like 500 or 1000 should be ok in most cases.

func (*Relay) WithServiceURL

func (rl *Relay) WithServiceURL(serviceURL string) http.Handler

type RelayManagementAPI

type RelayManagementAPI struct {
	OnAPICall func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string)

	BanPubKey                   func(ctx context.Context, pubkey nostr.PubKey, reason string) error
	ListBannedPubKeys           func(ctx context.Context) ([]nip86.PubKeyReason, error)
	AllowPubKey                 func(ctx context.Context, pubkey nostr.PubKey, reason string) error
	ListAllowedPubKeys          func(ctx context.Context) ([]nip86.PubKeyReason, error)
	ListEventsNeedingModeration func(ctx context.Context) ([]nip86.IDReason, error)
	AllowEvent                  func(ctx context.Context, id nostr.ID, reason string) error
	BanEvent                    func(ctx context.Context, id nostr.ID, reason string) error
	ListBannedEvents            func(ctx context.Context) ([]nip86.IDReason, error)
	ListAllowedEvents           func(ctx context.Context) ([]nip86.IDReason, error)
	ChangeRelayName             func(ctx context.Context, name string) error
	ChangeRelayDescription      func(ctx context.Context, desc string) error
	ChangeRelayIcon             func(ctx context.Context, icon string) error
	AllowKind                   func(ctx context.Context, kind nostr.Kind) error
	DisallowKind                func(ctx context.Context, kind nostr.Kind) error
	ListAllowedKinds            func(ctx context.Context) ([]nostr.Kind, error)
	ListDisallowedKinds         func(ctx context.Context) ([]nostr.Kind, error)
	BlockIP                     func(ctx context.Context, ip net.IP, reason string) error
	UnblockIP                   func(ctx context.Context, ip net.IP, reason string) error
	ListBlockedIPs              func(ctx context.Context) ([]nip86.IPReason, error)
	Stats                       func(ctx context.Context) (nip86.Response, error)
	GrantAdmin                  func(ctx context.Context, pubkey nostr.PubKey, methods []string) error
	RevokeAdmin                 func(ctx context.Context, pubkey nostr.PubKey, methods []string) error
	Generic                     func(ctx context.Context, request nip86.Request) (nip86.Response, error)
}

type SubscriptionInfo

type SubscriptionInfo struct {
	ID     string
	Filter nostr.Filter
}

type WebSocket

type WebSocket struct {

	// original request
	Request *http.Request

	// this Context will be canceled whenever the connection is closed from the client side or server-side.
	Context context.Context

	// nip42
	Challenge        string
	AuthedPublicKeys []nostr.PubKey
	// contains filtered or unexported fields
}

func GetConnection

func GetConnection(ctx context.Context) *WebSocket

func (*WebSocket) GetID

func (ws *WebSocket) GetID() string

func (*WebSocket) WriteJSON

func (ws *WebSocket) WriteJSON(any any) error

func (*WebSocket) WriteMessage

func (ws *WebSocket) WriteMessage(t int, b []byte) error

Directories

Path Synopsis
examples
basic-lmdb command
blossom command
exclusive command
grasp command
readme-demo command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL