diff --git a/go.mod b/go.mod index c402ba9..83010ec 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.21.4 require ( github.com/fasthttp/websocket v1.5.7 - github.com/fiatjaf/eventstore v0.3.8 - github.com/nbd-wtf/go-nostr v0.30.0 + github.com/fiatjaf/eventstore v0.5.0 + github.com/nbd-wtf/go-nostr v0.34.2 github.com/puzpuzpuz/xsync/v3 v3.0.2 github.com/rs/cors v1.7.0 ) @@ -37,7 +37,7 @@ require ( github.com/google/flatbuffers v23.5.26+incompatible // indirect github.com/jmoiron/sqlx v1.3.5 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.17.3 // indirect + github.com/klauspost/compress v1.17.8 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-sqlite3 v1.14.18 // indirect @@ -51,6 +51,6 @@ require ( go.opencensus.io v0.24.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/net v0.18.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.20.0 // indirect google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/go.sum b/go.sum index c4dbf4c..3da7710 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,8 @@ github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KH github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/fiatjaf/eventstore v0.3.8 h1:q4jcN95O2CVA+wP47V25BcVSNvjfOiPPIWgPmQ6hTRk= -github.com/fiatjaf/eventstore v0.3.8/go.mod h1:Qsm5loQICkazpsj8tQmcOK95AVkQQNF09Xx/NS/Biow= +github.com/fiatjaf/eventstore v0.5.0 h1:s+oROGUylAJhntIAPLgLekpTtxpExNd+QhSw0tby7Es= +github.com/fiatjaf/eventstore v0.5.0/go.mod h1:A3SgQ8hwDjZuhZ1aFT250BA70EsWsTIw0KRjm6PDh0w= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= @@ -103,8 +103,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA= -github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -113,8 +113,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.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/nbd-wtf/go-nostr v0.30.0 h1:rN085pe4IxmSBVht8LChZbWLggonjA8hPIk8l4/+Hjk= -github.com/nbd-wtf/go-nostr v0.30.0/go.mod h1:tiKJY6fWYSujbTQb201Y+IQ3l4szqYVt+fsTnsm7FCk= +github.com/nbd-wtf/go-nostr v0.34.2 h1:9b4qZ29DhQf9xEWN8/7zfDD868r1jFbpjrR3c+BHc+E= +github.com/nbd-wtf/go-nostr v0.34.2/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= 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= @@ -133,8 +133,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -184,8 +184,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/handlers.go b/handlers.go index 0c94ad6..d14114e 100644 --- a/handlers.go +++ b/handlers.go @@ -5,7 +5,6 @@ import ( "crypto/rand" "crypto/sha256" "encoding/hex" - "encoding/json" "errors" "net/http" "strings" @@ -28,6 +27,8 @@ func (rl *Relay) ServeHTTP(w http.ResponseWriter, r *http.Request) { rl.HandleWebsocket(w, r) } else if r.Header.Get("Accept") == "application/nostr+json" { cors.AllowAll().Handler(http.HandlerFunc(rl.HandleNIP11)).ServeHTTP(w, r) + } else if r.Header.Get("Accept") == "application/nostr+json+rpc" { + cors.AllowAll().Handler(http.HandlerFunc(rl.HandleNIP86)).ServeHTTP(w, r) } else { rl.serveMux.ServeHTTP(w, r) } @@ -277,22 +278,3 @@ 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") - - info := *rl.Info - - if len(rl.DeleteEvent) > 0 { - info.SupportedNIPs = append(info.SupportedNIPs, 9) - } - if len(rl.CountEvents) > 0 { - info.SupportedNIPs = append(info.SupportedNIPs, 45) - } - - for _, ovw := range rl.OverwriteRelayInformation { - info = ovw(r.Context(), r, info) - } - - json.NewEncoder(w).Encode(info) -} diff --git a/nip11.go b/nip11.go new file mode 100644 index 0000000..87bc5c4 --- /dev/null +++ b/nip11.go @@ -0,0 +1,25 @@ +package khatru + +import ( + "encoding/json" + "net/http" +) + +func (rl *Relay) HandleNIP11(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/nostr+json") + + info := *rl.Info + + if len(rl.DeleteEvent) > 0 { + info.SupportedNIPs = append(info.SupportedNIPs, 9) + } + if len(rl.CountEvents) > 0 { + info.SupportedNIPs = append(info.SupportedNIPs, 45) + } + + for _, ovw := range rl.OverwriteRelayInformation { + info = ovw(r.Context(), r, info) + } + + json.NewEncoder(w).Encode(info) +} diff --git a/nip86.go b/nip86.go new file mode 100644 index 0000000..8f137ed --- /dev/null +++ b/nip86.go @@ -0,0 +1,211 @@ +package khatru + +import ( + "encoding/json" + "fmt" + "net" + "net/http" + "reflect" + "strings" + + "github.com/nbd-wtf/go-nostr/nip86" +) + +type RelayManagementAPI struct { + BanPubKey func(pubkey string, reason string) error + ListBannedPubKeys func() ([]nip86.PubKeyReason, error) + AllowPubKey func(pubkey string, reason string) error + ListAllowedPubKeys func() ([]nip86.PubKeyReason, error) + ListEventsNeedingModeration func() ([]nip86.IDReason, error) + AllowEvent func(id string, reason string) error + BanEvent func(id string, reason string) error + ListBannedEvents func() ([]nip86.IDReason, error) + ChangeRelayName func(name string) error + ChangeRelayDescription func(desc string) error + ChangeRelayIcon func(icon string) error + AllowKind func(kind int) error + DisallowKind func(kind int) error + ListAllowedKinds func() ([]int, error) + BlockIP func(ip net.IP, reason string) error + UnblockIP func(ip net.IP, reason string) error + ListBlockedIPs func() ([]nip86.IPReason, error) +} + +func (rl *Relay) HandleNIP86(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/nostr+json+rpc") + + var req nip86.Request + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid json body", 400) + return + } + + mp, err := nip86.DecodeRequest(req) + if err != nil { + http.Error(w, "invalid params: "+err.Error(), 400) + return + } + + var resp nip86.Response + if _, ok := mp.(nip86.SupportedMethods); ok { + mat := reflect.TypeOf(rl.ManagementAPI) + mav := reflect.ValueOf(rl.ManagementAPI) + + methods := make([]string, 0, mat.NumField()) + for i := 0; i < mat.NumField(); i++ { + field := mat.Field(i) + + // danger: this assumes the struct fields are appropriately named + methodName := strings.ToLower(field.Name) + + // assign this only if the function was defined + if mav.Field(i).Interface() != nil { + methods[i] = methodName + } + } + resp.Result = methods + } else { + switch thing := mp.(type) { + case nip86.BanPubKey: + if rl.ManagementAPI.BanPubKey == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if err := rl.ManagementAPI.BanPubKey(thing.PubKey, thing.Reason); err != nil { + resp.Error = err.Error() + } else { + resp.Result = true + } + case nip86.ListBannedPubKeys: + if rl.ManagementAPI.ListBannedPubKeys == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if result, err := rl.ManagementAPI.ListBannedPubKeys(); err != nil { + resp.Error = err.Error() + } else { + resp.Result = result + } + case nip86.AllowPubKey: + if rl.ManagementAPI.AllowPubKey == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if err := rl.ManagementAPI.AllowPubKey(thing.PubKey, thing.Reason); err != nil { + resp.Error = err.Error() + } else { + resp.Result = true + } + case nip86.ListAllowedPubKeys: + if rl.ManagementAPI.ListAllowedPubKeys == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if result, err := rl.ManagementAPI.ListAllowedPubKeys(); err != nil { + resp.Error = err.Error() + } else { + resp.Result = result + } + case nip86.BanEvent: + if rl.ManagementAPI.BanEvent == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if err := rl.ManagementAPI.BanEvent(thing.ID, thing.Reason); err != nil { + resp.Error = err.Error() + } else { + resp.Result = true + } + case nip86.AllowEvent: + if rl.ManagementAPI.AllowEvent == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if err := rl.ManagementAPI.AllowEvent(thing.ID, thing.Reason); err != nil { + resp.Error = err.Error() + } else { + resp.Result = true + } + case nip86.ListEventsNeedingModeration: + if rl.ManagementAPI.ListEventsNeedingModeration == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if result, err := rl.ManagementAPI.ListEventsNeedingModeration(); err != nil { + resp.Error = err.Error() + } else { + resp.Result = result + } + case nip86.ListBannedEvents: + if rl.ManagementAPI.ListBannedEvents == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if result, err := rl.ManagementAPI.ListEventsNeedingModeration(); err != nil { + resp.Error = err.Error() + } else { + resp.Result = result + } + case nip86.ChangeRelayName: + if rl.ManagementAPI.ChangeRelayName == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if err := rl.ManagementAPI.ChangeRelayName(thing.Name); err != nil { + resp.Error = err.Error() + } else { + resp.Result = true + } + case nip86.ChangeRelayDescription: + if rl.ManagementAPI.ChangeRelayDescription == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if err := rl.ManagementAPI.ChangeRelayDescription(thing.Description); err != nil { + resp.Error = err.Error() + } else { + resp.Result = true + } + case nip86.ChangeRelayIcon: + if rl.ManagementAPI.ChangeRelayIcon == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if err := rl.ManagementAPI.ChangeRelayIcon(thing.IconURL); err != nil { + resp.Error = err.Error() + } else { + resp.Result = true + } + case nip86.AllowKind: + if rl.ManagementAPI.AllowKind == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if err := rl.ManagementAPI.AllowKind(thing.Kind); err != nil { + resp.Error = err.Error() + } else { + resp.Result = true + } + case nip86.DisallowKind: + if rl.ManagementAPI.DisallowKind == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if err := rl.ManagementAPI.DisallowKind(thing.Kind); err != nil { + resp.Error = err.Error() + } else { + resp.Result = true + } + case nip86.ListAllowedKinds: + if rl.ManagementAPI.ListAllowedKinds == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if result, err := rl.ManagementAPI.ListAllowedKinds(); err != nil { + resp.Error = err.Error() + } else { + resp.Result = result + } + case nip86.BlockIP: + if rl.ManagementAPI.BlockIP == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if err := rl.ManagementAPI.BlockIP(thing.IP, thing.Reason); err != nil { + resp.Error = err.Error() + } else { + resp.Result = true + } + case nip86.UnblockIP: + if rl.ManagementAPI.UnblockIP == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if err := rl.ManagementAPI.UnblockIP(thing.IP, thing.Reason); err != nil { + resp.Error = err.Error() + } else { + resp.Result = true + } + case nip86.ListBlockedIPs: + if rl.ManagementAPI.ListBlockedIPs == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if result, err := rl.ManagementAPI.ListBlockedIPs(); err != nil { + resp.Error = err.Error() + } else { + resp.Result = result + } + default: + resp.Error = fmt.Sprintf("method '%s' not known", mp.MethodName()) + } + } + + json.NewEncoder(w).Encode(resp) +} diff --git a/relay.go b/relay.go index d6e965a..f7e72be 100644 --- a/relay.go +++ b/relay.go @@ -20,7 +20,7 @@ func NewRelay() *Relay { Info: &nip11.RelayInformationDocument{ Software: "https://github.com/fiatjaf/khatru", Version: "n/a", - SupportedNIPs: []int{1, 11, 42, 70}, + SupportedNIPs: []int{1, 11, 42, 70, 86}, }, upgrader: websocket.Upgrader{ @@ -60,7 +60,10 @@ type Relay struct { OnEventSaved []func(ctx context.Context, event *nostr.Event) OnEphemeralEvent []func(ctx context.Context, event *nostr.Event) - // editing info will affect + // 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] ",