Compare commits

...

9 Commits

Author SHA1 Message Date
fiatjaf
0a62169e14 update examples. 2023-11-18 10:37:07 -03:00
fiatjaf
8fd6436ac8 rework nip11 support to be more transparent. 2023-11-18 10:35:08 -03:00
fiatjaf
d2544d0f4d stop uselessly returning NIPs that are obviously supported on NIP-11 response. 2023-11-18 08:03:32 -03:00
fiatjaf
7a3eb6fb08 plugins.PreventLargeTags() 2023-11-13 16:26:27 -03:00
fiatjaf
1abeab4851 filter.Limit is zero by default, so we will short-circuit on negative limits. 2023-11-13 10:55:17 -03:00
fiatjaf
ecb4fc66f8 shortcut querying when limit == 0 2023-11-12 09:27:36 -03:00
fiatjaf
af24bd2132 fix eose waitgroup on early reject. 2023-11-11 21:15:10 -03:00
fiatjaf
4905a46ccd make filter rejection actually work and move logic to a separate file. 2023-11-11 21:08:39 -03:00
fiatjaf
21e0c559f7 simplify event rejection code. 2023-11-11 18:00:04 -03:00
9 changed files with 128 additions and 128 deletions

View File

@@ -31,10 +31,10 @@ func main() {
relay := khatru.NewRelay()
// set up some basic properties (will be returned on the NIP-11 endpoint)
relay.Name = "my relay"
relay.PubKey = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
relay.Description = "this is my custom relay"
relay.IconURL = "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"
relay.Info.Name = "my relay"
relay.Info.PubKey = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
relay.Info.Description = "this is my custom relay"
relay.Info.IconURL = "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)

View File

@@ -13,22 +13,15 @@ func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) error {
return fmt.Errorf("event is nil")
}
msg := ""
rejecting := false
for _, reject := range rl.RejectEvent {
rejecting, msg = reject(ctx, evt)
if rejecting {
break
if reject, msg := reject(ctx, evt); reject {
if msg == "" {
msg = "no reason"
}
return fmt.Errorf(msg)
}
}
if rejecting {
if msg == "" {
msg = "no reason"
}
return fmt.Errorf(msg)
}
if 20000 <= evt.Kind && evt.Kind < 30000 {
// do not store ephemeral events
} else {

View File

@@ -15,10 +15,10 @@ func main() {
relay := khatru.NewRelay()
// set up some basic properties (will be returned on the NIP-11 endpoint)
relay.Name = "my relay"
relay.PubKey = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
relay.Description = "this is my custom relay"
relay.IconURL = "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"
relay.Info.Name = "my relay"
relay.Info.PubKey = "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)

4
go.mod
View File

@@ -5,8 +5,7 @@ go 1.21.0
require (
github.com/fasthttp/websocket v1.5.3
github.com/fiatjaf/eventstore v0.1.0
github.com/gobwas/ws v1.2.0
github.com/nbd-wtf/go-nostr v0.25.1
github.com/nbd-wtf/go-nostr v0.25.7
github.com/puzpuzpuz/xsync/v2 v2.5.1
github.com/rs/cors v1.7.0
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
@@ -30,6 +29,7 @@ require (
github.com/fatih/structs v1.1.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect

4
go.sum
View File

@@ -90,8 +90,8 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/nbd-wtf/go-nostr v0.25.1 h1:YTLTDUgngfzd3qQ0fWmQmq20flwnGtHH0g0Q8S3HlW4=
github.com/nbd-wtf/go-nostr v0.25.1/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0=
github.com/nbd-wtf/go-nostr v0.25.7 h1:DcGOSgKVr/L6w62tRtKeV2t46sRyFcq9pWcyIFkh0eM=
github.com/nbd-wtf/go-nostr v0.25.7/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

View File

@@ -7,12 +7,12 @@ import (
"encoding/hex"
"encoding/json"
"net/http"
"strings"
"sync"
"time"
"github.com/fasthttp/websocket"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip11"
"github.com/nbd-wtf/go-nostr/nip42"
)
@@ -165,30 +165,7 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
ws.WriteJSON(nostr.NoticeEnvelope("failed to decode filter"))
continue
}
filter := filters[i]
// overwrite the filter (for example, to eliminate some kinds or tags that we know we don't support)
for _, ovw := range rl.OverwriteCountFilter {
ovw(ctx, &filter)
}
// then check if we'll reject this filter
for _, reject := range rl.RejectCountFilter {
if rejecting, msg := reject(ctx, filter); rejecting {
ws.WriteJSON(nostr.NoticeEnvelope(msg))
continue
}
}
// run the functions to count (generally it will be just one)
for _, count := range rl.CountEvents {
res, err := count(ctx, filter)
if err != nil {
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
}
total += res
}
total += rl.handleCountRequest(ctx, ws, filters[i])
}
ws.WriteJSON([]interface{}{"COUNT", id, map[string]int64{"count": total}})
@@ -205,57 +182,13 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
eose.Add(len(request[2:]))
for i, filterReq := range request[2:] {
if err := json.Unmarshal(
filterReq,
&filters[i],
); err != nil {
if err := json.Unmarshal(filterReq, &filters[i]); err != nil {
ws.WriteJSON(nostr.NoticeEnvelope("failed to decode filter"))
eose.Done()
continue
}
filter := filters[i]
// overwrite the filter (for example, to eliminate some kinds or
// that we know we don't support)
for _, ovw := range rl.OverwriteFilter {
ovw(ctx, &filter)
}
// then check if we'll reject this filter (we apply this after overwriting
// because we may, for example, remove some things from the incoming filters
// that we know we don't support, and then if the end result is an empty
// filter we can just reject it)
for _, reject := range rl.RejectFilter {
if rejecting, msg := reject(ctx, filter); rejecting {
ws.WriteJSON(nostr.NoticeEnvelope(msg))
continue
}
}
// run the functions to query events (generally just one,
// but we might be fetching stuff from multiple places)
eose.Add(len(rl.QueryEvents))
for _, query := range rl.QueryEvents {
ch, err := query(ctx, filter)
if err != nil {
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
eose.Done()
continue
}
go func(ch chan *nostr.Event) {
for event := range ch {
for _, ovw := range rl.OverwriteResponseEvent {
ovw(ctx, event)
}
ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &id, Event: *event})
}
eose.Done()
}(ch)
}
eose.Done()
go rl.handleRequest(ctx, id, &eose, ws, filters[i])
}
go func() {
@@ -306,7 +239,7 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
case <-ticker.C:
err := ws.WriteMessage(websocket.PingMessage, nil)
if err != nil {
if err.Error() != "use of closed network connection" {
if !strings.HasSuffix(err.Error(), "use of closed network connection") {
rl.Log.Printf("error writing ping: %v; closing websocket\n", err)
}
return
@@ -318,29 +251,5 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
func (rl *Relay) HandleNIP11(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/nostr+json")
supportedNIPs := []int{9, 11, 12, 15, 16, 20, 33}
if rl.ServiceURL != "" {
supportedNIPs = append(supportedNIPs, 42)
}
if rl.CountEvents != nil {
supportedNIPs = append(supportedNIPs, 45)
}
info := nip11.RelayInformationDocument{
Name: rl.Name,
Description: rl.Description,
PubKey: rl.PubKey,
Contact: rl.Contact,
Icon: rl.IconURL,
SupportedNIPs: supportedNIPs,
Software: "https://github.com/trailriver/khatru",
Version: "n/a",
}
for _, edit := range rl.EditInformation {
edit(r.Context(), &info)
}
json.NewEncoder(w).Encode(info)
json.NewEncoder(w).Encode(rl.Info)
}

View File

@@ -23,6 +23,20 @@ func PreventTooManyIndexableTags(max int) func(context.Context, *nostr.Event) (b
}
}
// PreventLargeTags rejects events that have indexable tag values greater than maxTagValueLen.
func PreventLargeTags(maxTagValueLen int) func(context.Context, *nostr.Event) (bool, string) {
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
for _, tag := range event.Tags {
if len(tag) > 1 && len(tag[0]) == 1 {
if len(tag[1]) > maxTagValueLen {
return true, "event contains too large tags"
}
}
}
return false, ""
}
}
// RestrictToSpecifiedKinds returns a function that can be used as a RejectFilter that will reject
// any events with kinds different than the specified ones.
func RestrictToSpecifiedKinds(kinds ...uint16) func(context.Context, *nostr.Event) (bool, string) {

View File

@@ -17,6 +17,11 @@ func NewRelay() *Relay {
return &Relay{
Log: log.New(os.Stderr, "[khatru-relay] ", log.LstdFlags),
Info: &nip11.RelayInformationDocument{
Software: "https://github.com/fiatjaf/khatru",
Version: "n/a",
},
upgrader: websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
@@ -34,12 +39,7 @@ func NewRelay() *Relay {
}
type Relay struct {
Name string
Description string
PubKey string
Contact string
ServiceURL string // required for nip-42
IconURL string
ServiceURL string // required for nip-42
RejectEvent []func(ctx context.Context, event *nostr.Event) (reject bool, msg string)
RejectFilter []func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
@@ -52,11 +52,13 @@ type Relay struct {
DeleteEvent []func(ctx context.Context, event *nostr.Event) error
QueryEvents []func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)
CountEvents []func(ctx context.Context, filter nostr.Filter) (int64, error)
EditInformation []func(ctx context.Context, info *nip11.RelayInformationDocument)
OnAuth []func(ctx context.Context, pubkey string)
OnConnect []func(ctx context.Context)
OnEventSaved []func(ctx context.Context, event *nostr.Event)
// editing info will affect
Info *nip11.RelayInformationDocument
// Default logger, as set by NewServer, is a stdlib logger prefixed with "[khatru-relay] ",
// outputting to stderr.
Log *log.Logger

82
serve-req.go Normal file
View File

@@ -0,0 +1,82 @@
package khatru
import (
"context"
"sync"
"github.com/nbd-wtf/go-nostr"
)
func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGroup, ws *WebSocket, filter nostr.Filter) {
defer eose.Done()
// overwrite the filter (for example, to eliminate some kinds or
// that we know we don't support)
for _, ovw := range rl.OverwriteFilter {
ovw(ctx, &filter)
}
if filter.Limit < 0 {
return
}
// then check if we'll reject this filter (we apply this after overwriting
// because we may, for example, remove some things from the incoming filters
// that we know we don't support, and then if the end result is an empty
// filter we can just reject it)
for _, reject := range rl.RejectFilter {
if reject, msg := reject(ctx, filter); reject {
ws.WriteJSON(nostr.NoticeEnvelope(msg))
return
}
}
// run the functions to query events (generally just one,
// but we might be fetching stuff from multiple places)
eose.Add(len(rl.QueryEvents))
for _, query := range rl.QueryEvents {
ch, err := query(ctx, filter)
if err != nil {
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
eose.Done()
continue
}
go func(ch chan *nostr.Event) {
for event := range ch {
for _, ovw := range rl.OverwriteResponseEvent {
ovw(ctx, event)
}
ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &id, Event: *event})
}
eose.Done()
}(ch)
}
}
func (rl *Relay) handleCountRequest(ctx context.Context, ws *WebSocket, filter nostr.Filter) int64 {
// overwrite the filter (for example, to eliminate some kinds or tags that we know we don't support)
for _, ovw := range rl.OverwriteCountFilter {
ovw(ctx, &filter)
}
// then check if we'll reject this filter
for _, reject := range rl.RejectCountFilter {
if rejecting, msg := reject(ctx, filter); rejecting {
ws.WriteJSON(nostr.NoticeEnvelope(msg))
return 0
}
}
// run the functions to count (generally it will be just one)
var subtotal int64 = 0
for _, count := range rl.CountEvents {
res, err := count(ctx, filter)
if err != nil {
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
}
subtotal += res
}
return subtotal
}