Merge remote-tracking branch 'origin/main'

This commit is contained in:
2025-07-21 15:05:19 +01:00
8 changed files with 182 additions and 42 deletions

View File

@@ -3,16 +3,20 @@ package relay
import (
"errors"
"net/http"
"regexp"
"strings"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/interfaces/relay"
"orly.dev/pkg/interfaces/store"
"orly.dev/pkg/protocol/socketapi"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/normalize"
)
var (
NIP20prefixmatcher = regexp.MustCompile(`^\w+: `)
)
// AddEvent processes an incoming event, saves it if valid, and delivers it to
// subscribers.
//
@@ -50,9 +54,7 @@ import (
// - Returns a boolean indicating whether the event was accepted and any
// relevant message.
func (s *Server) AddEvent(
c context.T, rl relay.I, ev *event.E,
hr *http.Request, origin string,
authedPubkey []byte,
c context.T, rl relay.I, ev *event.E, hr *http.Request, origin string,
) (accepted bool, message []byte) {
if ev == nil {
@@ -65,9 +67,12 @@ func (s *Server) AddEvent(
return false, []byte(saveErr.Error())
}
errmsg := saveErr.Error()
if socketapi.NIP20prefixmatcher.MatchString(errmsg) {
if NIP20prefixmatcher.MatchString(errmsg) {
if strings.Contains(errmsg, "tombstone") {
return false, normalize.Error.F("event was deleted, not storing it again")
return false, normalize.Error.F(
"%s event was deleted, not storing it again",
origin,
)
}
if strings.HasPrefix(errmsg, string(normalize.Blocked)) {
return false, []byte(errmsg)

View File

@@ -35,7 +35,20 @@ func NewChallengeWith[V string | []byte](challenge V) *Challenge {
// Label returns the label of a authenvelope.Challenge.
func (en *Challenge) Label() string { return L }
// Write the authenvelope.Challenge to a provided io.Writer.
// Write encodes and writes the Challenge instance to the provided writer.
//
// # Parameters
//
// - w (io.Writer): The destination where the encoded data will be written.
//
// # Return Values
//
// - err (error): An error if writing to the writer fails.
//
// # Expected behaviour
//
// Encodes the Challenge instance into a byte slice using Marshal, logs the
// encoded challenge, and writes it to the provided io.Writer.
func (en *Challenge) Write(w io.Writer) (err error) {
var b []byte
b = en.Marshal(b)
@@ -44,8 +57,26 @@ func (en *Challenge) Write(w io.Writer) (err error) {
return
}
// Marshal a authenvelope.Challenge to minified JSON, appending to a provided destination
// slice. Note that this ensures correct string escaping on the challenge field.
// Marshal encodes the Challenge instance into a byte slice, formatting it as
// a JSON-like structure with a specific label and escaping rules applied to
// its content.
//
// # Parameters
//
// - dst ([]byte): The destination buffer where the encoded data will be written.
//
// # Return Values
//
// - b ([]byte): The byte slice containing the encoded Challenge data.
//
// # Expected behaviour
//
// - Prepares the destination buffer and applies a label to it.
//
// - Escapes the challenge content according to Nostr-specific rules before
// appending it to the output.
//
// - Returns the resulting byte slice with the complete encoded structure.
func (en *Challenge) Marshal(dst []byte) (b []byte) {
b = dst
var err error
@@ -63,9 +94,24 @@ func (en *Challenge) Marshal(dst []byte) (b []byte) {
return
}
// Unmarshal a authenvelope.Challenge from minified JSON, returning the remainder after the
// end of the envelope. Note that this ensures the challenge string was
// correctly escaped by NIP-01 escaping rules.
// Unmarshal parses the provided byte slice and extracts the challenge value,
// leaving any remaining bytes after parsing.
//
// # Parameters
//
// - b ([]byte): The byte slice containing the encoded challenge data.
//
// # Return Values
//
// - r ([]byte): Any remaining bytes after parsing the challenge.
//
// - err (error): An error if parsing fails.
//
// # Expected behaviour
//
// - Extracts the quoted challenge string from the input byte slice.
//
// - Trims any trailing characters following the closing quote.
func (en *Challenge) Unmarshal(b []byte) (r []byte, err error) {
r = b
if en.Challenge, r, err = text2.UnmarshalQuoted(r); chk.E(err) {
@@ -80,8 +126,26 @@ func (en *Challenge) Unmarshal(b []byte) (r []byte, err error) {
return
}
// ParseChallenge reads a authenvelope.Challenge encoded in minified JSON and unpacks it to
// the runtime format.
// ParseChallenge parses the provided byte slice into a new Challenge instance,
// extracting the challenge value and returning any remaining bytes after parsing.
//
// # Parameters
//
// - b ([]byte): The byte slice containing the encoded challenge data.
//
// # Return Values
//
// - t (*Challenge): A pointer to the newly created and populated Challenge
// instance.
//
// - rem ([]byte): Any remaining bytes in the input slice after parsing.
//
// - err (error): An error if parsing fails.
//
// # Expected behaviour
//
// Parses the byte slice into a new Challenge instance using Unmarshal,
// returning any remaining bytes and an error if parsing fails.
func ParseChallenge(b []byte) (t *Challenge, rem []byte, err error) {
t = NewChallenge()
if rem, err = t.Unmarshal(b); chk.E(err) {

View File

@@ -20,12 +20,8 @@ type I interface {
authedPubkey []byte, remote string,
) (allowed *filters.T, accept bool, modified bool)
AddEvent(
c context.T, rl relay.I, ev *event.E, hr *http.Request,
origin string, authedPubkey []byte,
) (
accepted bool,
message []byte,
)
c context.T, rl relay.I, ev *event.E, hr *http.Request, origin string,
) (accepted bool, message []byte)
Context() context.T
Publisher() *publish.S
Publish(c context.T, evt *event.E) (err error)

View File

@@ -0,0 +1,62 @@
package openapi
import (
"net/http"
"github.com/danielgtaylor/huma/v2"
"github.com/danielgtaylor/huma/v2/adapters/humago"
"orly.dev/pkg/protocol/servemux"
"orly.dev/pkg/utils/lol"
)
// ExposeMiddleware adds the http.Request and http.ResponseWriter to the context
// for the Operations handler.
func ExposeMiddleware(ctx huma.Context, next func(huma.Context)) {
lol.Tracer("ExposeMiddleware")
defer func() { lol.Tracer("end ExposeMiddleware") }()
// Unwrap the request and response objects.
r, w := humago.Unwrap(ctx)
ctx = huma.WithValue(ctx, "http-request", r)
ctx = huma.WithValue(ctx, "http-response", w)
next(ctx)
}
// NewHuma creates a new huma.API with a Scalar docs UI, and a middleware that allows methods to
// access the http.Request and http.ResponseWriter.
func NewHuma(
router *servemux.S, name, version, description string,
) (api huma.API) {
lol.Tracer("NewHuma", name, version, description)
defer func() { lol.Tracer("end NewHuma") }()
config := huma.DefaultConfig(name, version)
config.Info.Description = description
config.DocsPath = ""
config.OpenAPIPath = "/api/openapi"
router.HandleFunc(
"/api", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write(
[]byte(`<!DOCTYPE html>
<html lang="en">
<head>
<title>realy HTTP API UI</title>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1" />
</head>
<body>
<script
id="api-reference"
data-url="/api/openapi.json"></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>`),
)
},
)
api = humago.New(router, config)
api.UseMiddleware(ExposeMiddleware)
return
}

View File

@@ -0,0 +1,27 @@
package openapi
import (
"github.com/danielgtaylor/huma/v2"
"orly.dev/pkg/interfaces/server"
"orly.dev/pkg/protocol/servemux"
"orly.dev/pkg/utils/lol"
)
type Operations struct {
server.I
path string
*servemux.S
}
// New creates a new openapi.Operations and registers its methods.
func New(
s server.I, name, version, description string, path string,
sm *servemux.S,
) {
lol.Tracer("New", name, version, description, path)
defer func() { lol.Tracer("end New") }()
a := NewHuma(sm, name, version, description)
huma.AutoRegister(a, &Operations{I: s, path: path})
return
}

View File

@@ -161,16 +161,6 @@ func (a *A) HandleEvent(
if len(split) != 3 {
continue
}
// Check if the deletion event is trying to delete itself
if bytes.Equal(split[2], env.E.Id) {
if err = Ok.Blocked(
a, env,
"deletion event cannot reference its own ID",
); chk.E(err) {
return
}
return
}
var pk []byte
if pk, err = hex.DecAppend(nil, split[1]); chk.E(err) {
if err = Ok.Invalid(
@@ -185,7 +175,8 @@ func (a *A) HandleEvent(
kin := ints.New(uint16(0))
if _, err = kin.Unmarshal(split[0]); chk.E(err) {
if err = Ok.Invalid(
a, env, "delete event a tag kind value invalid: %s",
a, env, "delete event a tag kind value "+
"invalid: %s",
t.Value(),
); chk.E(err) {
return
@@ -195,7 +186,8 @@ func (a *A) HandleEvent(
kk := kind.New(kin.Uint16())
if kk.Equal(kind.Deletion) {
if err = Ok.Blocked(
a, env, "delete event kind may not be deleted",
a, env, "delete event kind may not be "+
"deleted",
); chk.E(err) {
return
}
@@ -204,7 +196,8 @@ func (a *A) HandleEvent(
if !kk.IsParameterizedReplaceable() {
if err = Ok.Error(
a, env,
"delete tags with a tags containing non-parameterized-replaceable events can't be processed",
"delete tags with a tags containing "+
"non-parameterized-replaceable events can't be processed",
); chk.E(err) {
return
}
@@ -325,9 +318,7 @@ func (a *A) HandleEvent(
}
}
var reason []byte
ok, reason = srv.AddEvent(
c, rl, env.E, a.Req(), a.RealRemote(), a.Listener.AuthedPubkey(),
)
ok, reason = srv.AddEvent(c, rl, env.E, a.Req(), a.RealRemote())
log.I.F("event %0x added %v, %s", env.E.Id, ok, reason)
if err = okenvelope.NewFrom(env.E.Id, ok).Write(a.Listener); chk.E(err) {
return

View File

@@ -26,7 +26,7 @@ import (
// corresponding handler method, generates a notice for errors or unknown types,
// logs the notice, and writes it back to the listener if required.
func (a *A) HandleMessage(msg []byte) {
log.T.F("received message:\n%s", string(msg))
log.T.F("%s received message:\n%s", a.Listener.RealRemote(), string(msg))
var notice []byte
var err error
var t string

View File

@@ -10,16 +10,11 @@ import (
"orly.dev/pkg/protocol/ws"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/log"
"regexp"
"sync"
)
const Type = "socketapi"
var (
NIP20prefixmatcher = regexp.MustCompile(`^\w+: `)
)
// Map is a map of filters associated with a collection of ws.Listener
// connections.
type Map map[*ws.Listener]map[string]*filters.T