Remove unused protocol packages and interfaces and clean up code
This commit is contained in:
@@ -23,9 +23,9 @@ import (
|
||||
"orly.dev/utils/apputil"
|
||||
)
|
||||
|
||||
// C is the configuration for realy relay. These are read from the environment
|
||||
// if present, or if a .env file is found in ~/.config/realy/ that is read
|
||||
// instead and overrides anything else.
|
||||
// C is the configuration for the relay. These are read from the environment if
|
||||
// present, or if a .env file is found in ~/.config/orly/ that is read instead
|
||||
// and overrides anything else.
|
||||
type C struct {
|
||||
AppName string `env:"ORLY_APP_NAME" default:"orly"`
|
||||
Config string `env:"ORLY_CONFIG_DIR" usage:"location for configuration file, which has the name '.env' to make it harder to delete, and is a standard environment KEY=value<newline>... style"`
|
||||
@@ -103,7 +103,7 @@ func (kv KVSlice) Less(i, j int) bool { return kv[i].Key < kv[j].Key }
|
||||
func (kv KVSlice) Swap(i, j int) { kv[i], kv[j] = kv[j], kv[i] }
|
||||
|
||||
// Compose merges two KVSlice together, replacing the values of earlier keys
|
||||
// with same named KV items later in the slice (enabling compositing two
|
||||
// with the same named KV items later in the slice (enabling compositing two
|
||||
// together as a .env, as well as them being composed as structs.
|
||||
func (kv KVSlice) Compose(kv2 KVSlice) (out KVSlice) {
|
||||
// duplicate the initial KVSlice
|
||||
@@ -128,7 +128,7 @@ out:
|
||||
// standard formatted environment variable key/value pair list, one per line.
|
||||
// Note you must dereference a pointer type to use this. This allows the
|
||||
// composition of the config in this file with an extended form with a
|
||||
// customized variant of realy to produce correct environment variables both
|
||||
// customized variant of the relay to produce correct environment variables both
|
||||
// read and write.
|
||||
func EnvKV(cfg any) (m KVSlice) {
|
||||
t := reflect.TypeOf(cfg)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package app implements the realy nostr relay with a simple follow/mute list authentication scheme and the new HTTP REST based protocol.
|
||||
// Package app implements the orly nostr relay.
|
||||
package app
|
||||
|
||||
import (
|
||||
@@ -74,7 +74,6 @@ func (r *Relay) AcceptReq(
|
||||
c context.T, hr *http.Request, id []byte,
|
||||
ff *filters.T, authedPubkey []byte,
|
||||
) (allowed *filters.T, ok bool, modified bool) {
|
||||
|
||||
allowed = ff
|
||||
ok = true
|
||||
return
|
||||
|
||||
@@ -22,22 +22,8 @@ func (s *Server) addEvent(
|
||||
if ev == nil {
|
||||
return false, normalize.Invalid.F("empty event")
|
||||
}
|
||||
// sto := rl.Storage()
|
||||
// advancedSaver, _ := sto.(relay.AdvancedSaver)
|
||||
// don't allow storing event with protected marker as per nip-70 with auth enabled.
|
||||
// if (s.authRequired || !s.publicReadable) && ev.Tags.ContainsProtectedMarker() {
|
||||
// if len(authedPubkey) == 0 || !bytes.Equal(ev.Pubkey, authedPubkey) {
|
||||
// return false,
|
||||
// []byte(fmt.Sprintf("event with relay marker tag '-' (nip-70 protected event) "+
|
||||
// "may only be published by matching npub: %0x is not %0x",
|
||||
// authedPubkey, ev.Pubkey))
|
||||
// }
|
||||
// }
|
||||
if ev.Kind.IsEphemeral() {
|
||||
} else {
|
||||
// if advancedSaver != nil {
|
||||
// advancedSaver.BeforeSave(c, ev)
|
||||
// }
|
||||
if saveErr := s.Publish(c, ev); saveErr != nil {
|
||||
if errors.Is(saveErr, store.ErrDupEvent) {
|
||||
return false, []byte(saveErr.Error())
|
||||
@@ -55,14 +41,7 @@ func (s *Server) addEvent(
|
||||
return false, []byte(errmsg)
|
||||
}
|
||||
}
|
||||
// if advancedSaver != nil {
|
||||
// advancedSaver.AfterSave(ev)
|
||||
// }
|
||||
}
|
||||
// var authRequired bool
|
||||
// if ar, ok := rl.(relay.Authenticator); ok {
|
||||
// authRequired = ar.AuthRequired()
|
||||
// }
|
||||
// notify subscribers
|
||||
s.listeners.Deliver(ev)
|
||||
accepted = true
|
||||
|
||||
@@ -21,16 +21,16 @@ func (s *Server) handleRelayInfo(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
supportedNIPs := relayinfo.GetList(
|
||||
relayinfo.BasicProtocol,
|
||||
relayinfo.EncryptedDirectMessage,
|
||||
// relayinfo.EncryptedDirectMessage,
|
||||
relayinfo.EventDeletion,
|
||||
relayinfo.RelayInformationDocument,
|
||||
relayinfo.GenericTagQueries,
|
||||
relayinfo.NostrMarketplace,
|
||||
// relayinfo.NostrMarketplace,
|
||||
relayinfo.EventTreatment,
|
||||
relayinfo.CommandResults,
|
||||
// relayinfo.CommandResults,
|
||||
relayinfo.ParameterizedReplaceableEvents,
|
||||
relayinfo.ExpirationTimestamp,
|
||||
relayinfo.ProtectedEvents,
|
||||
// relayinfo.ExpirationTimestamp,
|
||||
// relayinfo.ProtectedEvents,
|
||||
relayinfo.RelayListMetadata,
|
||||
)
|
||||
sort.Sort(supportedNIPs)
|
||||
|
||||
@@ -7,6 +7,6 @@ import (
|
||||
)
|
||||
|
||||
func (s *Server) handleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
a := &socketapi.A{Server: s}
|
||||
a := &socketapi.A{S: s}
|
||||
a.Serve(w, r, s)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,27 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GenerateDescription generates a detailed description containing the provided
|
||||
// text and an optional list of scopes.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// - text: A string representing the base description.
|
||||
//
|
||||
// - scopes: A slice of strings indicating scopes to be included in the
|
||||
// description.
|
||||
//
|
||||
// Return values:
|
||||
//
|
||||
// - A string combining the base description and a formatted list of
|
||||
// scopes, if provided.
|
||||
//
|
||||
// Expected behavior:
|
||||
//
|
||||
// The function appends a formatted list of scopes to the base description if
|
||||
// any scopes are provided. If no scopes are provided, it returns the base
|
||||
// description unchanged. The formatted list of scopes includes each scope
|
||||
// surrounded by backticks and separated by commas.
|
||||
func GenerateDescription(text string, scopes []string) string {
|
||||
if len(scopes) == 0 {
|
||||
return text
|
||||
@@ -16,8 +37,55 @@ func GenerateDescription(text string, scopes []string) string {
|
||||
return text + "<br/><br/>**Scopes**<br/>" + strings.Join(result, ", ")
|
||||
}
|
||||
|
||||
// GetRemoteFromReq retrieves the originating IP address of the client from
|
||||
// an HTTP request, considering standard and non-standard proxy headers.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// - r: The HTTP request object containing details of the client and
|
||||
// routing information.
|
||||
//
|
||||
// Return values:
|
||||
//
|
||||
// - rr: A string value representing the IP address of the originating
|
||||
// remote client.
|
||||
//
|
||||
// Expected behavior:
|
||||
//
|
||||
// The function first checks for the standardized "Forwarded" header (RFC 7239)
|
||||
// to identify the original client IP. If that's not available, it falls back to
|
||||
// the "X-Forwarded-For" header. If both headers are absent, it defaults to
|
||||
// using the request's RemoteAddr.
|
||||
//
|
||||
// For the "Forwarded" header, it extracts the client IP from the "for"
|
||||
// parameter. For the "X-Forwarded-For" header, if it contains one IP, it
|
||||
// returns that. If it contains two IPs, it returns the second.
|
||||
func GetRemoteFromReq(r *http.Request) (rr string) {
|
||||
// reverse proxy should populate this field so we see the remote not the proxy
|
||||
// First check for the standardized Forwarded header (RFC 7239)
|
||||
forwarded := r.Header.Get("Forwarded")
|
||||
if forwarded != "" {
|
||||
// Parse the Forwarded header which can contain multiple parameters
|
||||
//
|
||||
// Format:
|
||||
//
|
||||
// Forwarded: by=<identifier>;for=<identifier>;host=<host>;proto=<http|https>
|
||||
parts := strings.Split(forwarded, ";")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if strings.HasPrefix(part, "for=") {
|
||||
// Extract the client IP from the "for" parameter
|
||||
forValue := strings.TrimPrefix(part, "for=")
|
||||
// Remove quotes if present
|
||||
forValue = strings.Trim(forValue, "\"")
|
||||
// Handle IPv6 addresses which are enclosed in square brackets
|
||||
forValue = strings.Trim(forValue, "[]")
|
||||
return forValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the Forwarded header is not available or doesn't contain "for"
|
||||
// parameter, fall back to X-Forwarded-For
|
||||
rem := r.Header.Get("X-Forwarded-For")
|
||||
if rem == "" {
|
||||
rr = r.RemoteAddr
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/app/realy/publish"
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/interfaces/relay"
|
||||
"orly.dev/interfaces/store"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
type Server interface {
|
||||
AddEvent(
|
||||
c context.T, rl relay.I, ev *event.E, hr *http.Request,
|
||||
origin string, authedPubkey []byte,
|
||||
) (
|
||||
accepted bool,
|
||||
message []byte,
|
||||
)
|
||||
Context() context.T
|
||||
Disconnect()
|
||||
Publisher() *publish.S
|
||||
Publish(c context.T, evt *event.E) (err error)
|
||||
Relay() relay.I
|
||||
Shutdown()
|
||||
Storage() store.I
|
||||
// Options() *options.T
|
||||
// AcceptEvent(
|
||||
// c context.T, ev *event.E, hr *http.Request, origin string,
|
||||
// authedPubkey []byte) (accept bool, notice string, afterSave func())
|
||||
// AdminAuth(r *http.Request,
|
||||
// tolerance ...time.Duration) (authed bool, pubkey []byte)
|
||||
// AuthRequired() bool
|
||||
// Configuration() store.Configuration
|
||||
// Owners() [][]byte
|
||||
// PublicReadable() bool
|
||||
// SetConfiguration(*store.Configuration)
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
// Package options provides some option configurations for the realy relay.
|
||||
// Package options provides some option configurations for the relay.
|
||||
//
|
||||
// None of this package is actually in use, and the skip event function has not been
|
||||
// implemented. In theory this could be used for something but it currently isn't.
|
||||
// None of this package is actually in use, and the skip event function has not
|
||||
// been implemented. In theory this could be used for something but it currently
|
||||
// isn't.
|
||||
package options
|
||||
|
||||
import (
|
||||
@@ -12,8 +13,8 @@ type SkipEventFunc func(*event.E) bool
|
||||
|
||||
// T is a collection of options.
|
||||
type T struct {
|
||||
// SkipEventFunc is in theory a function to test whether an event should not be sent in
|
||||
// response to a query.
|
||||
// SkipEventFunc is in theory a function to test whether an event should not
|
||||
// be sent in response to a query.
|
||||
SkipEventFunc
|
||||
}
|
||||
|
||||
@@ -25,7 +26,8 @@ func Default() *T {
|
||||
return &T{}
|
||||
}
|
||||
|
||||
// WithSkipEventFunc is an options.T generator that adds a function to skip events.
|
||||
// WithSkipEventFunc is an options.T generator that adds a function to skip
|
||||
// events.
|
||||
func WithSkipEventFunc(skipEventFunc func(*event.E) bool) O {
|
||||
return func(o *T) {
|
||||
o.SkipEventFunc = skipEventFunc
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
package publish
|
||||
|
||||
import (
|
||||
"orly.dev/app/realy/publish/publisher"
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/interfaces/publisher"
|
||||
)
|
||||
|
||||
// S is the control structure for the subscription management scheme.
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package publisher
|
||||
|
||||
import (
|
||||
"orly.dev/encoders/event"
|
||||
)
|
||||
|
||||
type Message interface {
|
||||
Type() string
|
||||
}
|
||||
|
||||
type I interface {
|
||||
Message
|
||||
Deliver(ev *event.E)
|
||||
Receive(msg Message)
|
||||
}
|
||||
|
||||
type Publishers []I
|
||||
@@ -2,10 +2,10 @@ package realy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/app/realy/interfaces"
|
||||
"orly.dev/app/realy/publish"
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/interfaces/relay"
|
||||
"orly.dev/interfaces/server"
|
||||
"orly.dev/interfaces/store"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
@@ -28,4 +28,4 @@ func (s *Server) Publisher() *publish.S { return s.listeners }
|
||||
|
||||
func (s *Server) Context() context.T { return s.Ctx }
|
||||
|
||||
var _ interfaces.Server = &Server{}
|
||||
var _ server.S = &Server{}
|
||||
|
||||
@@ -48,7 +48,6 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
|
||||
"maybe replace %s with %s", ev.Serialize(), evt.Serialize(),
|
||||
)
|
||||
if ev.CreatedAt.Int() > evt.CreatedAt.Int() {
|
||||
log.I.S(ev, evt)
|
||||
return errorf.W(string(normalize.Invalid.F("not replacing newer replaceable event")))
|
||||
}
|
||||
// not deleting these events because some clients are retarded
|
||||
@@ -74,8 +73,6 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
|
||||
)
|
||||
},
|
||||
)
|
||||
// replaceable events we don't tombstone when replacing,
|
||||
// so if deleted, old versions can be restored
|
||||
if err = sto.DeleteEvent(c, ev.EventId()); chk.E(err) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,19 +10,17 @@ import (
|
||||
"orly.dev/app/realy/options"
|
||||
"orly.dev/app/realy/publish"
|
||||
"orly.dev/interfaces/relay"
|
||||
"orly.dev/protocol/servemux"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/log"
|
||||
realy_lol "orly.dev/version"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/fasthttp/websocket"
|
||||
"github.com/rs/cors"
|
||||
|
||||
"orly.dev/interfaces/signer"
|
||||
"orly.dev/protocol/openapi"
|
||||
"orly.dev/protocol/socketapi"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
@@ -35,17 +33,9 @@ type Server struct {
|
||||
clientsMu sync.Mutex
|
||||
clients map[*websocket.Conn]struct{}
|
||||
Addr string
|
||||
mux *openapi.ServeMux
|
||||
mux *servemux.S
|
||||
httpServer *http.Server
|
||||
// authRequired bool
|
||||
// publicReadable bool
|
||||
// maxLimit int
|
||||
// admins []signer.I
|
||||
// owners [][]byte
|
||||
listeners *publish.S
|
||||
huma.API
|
||||
// ConfigurationMx sync.Mutex
|
||||
// configuration *store.Configuration
|
||||
listeners *publish.S
|
||||
}
|
||||
|
||||
type ServerParams struct {
|
||||
@@ -69,22 +59,18 @@ func NewServer(sp *ServerParams, opts ...options.O) (s *Server, err error) {
|
||||
return nil, fmt.Errorf("storage init: %w", err)
|
||||
}
|
||||
}
|
||||
serveMux := openapi.NewServeMux()
|
||||
serveMux := servemux.NewServeMux()
|
||||
s = &Server{
|
||||
Ctx: sp.Ctx,
|
||||
Cancel: sp.Cancel,
|
||||
relay: sp.Rl,
|
||||
clients: make(map[*websocket.Conn]struct{}),
|
||||
mux: serveMux,
|
||||
options: op,
|
||||
listeners: publish.New(socketapi.New(), openapi.New()),
|
||||
API: openapi.NewHuma(
|
||||
serveMux, sp.Rl.Name(), realy_lol.V,
|
||||
realy_lol.Description,
|
||||
Ctx: sp.Ctx,
|
||||
Cancel: sp.Cancel,
|
||||
relay: sp.Rl,
|
||||
clients: make(map[*websocket.Conn]struct{}),
|
||||
mux: serveMux,
|
||||
options: op,
|
||||
listeners: publish.New(
|
||||
socketapi.New(),
|
||||
),
|
||||
}
|
||||
// register the http API operations
|
||||
huma.AutoRegister(s.API, openapi.NewOperations(s))
|
||||
go func() {
|
||||
if err := s.relay.Init(); chk.E(err) {
|
||||
s.Shutdown()
|
||||
|
||||
@@ -12,19 +12,19 @@ import (
|
||||
func MonitorResources(c context.T) {
|
||||
tick := time.NewTicker(time.Minute * 15)
|
||||
log.I.Ln("running process", os.Args[0], os.Getpid())
|
||||
// memStats := &runtime.MemStats{}
|
||||
memStats := &runtime.MemStats{}
|
||||
for {
|
||||
select {
|
||||
case <-c.Done():
|
||||
log.D.Ln("shutting down resource monitor")
|
||||
return
|
||||
case <-tick.C:
|
||||
// runtime.ReadMemStats(memStats)
|
||||
runtime.ReadMemStats(memStats)
|
||||
log.D.Ln(
|
||||
"# goroutines", runtime.NumGoroutine(), "# cgo calls",
|
||||
runtime.NumCgoCall(),
|
||||
)
|
||||
// log.D.S(memStats)
|
||||
log.D.S(memStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Package cmd contains the executable applications of the realy suite.
|
||||
package cmd
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
// hexToBytes converts the passed hex string into bytes and will panic if there
|
||||
// is an error. This is only provided for the hard-coded constants so errors in
|
||||
// is an error. This is only provided for the hard-coded constants, so errors in
|
||||
// the source code can be detected. It will only (and must only) be called with
|
||||
// hard-coded values.
|
||||
func hexToBytes(s string) []byte {
|
||||
@@ -28,8 +28,10 @@ func hexToBytes(s string) []byte {
|
||||
}
|
||||
|
||||
// hexToModNScalar converts the passed hex string into a ModNScalar and will
|
||||
// panic if there is an error. This is only provided for the hard-coded
|
||||
// constants so errors in the source code can be detected. It will only (and
|
||||
// panic if there is an error. This is only provided for the hard-coded
|
||||
//
|
||||
// constants, so errors in the source code can be detected. It will only (and
|
||||
//
|
||||
// must only) be called with hard-coded values.
|
||||
func hexToModNScalar(s string) *btcec.ModNScalar {
|
||||
b, err := hex.Dec(s)
|
||||
@@ -44,10 +46,10 @@ func hexToModNScalar(s string) *btcec.ModNScalar {
|
||||
}
|
||||
|
||||
// hexToFieldVal converts the passed hex string into a FieldVal and will panic
|
||||
// if there is an error. This is only provided for the hard-coded constants so
|
||||
// if there is an error. This is only provided for the hard-coded constants, so
|
||||
// errors in the source code can be detected. It will only (and must only) be
|
||||
// called with hard-coded values.
|
||||
func hexToFieldVal(s string) *btcec.btcec {
|
||||
func hexToFieldVal(s string) *btcec.FieldVal {
|
||||
b, err := hex.Dec(s)
|
||||
if err != nil {
|
||||
panic("invalid hex in source file: " + s)
|
||||
@@ -60,8 +62,8 @@ func hexToFieldVal(s string) *btcec.btcec {
|
||||
}
|
||||
|
||||
// fromHex converts the passed hex string into a big integer pointer and will
|
||||
// panic is there is an error. This is only provided for the hard-coded
|
||||
// constants so errors in the source code can bet detected. It will only (and
|
||||
// panic if there is an error. This is only provided for the hard-coded
|
||||
// constants, so errors in the source code can be detected. It will only (and
|
||||
// must only) be called for initialization purposes.
|
||||
func fromHex(s string) *big.Int {
|
||||
if s == "" {
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
package schnorr
|
||||
|
||||
// ErrorKind identifies a kind of error. It has full support for errors.Is
|
||||
// and errors.As, so the caller can directly check against an error kind
|
||||
// when determining the reason for an error.
|
||||
// ErrorKind identifies a kind of error. It has full support for errors.Is and
|
||||
// errors.As, so the caller can directly check against an error kind when
|
||||
// determining the reason for an error.
|
||||
type ErrorKind string
|
||||
|
||||
// These constants are used to identify a specific RuleError.
|
||||
@@ -51,9 +51,9 @@ const (
|
||||
// Error satisfies the error interface and prints human-readable errors.
|
||||
func (err ErrorKind) Error() string { return string(err) }
|
||||
|
||||
// Error identifies an error related to a schnorr signature. It has full
|
||||
// support for errors.Is and errors.As, so the caller can ascertain the
|
||||
// specific reason for the error by checking the underlying error.
|
||||
// Error identifies an error related to a schnorr signature. It has full support
|
||||
// for errors.Is and errors.As, so the caller can ascertain the specific reason
|
||||
// for the error by checking the underlying error.
|
||||
type Error struct {
|
||||
Err error
|
||||
Description string
|
||||
|
||||
@@ -32,8 +32,8 @@ func ParsePubKey(pubKeyStr []byte) (*btcec.PublicKey, error) {
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
// We'll manually prepend the compressed byte so we can re-use the
|
||||
// existing pubkey parsing routine of the main btcec package.
|
||||
// We'll manually prepend the compressed byte so we can re-use the existing
|
||||
// pubkey parsing routine of the main btcec package.
|
||||
var keyCompressed [btcec.PubKeyBytesLenCompressed]byte
|
||||
keyCompressed[0] = secp256k1.PubKeyFormatCompressedEven
|
||||
copy(keyCompressed[1:], pubKeyStr)
|
||||
@@ -41,7 +41,7 @@ func ParsePubKey(pubKeyStr []byte) (*btcec.PublicKey, error) {
|
||||
}
|
||||
|
||||
// SerializePubKey serializes a public key as specified by BIP 340. Public keys
|
||||
// in this format are 32 bytes in length, and are assumed to have an even y
|
||||
// in this format are 32 bytes in length and are assumed to have an even y
|
||||
// coordinate.
|
||||
func SerializePubKey(pub *btcec.PublicKey) []byte {
|
||||
pBytes := pub.SerializeCompressed()
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package schnorr provides custom Schnorr signing and verification via secp256k1.
|
||||
// Package schnorr provides custom Schnorr signing and verification via
|
||||
// secp256k1.
|
||||
//
|
||||
// This package provides data structures and functions necessary to produce and
|
||||
// verify deterministic canonical Schnorr signatures using a custom scheme named
|
||||
// EC-Schnorr-DCRv0 that is described herein. The signatures and implementation
|
||||
// are optimized specifically for the secp256k1 curve. See
|
||||
// EC-Schnorr-DCRv0 that is described herein. The signatures and implementation
|
||||
// are optimized specifically for the secp256k1 curve. See
|
||||
// https://www.secg.org/sec2-v2.pdf for details on the secp256k1 standard.
|
||||
//
|
||||
// It also provides functions to parse and serialize the Schnorr signatures
|
||||
@@ -20,81 +21,83 @@
|
||||
// A Schnorr signature is a digital signature scheme that is known for its
|
||||
// simplicity, provable security and efficient generation of short signatures.
|
||||
//
|
||||
// It provides many advantages over ECDSA signatures that make them ideal for use
|
||||
// with the only real downside being that they are not well standardized at the
|
||||
// time of this writing.
|
||||
// It provides many advantages over ECDSA signatures that make them ideal for
|
||||
// use with the only real downside being that they are not well standardized at
|
||||
// the time of this writing.
|
||||
//
|
||||
// Some of the advantages over ECDSA include:
|
||||
//
|
||||
// - They are linear which makes them easier to aggregate and use in protocols that
|
||||
// build on them such as multi-party signatures, threshold signatures, adaptor
|
||||
// signatures, and blind signatures
|
||||
// - They are provably secure with weaker assumptions than the best known security
|
||||
// proofs for ECDSA
|
||||
// - They are linear which makes them easier to aggregate and use in
|
||||
// protocols that build on them such as multi-party signatures, threshold
|
||||
// signatures, adaptor signatures, and blind signatures
|
||||
// - They are provably secure with weaker assumptions than the best known
|
||||
// security proofs for ECDSA
|
||||
// - Specifically Schnorr signatures are provably secure under SUF-CMA (Strong
|
||||
// Existential Unforgeability under Chosen Message Attack) in the ROM (Random
|
||||
// Oracle Model) which guarantees that as long as the hash function behaves
|
||||
// ideally, the only way to break Schnorr signatures is by solving the ECDLP
|
||||
// (Elliptic Curve Discrete Logarithm Problem).
|
||||
// - Their relatively straightforward and efficient aggregation properties make
|
||||
// them excellent for scalability and allow them to provide some nice secacy
|
||||
// characteristics
|
||||
// Existential Unforgeability under Chosen Message Attack) in the ROM
|
||||
// (Random Oracle Model) which guarantees that as long as the hash
|
||||
// function behaves ideally, the only way to break Schnorr signatures is
|
||||
// by solving the ECDLP (Elliptic Curve Discrete Logarithm Problem).
|
||||
// - Their relatively straightforward and efficient aggregation properties
|
||||
// make them excellent for scalability and allow them to provide some nice
|
||||
// secrecy characteristics
|
||||
// - They support faster batch verification unlike the standardized version of
|
||||
// ECDSA signatures
|
||||
//
|
||||
// # Custom Schnorr-based Signature Scheme
|
||||
//
|
||||
// As mentioned in the overview, the primary downside of Schnorr signatures for
|
||||
// elliptic curves is that they are not standardized as well as ECDSA signatures
|
||||
// which means there are a number of variations that are not compatible with each
|
||||
// other.
|
||||
// elliptic curves is that they are not standardized as well as ECDSA signatures,
|
||||
// which means there are a number of variations that are not compatible with
|
||||
// each other.
|
||||
//
|
||||
// In addition, many of the standardization attempts have various disadvantages
|
||||
// that make them unsuitable for use in Decred. Some of these details and some
|
||||
// insight into the design decisions made are discussed further in the README.md
|
||||
// file.
|
||||
// In addition, many of the standardization attempts have had various
|
||||
// disadvantages that make them unsuitable for use in Decred. Some of these
|
||||
// details and some insight into the design decisions made are discussed further
|
||||
// in the README.md file.
|
||||
//
|
||||
// Consequently, this package implements a custom Schnorr-based signature scheme
|
||||
// named EC-Schnorr-DCRv0 suitable for use in Decred.
|
||||
//
|
||||
// The following provides a high-level overview of the key design features of the
|
||||
// scheme:
|
||||
// The following provides a high-level overview of the key design features of
|
||||
// the scheme:
|
||||
//
|
||||
// - Uses signatures of the form (R, s)
|
||||
// - Produces 64-byte signatures by only encoding the x coordinate of R
|
||||
// - Enforces even y coordinates for R to support efficient verification by
|
||||
// disambiguating the two possible y coordinates
|
||||
// - Canonically encodes by both components of the signature with 32-bytes each
|
||||
// - Uses BLAKE-256 with 14 rounds for the hash function to calculate challenge e
|
||||
// - Canonically encodes by both components of the signature with 32-bytes
|
||||
// each
|
||||
// - Uses BLAKE-256 with 14 rounds for the hash function to calculate
|
||||
// challenge e
|
||||
// - Uses RFC6979 to obviate the need for an entropy source at signing time
|
||||
// - Produces deterministic signatures for a given message and secret key pair
|
||||
//
|
||||
// # EC-Schnorr-DCRv0 Specification
|
||||
//
|
||||
// See the README.md file for the specific details of the signing and verification
|
||||
// algorithm as well as the signature serialization format.
|
||||
// See the README.md file for the specific details of the signing and
|
||||
// verification algorithm as well as the signature serialization format.
|
||||
//
|
||||
// # Future Design Considerations
|
||||
//
|
||||
// It is worth noting that there are some additional optimizations and
|
||||
// modifications that have been identified since the introduction of
|
||||
// EC-Schnorr-DCRv0 that can be made to further harden security for multi-party and
|
||||
// threshold signature use cases as well provide the opportunity for faster
|
||||
// EC-Schnorr-DCRv0 that can be made to further harden security for multi-party
|
||||
// and threshold signature use cases as well provide the opportunity for faster
|
||||
// signature verification with a sufficiently optimized implementation.
|
||||
//
|
||||
// However, the v0 scheme is used in the existing consensus rules and any changes
|
||||
// to the signature scheme would invalidate existing uses. Therefore changes in
|
||||
// this regard will need to come in the form of a v1 signature scheme and be
|
||||
// accompanied by the necessary consensus updates.
|
||||
// However, the v0 scheme is used in the existing consensus rules and any
|
||||
// changes to the signature scheme would invalidate existing uses. Therefore
|
||||
// changes in this regard will need to come in the form of a v1 signature scheme
|
||||
// and be accompanied by the necessary consensus updates.
|
||||
//
|
||||
// # Schnorr use in Decred
|
||||
//
|
||||
// At the time of this writing, Schnorr signatures are not yet in widespread use on
|
||||
// the Decred network, largely due to the current lack of support in wallets and
|
||||
// infrastructure for secure multi-party and threshold signatures.
|
||||
// At the time of this writing, Schnorr signatures are not yet in widespread use
|
||||
// on the Decred network, largely due to the current lack of support in wallets
|
||||
// and infrastructure for secure multi-party and threshold signatures.
|
||||
//
|
||||
// However, the consensus rules and scripting engine supports the necessary
|
||||
// primitives and given many of the beneficial properties of Schnorr signatures, a
|
||||
// good goal is to work towards providing the additional infrastructure to increase
|
||||
// their usage.
|
||||
// primitives and given many of the beneficial properties of Schnorr signatures,
|
||||
// a good goal is to work towards providing the additional infrastructure to
|
||||
// increase their usage.
|
||||
package schnorr
|
||||
|
||||
@@ -18,10 +18,10 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// rfc6979ExtraDataV0 is the extra data to feed to RFC6979 when
|
||||
// generating the deterministic nonce for the BIP-340 scheme. This
|
||||
// ensures the same nonce is not generated for the same message and key
|
||||
// as for other signing algorithms such as ECDSA.
|
||||
// rfc6979ExtraDataV0 is the extra data to feed to RFC6979 when generating
|
||||
// the deterministic nonce for the BIP-340 scheme. This ensures the same
|
||||
// nonce is not generated for the same message and key as for other signing
|
||||
// algorithms such as ECDSA.
|
||||
//
|
||||
// It is equal to SHA-256(by("BIP-340")).
|
||||
rfc6979ExtraDataV0 = [32]uint8{
|
||||
@@ -46,12 +46,14 @@ func NewSignature(r *btcec.FieldVal, s *btcec.ModNScalar) *Signature {
|
||||
return &sig
|
||||
}
|
||||
|
||||
// Serialize returns the Schnorr signature in the more strict format.
|
||||
// Serialize returns the Schnorr signature in a stricter format.
|
||||
//
|
||||
// The signatures are encoded as
|
||||
//
|
||||
// sig[0:32] x coordinate of the point R, encoded as a big-endian uint256
|
||||
// sig[32:64] s, encoded also as big-endian uint256
|
||||
// sig[0:32]
|
||||
// x coordinate of the point R, encoded as a big-endian uint256
|
||||
// sig[32:64]
|
||||
// s, encoded also as big-endian uint256
|
||||
func (sig Signature) Serialize() []byte {
|
||||
// Total length of returned signature is the length of r and s.
|
||||
var b [SignatureSize]byte
|
||||
@@ -64,6 +66,7 @@ func (sig Signature) Serialize() []byte {
|
||||
// enforces the following additional restrictions specific to secp256k1:
|
||||
//
|
||||
// - The r component must be in the valid range for secp256k1 field elements
|
||||
//
|
||||
// - The s component must be in the valid range for secp256k1 scalars
|
||||
func ParseSignature(sig []byte) (*Signature, error) {
|
||||
// The signature must be the correct length.
|
||||
@@ -97,9 +100,9 @@ func ParseSignature(sig []byte) (*Signature, error) {
|
||||
return NewSignature(&r, &s), nil
|
||||
}
|
||||
|
||||
// IsEqual compares this Signature instance to the one passed, returning true
|
||||
// if both Signatures are equivalent. A signature is equivalent to another, if
|
||||
// they both have the same scalar value for R and S.
|
||||
// IsEqual compares this Signature instance to the one passed, returning true if
|
||||
// both Signatures are equivalent. A signature is equivalent to another if they
|
||||
// both have the same scalar value for R and S.
|
||||
func (sig Signature) IsEqual(otherSig *Signature) bool {
|
||||
return sig.r.Equals(&otherSig.r) && sig.s.Equals(&otherSig.s)
|
||||
}
|
||||
@@ -109,7 +112,7 @@ func (sig Signature) IsEqual(otherSig *Signature) bool {
|
||||
// indicating why it failed if not successful.
|
||||
//
|
||||
// This differs from the exported Verify method in that it returns a specific
|
||||
// error to support better testing while the exported method simply returns a
|
||||
// error to support better testing, while the exported method simply returns a
|
||||
// bool indicating success or failure.
|
||||
func schnorrVerify(sig *Signature, hash []byte, pubKeyBytes []byte) error {
|
||||
// The algorithm for producing a BIP-340 signature is described in
|
||||
@@ -231,12 +234,12 @@ func zeroArray(a *[scalarSize]byte) {
|
||||
|
||||
// schnorrSign generates an BIP-340 signature over the secp256k1 curve for the
|
||||
// provided hash (which should be the result of hashing a larger message) using
|
||||
// the given nonce and secret key. The produced signature is deterministic
|
||||
// (same message, nonce, and key yield the same signature) and canonical.
|
||||
// the given nonce and secret key. The produced signature is deterministic (the
|
||||
// same message, nonce, and key yield the same signature) and canonical.
|
||||
//
|
||||
// WARNING: The hash MUST be 32 bytes and both the nonce and secret keys must
|
||||
// NOT be 0. Since this is an internal use function, these preconditions MUST
|
||||
// be satisified by the caller.
|
||||
// WARNING: The hash MUST be 32 bytes, and both the nonce and secret keys must
|
||||
// NOT be 0. Since this is an internal use function, these preconditions MUST be
|
||||
// satisified by the caller.
|
||||
func schnorrSign(
|
||||
privKey, nonce *btcec.ModNScalar, pubKey *btcec.PublicKey,
|
||||
hash []byte, opts *signOptions,
|
||||
@@ -327,8 +330,8 @@ func schnorrSign(
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// SignOption is a functional option arguemnt that allows callers to modify the
|
||||
// way we generate BIP-340 schnorr signatues.
|
||||
// SignOption is a functional option argument that allows callers to modify the
|
||||
// way we generate BIP-340 schnorr signatures.
|
||||
type SignOption func(*signOptions)
|
||||
|
||||
// signOptions houses the set of functional options that can be used to modify
|
||||
@@ -361,16 +364,16 @@ func CustomNonce(auxData [32]byte) SignOption {
|
||||
return func(o *signOptions) { o.authNonce = &auxData }
|
||||
}
|
||||
|
||||
// Sign generates an BIP-340 signature over the secp256k1 curve for the
|
||||
// provided hash (which should be the result of hashing a larger message) using
|
||||
// the given secret key. The produced signature is deterministic (same
|
||||
// message and same key yield the same signature) and canonical.
|
||||
// Sign generates an BIP-340 signature over the secp256k1 curve for the provided
|
||||
// hash (which should be the result of hashing a larger message) using the given
|
||||
// secret key. The produced signature is deterministic (the same message and the
|
||||
// same key yield the same signature) and canonical.
|
||||
//
|
||||
// Note that the current signing implementation has a few remaining variable
|
||||
// time aspects which make use of the secret key and the generated nonce,
|
||||
// which can expose the signer to constant time attacks. As a result, this
|
||||
// function should not be used in situations where there is the possibility of
|
||||
// someone having EM field/cache/etc access.
|
||||
// time aspects which make use of the secret key and the generated nonce, which
|
||||
// can expose the signer to constant time attacks. As a result, this function
|
||||
// should not be used in situations where there is the possibility of someone
|
||||
// having EM field/cache/etc access.
|
||||
func Sign(
|
||||
privKey *btcec.SecretKey, hash []byte,
|
||||
signOpts ...SignOption,
|
||||
@@ -380,8 +383,8 @@ func Sign(
|
||||
for _, option := range signOpts {
|
||||
option(opts)
|
||||
}
|
||||
// The algorithm for producing a BIP-340 signature is described in
|
||||
// README.md and is reproduced here for reference:
|
||||
// The algorithm for producing a BIP-340 signature is described in README.md
|
||||
// and is reproduced here for reference:
|
||||
//
|
||||
// G = curve generator
|
||||
// n = curve order
|
||||
@@ -406,20 +409,20 @@ func Sign(
|
||||
// 14. If Verify(bytes(P), m, sig) fails, abort.
|
||||
// 15. return sig.
|
||||
//
|
||||
// Note that the set of functional options passed in may modify the
|
||||
// above algorithm. Namely if CustomNonce is used, then steps 6-8 are
|
||||
// replaced with a process that generates the nonce using rfc6979. If
|
||||
// FastSign is passed, then we skip set 14.
|
||||
// Note that the set of functional options passed in may modify the above
|
||||
// algorithm. Namely if CustomNonce is used, then steps 6-8 are replaced
|
||||
// with a process that generates the nonce using rfc6979. If FastSign is
|
||||
// passed, then we skip set 14.
|
||||
//
|
||||
// Step 1.
|
||||
//
|
||||
// d' = int(d)
|
||||
var privKeyScalar btcec.ModNScalar
|
||||
privKeyScalar.Set(&privKey.Key)
|
||||
|
||||
// Step 2.
|
||||
//
|
||||
// // Step 2.
|
||||
// //
|
||||
// // Fail if m is not 32 bytes
|
||||
// Fail if m is not 32 bytes
|
||||
// if len(hash) != scalarSize {
|
||||
// str := fmt.Sprintf("wrong size for message hash (got %v, want %v)",
|
||||
// len(hash), scalarSize)
|
||||
@@ -462,8 +465,8 @@ func Sign(
|
||||
//
|
||||
// rand = tagged_hash("BIP0340/nonce", t || bytes(P) || m)
|
||||
//
|
||||
// We snip off the first byte of the serialized pubkey, as we
|
||||
// only need the x coordinate and not the market byte.
|
||||
// We snip off the first byte of the serialized pubkey, as we only need
|
||||
// the x coordinate and not the market byte.
|
||||
rand := chainhash.TaggedHash(
|
||||
chainhash.TagBIP0340Nonce, t[:], pubKeyBytes[1:], hash,
|
||||
)
|
||||
|
||||
@@ -187,8 +187,8 @@ var bip340TestVectors = []bip340Test{
|
||||
},
|
||||
}
|
||||
|
||||
// decodeHex decodes the passed hex string and returns the resulting bytes. It
|
||||
// panics if an error occurs. This is only used in the tests as a helper since
|
||||
// decodeHex decodes the passed hex string and returns the resulting bytes. It
|
||||
// panics if an error occurs. This is only used in the tests as a helper since
|
||||
// the only way it can fail is if there is an error in the test source code.
|
||||
func decodeHex(hexStr string) []byte {
|
||||
b, err := hex.Dec(hexStr)
|
||||
@@ -208,7 +208,7 @@ func TestSchnorrSign(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
d := decodeHex(test.secretKey)
|
||||
privKey, _ := btcec.btcec.SecKeyFromBytes(d)
|
||||
privKey, _ := btcec.SecKeyFromBytes(d)
|
||||
var auxBytes [32]byte
|
||||
aux := decodeHex(test.auxRand)
|
||||
copy(auxBytes[:], aux)
|
||||
|
||||
@@ -7,12 +7,12 @@ package filter
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"orly.dev/app/realy/pointers"
|
||||
"orly.dev/crypto/ec/schnorr"
|
||||
"orly.dev/crypto/ec/secp256k1"
|
||||
"orly.dev/crypto/sha256"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/errorf"
|
||||
"orly.dev/utils/pointers"
|
||||
"sort"
|
||||
|
||||
"lukechampine.com/frand"
|
||||
|
||||
@@ -2,11 +2,11 @@ package filter
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"orly.dev/app/realy/pointers"
|
||||
"orly.dev/crypto/ec/schnorr"
|
||||
"orly.dev/crypto/sha256"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/errorf"
|
||||
"orly.dev/utils/pointers"
|
||||
"sort"
|
||||
|
||||
"orly.dev/encoders/event"
|
||||
|
||||
@@ -18,8 +18,9 @@ type Envelope interface {
|
||||
Label() string
|
||||
// Write outputs the envelope to an io.Writer
|
||||
Write(w io.Writer) (err error)
|
||||
// JSON is a somewhat simplified version of the json.Marshaler/json.Unmarshaler
|
||||
// that has no error for the Marshal side of the operation.
|
||||
// JSON is a somewhat simplified version of the
|
||||
// json.Marshaler/json.Unmarshaler that has no error for the Marshal side of
|
||||
// the operation.
|
||||
JSON
|
||||
}
|
||||
|
||||
@@ -35,12 +36,12 @@ type JSON interface {
|
||||
}
|
||||
|
||||
// Binary is a similarly simplified form of the stdlib binary Marshal/Unmarshal
|
||||
// interfaces. Same as JSON it does not have an error for the MarshalBinary.
|
||||
// server. Same as JSON it does not have an error for the MarshalBinary.
|
||||
type Binary interface {
|
||||
// MarshalBinary converts the data of the type into binary form, appending it to
|
||||
// the provided slice.
|
||||
// MarshalBinary converts the data of the type into binary form, appending
|
||||
// it to the provided slice.
|
||||
MarshalBinary(dst []byte) (b []byte)
|
||||
// UnmarshalBinary decodes a binary form of a type back into the runtime form,
|
||||
// and returns whatever remains after the type has been decoded out.
|
||||
// UnmarshalBinary decodes a binary form of a type back into the runtime
|
||||
// form, and returns whatever remains after the type has been decoded out.
|
||||
UnmarshalBinary(b []byte) (r []byte, err error)
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package listener
|
||||
|
||||
type I interface {
|
||||
Write(p []byte) (n int, err error)
|
||||
Close() error
|
||||
Remote() string
|
||||
}
|
||||
@@ -2,13 +2,16 @@ package publisher
|
||||
|
||||
import (
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/interfaces/typer"
|
||||
)
|
||||
|
||||
type Message interface {
|
||||
Type() string
|
||||
}
|
||||
|
||||
type I interface {
|
||||
typer.T
|
||||
Message
|
||||
Deliver(ev *event.E)
|
||||
Receive(msg typer.T)
|
||||
Receive(msg Message)
|
||||
}
|
||||
|
||||
type Publishers []I
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package relay contains a collection of interfaces for enabling the building
|
||||
// Package relay contains a collection of server for enabling the building
|
||||
// of modular nostr relay implementations.
|
||||
package relay
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
// I is the main interface for implementing a nostr relay.
|
||||
type I interface {
|
||||
// Name is used as the "name" field in NIP-11 and as a prefix in default
|
||||
// Server logging. For other NIP-11 fields, see [Informationer].
|
||||
// S logging. For other NIP-11 fields, see [Informationer].
|
||||
Name() string
|
||||
// Init is called at the very beginning by [Server.Start], allowing a realy
|
||||
// Init is called at the very beginning by [S.Start], allowing a realy
|
||||
// to initialize its internal resources.
|
||||
Init() error
|
||||
// Storage returns the realy storage implementation.
|
||||
@@ -28,12 +28,12 @@ type Informationer interface {
|
||||
}
|
||||
|
||||
// ShutdownAware is called during the server shutdown.
|
||||
// See [Server.Shutdown] for details.
|
||||
// See [S.Shutdown] for details.
|
||||
type ShutdownAware interface {
|
||||
OnShutdown(context.T)
|
||||
}
|
||||
|
||||
// Logger is what [Server] uses to log messages.
|
||||
// Logger is what [S] uses to log messages.
|
||||
type Logger interface {
|
||||
Infof(format string, v ...any)
|
||||
Warningf(format string, v ...any)
|
||||
|
||||
@@ -2,18 +2,26 @@ package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/app/realy/publish"
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/interfaces/relay"
|
||||
"orly.dev/interfaces/store"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
type I interface {
|
||||
Context() context.T
|
||||
HandleRelayInfo(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
)
|
||||
Storage() store.I
|
||||
type S interface {
|
||||
AddEvent(
|
||||
c context.T, ev *event.E, hr *http.Request, remote string,
|
||||
) (accepted bool, message []byte)
|
||||
c context.T, rl relay.I, ev *event.E, hr *http.Request,
|
||||
origin string, authedPubkey []byte,
|
||||
) (
|
||||
accepted bool,
|
||||
message []byte,
|
||||
)
|
||||
Context() context.T
|
||||
Disconnect()
|
||||
Publisher() *publish.S
|
||||
Publish(c context.T, evt *event.E) (err error)
|
||||
Relay() relay.I
|
||||
Shutdown()
|
||||
Storage() store.I
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package signer defines interfaces for management of signatures, used to
|
||||
// Package signer defines server for management of signatures, used to
|
||||
// abstract the signature algorithm from the usage.
|
||||
package signer
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package typer is an interface for interfaces to use to identify their type simply for
|
||||
// aggregating multiple self-registered interfaces such that the top level can recognise the
|
||||
// Package typer is an interface for server to use to identify their type simply for
|
||||
// aggregating multiple self-registered server such that the top level can recognise the
|
||||
// type of a message and match it to the type of handler.
|
||||
package typer
|
||||
|
||||
|
||||
@@ -57,8 +57,9 @@ var (
|
||||
RelayTag = []byte("relay")
|
||||
)
|
||||
|
||||
// Validate checks whether event is a valid NIP-42 event for given challenge and
|
||||
// relayURL. The result of the validation is encoded in the ok bool.
|
||||
// Validate checks whether an event is a valid NIP-42 event for a given
|
||||
// challenge and relayURL. The result of the validation is encoded in the ok
|
||||
// bool.
|
||||
func Validate(evt *event.E, challenge []byte, relayURL string) (
|
||||
ok bool, err error,
|
||||
) {
|
||||
@@ -76,7 +77,6 @@ func Validate(evt *event.E, challenge []byte, relayURL string) (
|
||||
log.D.Ln(err)
|
||||
return
|
||||
}
|
||||
// log.I.Ln(relayURL)
|
||||
var expected, found *url.URL
|
||||
if expected, err = parseURL(relayURL); chk.D(err) {
|
||||
log.D.Ln(err)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// Package nwc is an implementation of the NWC Nostr Wallet Connect protocol for
|
||||
// communicating with lightning (and potentially other kinds of wallets) using
|
||||
// nostr ephemeral event messages.
|
||||
package nwc
|
||||
@@ -1,6 +0,0 @@
|
||||
package nwc
|
||||
|
||||
type Error struct {
|
||||
Code []byte
|
||||
Message []byte
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package nwc
|
||||
|
||||
type GetBalanceRequest struct {
|
||||
Request
|
||||
// nothing to see here, move along
|
||||
}
|
||||
|
||||
func NewGetBalanceRequest() *GetBalanceRequest {
|
||||
return &GetBalanceRequest{Request{Methods.GetBalance}}
|
||||
}
|
||||
|
||||
type GetBalanceResponse struct {
|
||||
Response
|
||||
Balance Msat
|
||||
}
|
||||
|
||||
func NewGetBalanceResponse(balance Msat) *GetBalanceResponse {
|
||||
return &GetBalanceResponse{Response{Type: Methods.GetBalance}, balance}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package nwc
|
||||
|
||||
type GetInfoRequest struct {
|
||||
Request
|
||||
// nothing to see here, move along
|
||||
}
|
||||
|
||||
func NewGetInfoRequest() GetInfoRequest {
|
||||
return GetInfoRequest{Request{Methods.GetInfo}}
|
||||
}
|
||||
|
||||
type GetInfo struct {
|
||||
Alias []byte
|
||||
Color []byte // Hex string
|
||||
Pubkey []byte
|
||||
Network []byte // mainnet/testnet/signet/regtest
|
||||
BlockHeight uint64
|
||||
BlockHash []byte
|
||||
Methods []byte // pay_invoice, get_balance, make_invoice, lookup_invoice, list_transactions, get_info (list of methods)
|
||||
}
|
||||
|
||||
type GetInfoResponse struct {
|
||||
Response
|
||||
GetInfo
|
||||
}
|
||||
|
||||
func NewGetInfoResponse(gi GetInfo) GetInfoResponse {
|
||||
return GetInfoResponse{Response{Type: Methods.GetInfo}, gi}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package nwc
|
||||
|
||||
import (
|
||||
"orly.dev/encoders/kind"
|
||||
)
|
||||
|
||||
var Kinds = []*kind.T{
|
||||
kind.WalletInfo,
|
||||
kind.WalletRequest,
|
||||
kind.WalletResponse,
|
||||
kind.WalletNotification,
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package nwc
|
||||
|
||||
type ListTransactionsRequest struct {
|
||||
Request
|
||||
ListTransactions
|
||||
}
|
||||
|
||||
func NewListTransactionsRequest(req ListTransactions) *ListTransactionsRequest {
|
||||
return &ListTransactionsRequest{
|
||||
Request{Methods.ListTransactions}, req,
|
||||
}
|
||||
}
|
||||
|
||||
type ListTransactionsResponse struct {
|
||||
Response
|
||||
Transactions []LookupInvoice
|
||||
}
|
||||
|
||||
func NewListTransactionsResponse(txs []LookupInvoice) ListTransactionsResponse {
|
||||
return ListTransactionsResponse{Response{Type: Methods.ListTransactions}, txs}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package nwc
|
||||
|
||||
type LookupInvoiceRequest struct {
|
||||
Request
|
||||
PaymentHash, Invoice []byte
|
||||
}
|
||||
|
||||
func NewLookupInvoiceRequest(paymentHash, invoice []byte) *LookupInvoiceRequest {
|
||||
return &LookupInvoiceRequest{
|
||||
Request{Methods.LookupInvoice}, paymentHash, invoice,
|
||||
}
|
||||
}
|
||||
|
||||
type LookupInvoice struct {
|
||||
Response
|
||||
InvoiceResponse
|
||||
SettledAt int64 // optional if unpaid
|
||||
}
|
||||
type LookupInvoiceResponse struct {
|
||||
Response
|
||||
LookupInvoice
|
||||
}
|
||||
|
||||
func NewLookupInvoiceResponse(resp LookupInvoice) LookupInvoiceResponse {
|
||||
return LookupInvoiceResponse{Response{Type: Methods.LookupInvoice}, resp}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package nwc
|
||||
|
||||
type MakeInvoiceRequest struct {
|
||||
Request
|
||||
Amount Msat
|
||||
Description []byte // optional
|
||||
DescriptionHash []byte // optional
|
||||
Expiry int // optional
|
||||
}
|
||||
|
||||
func NewMakeInvoiceRequest(amount Msat, description, descriptionHash []byte,
|
||||
expiry int) MakeInvoiceRequest {
|
||||
return MakeInvoiceRequest{
|
||||
Request{Methods.MakeInvoice},
|
||||
amount,
|
||||
description,
|
||||
descriptionHash,
|
||||
expiry,
|
||||
}
|
||||
}
|
||||
|
||||
type MakeInvoiceResponse struct {
|
||||
Response
|
||||
InvoiceResponse
|
||||
}
|
||||
|
||||
func NewMakeInvoiceResponse(resp InvoiceResponse) MakeInvoiceResponse {
|
||||
return MakeInvoiceResponse{Response{Type: Methods.MakeInvoice}, resp}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package nwc
|
||||
|
||||
type MultiPayInvoiceRequest struct {
|
||||
Request
|
||||
Invoices []Invoice
|
||||
}
|
||||
|
||||
func NewMultiPayInvoiceRequest(invoices []Invoice) MultiPayInvoiceRequest {
|
||||
return MultiPayInvoiceRequest{
|
||||
Request: Request{Methods.MultiPayInvoice},
|
||||
Invoices: invoices,
|
||||
}
|
||||
}
|
||||
|
||||
type MultiPayInvoiceResponse = PayInvoiceResponse
|
||||
|
||||
func NewMultiPayInvoiceResponse(preimage []byte, feesPaid Msat) MultiPayInvoiceResponse {
|
||||
return MultiPayInvoiceResponse{Response{Type: Methods.MultiPayInvoice}, preimage, feesPaid}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package nwc
|
||||
|
||||
type MultiPayKeysendRequest struct {
|
||||
Request
|
||||
Keysends []PayKeysendRequest
|
||||
}
|
||||
|
||||
func NewMultiPayKeysendRequest(keysends []PayKeysendRequest) MultiPayKeysendRequest {
|
||||
return MultiPayKeysendRequest{Request{Methods.MultiPayKeysend}, keysends}
|
||||
}
|
||||
|
||||
type MultiPayKeysendResponse = PayKeysendResponse
|
||||
|
||||
func NewMultiPayKKeysendResponse(preimage []byte, feesPaid Msat) MultiPayKeysendResponse {
|
||||
return MultiPayKeysendResponse{
|
||||
Response{Type: Methods.MultiPayKeysend}, preimage, feesPaid,
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package nwc
|
||||
|
||||
// Methods are the text of the value of the Method field of Request.Method and
|
||||
// Response.ResultType in a form that allows more convenient reference than using
|
||||
// a map or package scoped variable. These appear in the API Request and Response
|
||||
// types.
|
||||
var Methods = struct {
|
||||
PayInvoice,
|
||||
MultiPayInvoice,
|
||||
PayKeysend,
|
||||
MultiPayKeysend,
|
||||
MakeInvoice,
|
||||
LookupInvoice,
|
||||
ListTransactions,
|
||||
GetBalance,
|
||||
GetInfo []byte
|
||||
}{
|
||||
[]byte("pay_invoice"),
|
||||
[]byte("multi_pay_invoice"),
|
||||
[]byte("pay_keysend"),
|
||||
[]byte("multi_pay_keysend"),
|
||||
[]byte("make_invoice"),
|
||||
[]byte("lookup_invoice"),
|
||||
[]byte("list_transactions"),
|
||||
[]byte("get_balance"),
|
||||
[]byte("get_info"),
|
||||
}
|
||||
|
||||
// Keys are the proper JSON bytes for the JSON object keys of the structs of the
|
||||
// same-named type used lower in the following. Anonymous struct syntax is used
|
||||
// to make neater addressing of these fields as symbols.
|
||||
var Keys = struct {
|
||||
Method,
|
||||
Params,
|
||||
ResultType,
|
||||
Error,
|
||||
Result,
|
||||
Invoice,
|
||||
Amount,
|
||||
Preimage,
|
||||
FeesPaid,
|
||||
Id,
|
||||
TLVRecords,
|
||||
Type,
|
||||
Value,
|
||||
Pubkey,
|
||||
Description,
|
||||
DescriptionHash,
|
||||
Expiry,
|
||||
CreatedAt,
|
||||
ExpiresAt,
|
||||
Metadata,
|
||||
SettledAt,
|
||||
From,
|
||||
Until,
|
||||
Offset,
|
||||
Unpaid,
|
||||
Balance,
|
||||
Notifications,
|
||||
NotificationType,
|
||||
Notification,
|
||||
PaymentHash []byte
|
||||
}{
|
||||
[]byte("method"),
|
||||
[]byte("params"),
|
||||
[]byte("result_type"),
|
||||
[]byte("error"),
|
||||
[]byte("result"),
|
||||
[]byte("invoice"),
|
||||
[]byte("amount"),
|
||||
[]byte("preimage"),
|
||||
[]byte("fees_paid"),
|
||||
[]byte("id"),
|
||||
[]byte("tlv_records"),
|
||||
[]byte("type"),
|
||||
[]byte("value"),
|
||||
[]byte("pubkey"),
|
||||
[]byte("description"),
|
||||
[]byte("description_hash"),
|
||||
[]byte("expiry"),
|
||||
[]byte("created_at"),
|
||||
[]byte("expires_at"),
|
||||
[]byte("metadata"),
|
||||
[]byte("settled_at"),
|
||||
[]byte("from"),
|
||||
[]byte("until"),
|
||||
[]byte("offset"),
|
||||
[]byte("unpaid"),
|
||||
[]byte("balance"),
|
||||
[]byte("notifications"),
|
||||
[]byte("notification_type"),
|
||||
[]byte("notification"),
|
||||
[]byte("payment_hash"),
|
||||
}
|
||||
|
||||
// Notifications are the proper strings for the Notification.NotificationType
|
||||
var Notifications = struct {
|
||||
PaymentReceived, PaymentSent []byte
|
||||
}{
|
||||
[]byte("payment_received"),
|
||||
[]byte("payment_sent"),
|
||||
}
|
||||
|
||||
var Errors = struct {
|
||||
// RateLimited - The client is sending commands too fast.It should retry in a few seconds.
|
||||
RateLimited,
|
||||
// NotImplemented - The command is not known or is intentionally not implemented.
|
||||
NotImplemented,
|
||||
// InsufficientBalance - The wallet does not have enough funds to cover a fee reserve or the payment amount.
|
||||
InsufficientBalance,
|
||||
// QuotaExceeded - The wallet has exceeded its spending quota.
|
||||
QuotaExceeded,
|
||||
// Restricted - This public key is not allowed to do this operation.
|
||||
Restricted,
|
||||
// Unauthorized - This public key has no wallet connected.
|
||||
Unauthorized,
|
||||
// Internal - An internal error.
|
||||
Internal,
|
||||
// Other - Other error.
|
||||
Other []byte
|
||||
}{
|
||||
[]byte("RATE_LIMITED"),
|
||||
[]byte("NOT_IMPLEMENTED"),
|
||||
[]byte("INSUFFICIENT_BALANCE"),
|
||||
[]byte("QUOTA_EXCEEDED"),
|
||||
[]byte("RESTRICTED"),
|
||||
[]byte("UNAUTHORIZED"),
|
||||
[]byte("INTERNAL"),
|
||||
[]byte("OTHER"),
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package nwc
|
||||
@@ -1,91 +0,0 @@
|
||||
package nwc
|
||||
|
||||
import (
|
||||
"orly.dev/encoders/text"
|
||||
)
|
||||
|
||||
type PayInvoiceRequest struct {
|
||||
Request
|
||||
Invoice
|
||||
}
|
||||
|
||||
func NewPayInvoiceRequest[V string | []byte](
|
||||
invoice V, amount Msat,
|
||||
) PayInvoiceRequest {
|
||||
return PayInvoiceRequest{
|
||||
Request{Methods.PayInvoice}, Invoice{nil, []byte(invoice), amount},
|
||||
}
|
||||
}
|
||||
|
||||
func (p PayInvoiceRequest) Marshal(dst []byte) (b []byte) {
|
||||
// open parentheses
|
||||
dst = append(dst, '{')
|
||||
// method
|
||||
dst = text.JSONKey(dst, Keys.Method)
|
||||
dst = text.Quote(dst, p.RequestType())
|
||||
dst = append(dst, ',')
|
||||
// Params
|
||||
dst = text.JSONKey(dst, Keys.Params)
|
||||
dst = append(dst, '{')
|
||||
// Invoice
|
||||
dst = text.JSONKey(dst, Keys.Invoice)
|
||||
dst = text.AppendQuote(dst, p.Invoice.Invoice, text.Noop)
|
||||
// Amount - optional (omit if zero)
|
||||
if p.Amount > 0 {
|
||||
dst = append(dst, ',')
|
||||
dst = text.JSONKey(dst, Keys.Amount)
|
||||
dst = p.Amount.Bytes(dst)
|
||||
}
|
||||
// close parentheses
|
||||
dst = append(dst, '}')
|
||||
dst = append(dst, '}')
|
||||
b = dst
|
||||
return
|
||||
}
|
||||
|
||||
func (p PayInvoiceRequest) Unmarshal(b []byte) (r []byte, err error) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type PayInvoiceResponse struct {
|
||||
Response
|
||||
Preimage []byte
|
||||
FeesPaid Msat // optional, omitted if zero
|
||||
}
|
||||
|
||||
func NewPayInvoiceResponse(preimage []byte, feesPaid Msat) PayInvoiceResponse {
|
||||
return PayInvoiceResponse{
|
||||
Response{Type: Methods.PayInvoice}, preimage, feesPaid,
|
||||
}
|
||||
}
|
||||
|
||||
func (p PayInvoiceResponse) Marshal(dst []byte) (b []byte) {
|
||||
// open parentheses
|
||||
dst = append(dst, '{')
|
||||
// method
|
||||
dst = text.JSONKey(dst, p.Response.Type)
|
||||
dst = text.Quote(dst, p.ResultType())
|
||||
// Params
|
||||
dst = text.JSONKey(dst, Keys.Params)
|
||||
// open parenthesis
|
||||
dst = append(dst, '{')
|
||||
// Invoice
|
||||
dst = text.JSONKey(dst, Keys.Preimage)
|
||||
dst = text.AppendQuote(dst, p.Preimage, text.Noop)
|
||||
// Amount - optional (omit if zero)
|
||||
if p.FeesPaid > 0 {
|
||||
dst = append(dst, ',')
|
||||
dst = text.JSONKey(dst, Keys.FeesPaid)
|
||||
dst = p.FeesPaid.Bytes(dst)
|
||||
}
|
||||
// close parentheses
|
||||
dst = append(dst, '}')
|
||||
dst = append(dst, '}')
|
||||
return
|
||||
}
|
||||
|
||||
func (p PayInvoiceResponse) Unmarshal(b []byte) (r []byte, err error) {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package nwc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"orly.dev/utils/chk"
|
||||
)
|
||||
|
||||
func ExamplePayInvoiceRequest_Marshal() {
|
||||
ir := NewPayInvoiceRequest("lnbc50n1...", 0)
|
||||
var b []byte
|
||||
var err error
|
||||
if b = ir.Marshal(b); chk.E(err) {
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s\n", b)
|
||||
b = b[:0]
|
||||
ir = NewPayInvoiceRequest("lnbc50n1...", 123)
|
||||
if b = ir.Marshal(b); chk.E(err) {
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s\n", b)
|
||||
// Output:
|
||||
// {"method":"pay_invoice","params":{"invoice":"lnbc50n1..."}}
|
||||
// {"method":"pay_invoice","params":{"invoice":"lnbc50n1...","amount":123}}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package nwc
|
||||
|
||||
type TLV struct {
|
||||
Type uint64
|
||||
Value []byte
|
||||
}
|
||||
|
||||
type PayKeysendRequest struct {
|
||||
Request
|
||||
Amount Msat
|
||||
Pubkey []byte
|
||||
Preimage []byte // optional
|
||||
TLVRecords []TLV // optional
|
||||
}
|
||||
|
||||
func NewPayKeysendRequest(amount Msat, pubkey, preimage []byte,
|
||||
tlvRecords []TLV) PayKeysendRequest {
|
||||
return PayKeysendRequest{
|
||||
Request{Methods.PayKeysend},
|
||||
amount,
|
||||
pubkey,
|
||||
preimage,
|
||||
tlvRecords,
|
||||
}
|
||||
}
|
||||
|
||||
type PayKeysendResponse = PayInvoiceResponse
|
||||
|
||||
func NewPayKeysendResponse(preimage []byte, feesPaid Msat) PayKeysendResponse {
|
||||
return PayInvoiceResponse{
|
||||
Response{Type: Methods.PayKeysend}, preimage, feesPaid,
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package nwc
|
||||
|
||||
import (
|
||||
"orly.dev/encoders/ints"
|
||||
)
|
||||
|
||||
// Interfaces
|
||||
//
|
||||
// By using these interfaces and embedding the following implementations it becomes simple to type check the specific
|
||||
// request, response or notification variable being used in a given place in the code, without using reflection.
|
||||
//
|
||||
// All request, responses and methods embed the implementations and their types then become easily checked.
|
||||
|
||||
type Requester interface {
|
||||
RequestType() []byte
|
||||
}
|
||||
|
||||
type Resulter interface {
|
||||
ResultType() []byte
|
||||
}
|
||||
|
||||
type Notifier interface {
|
||||
NotificationType() []byte
|
||||
}
|
||||
|
||||
// Implementations
|
||||
//
|
||||
// By embedding the following types into the message structs and writing a constructor that loads the type name,
|
||||
// code can handle these without reflection, determine type via type assertion and introspect the message type via
|
||||
// the interface accessor method.
|
||||
|
||||
type Request struct {
|
||||
Method []byte
|
||||
}
|
||||
|
||||
func (r Request) RequestType() []byte { return r.Method }
|
||||
|
||||
type Response struct {
|
||||
Type []byte
|
||||
Error
|
||||
}
|
||||
|
||||
func (r Response) ResultType() []byte { return r.Type }
|
||||
|
||||
type Notification struct {
|
||||
Type []byte
|
||||
}
|
||||
|
||||
func (n Notification) NotificationType() []byte { return n.Type }
|
||||
|
||||
// Msat is milli-sat, max possible value is 1000 x 21 x 100 000 000 (well, under 19 places of 64 bits in base 10)
|
||||
type Msat uint64
|
||||
|
||||
func (m Msat) Bytes(dst []byte) (b []byte) { return ints.New(uint64(m)).Marshal(dst) }
|
||||
|
||||
// Methods
|
||||
|
||||
type Invoice struct {
|
||||
Id []byte // nil for request, required for responses (omitted if nil)
|
||||
Invoice []byte
|
||||
Amount Msat // optional, omitted if zero
|
||||
}
|
||||
|
||||
type InvoiceResponse struct {
|
||||
Type []byte // incoming or outgoing
|
||||
Invoice []byte // optional
|
||||
Description []byte // optional
|
||||
DescriptionHash []byte // optional
|
||||
Preimage []byte // optional if unpaid
|
||||
PaymentHash []byte
|
||||
Amount Msat
|
||||
FeesPaid Msat
|
||||
CreatedAt int64
|
||||
ExpiresAt int64 // optional if not applicable
|
||||
Metadata []any // optional, probably like tags but retardation can be retarded so allow also numbers and floats
|
||||
|
||||
}
|
||||
|
||||
type ListTransactions struct {
|
||||
From int64 // optional
|
||||
Until int64 // optional
|
||||
Limit int // optional
|
||||
Offset int // optional
|
||||
Unpaid bool // optional default false
|
||||
Type []byte // incoming/outgoing/empty for "both"
|
||||
}
|
||||
|
||||
// Notifications
|
||||
|
||||
var (
|
||||
PaymentSent = []byte("payment_sent")
|
||||
PaymentReceived = []byte("payment_received")
|
||||
)
|
||||
|
||||
type PaymentSentNotification struct {
|
||||
LookupInvoiceResponse
|
||||
}
|
||||
|
||||
type PaymentReceivedNotification struct {
|
||||
LookupInvoiceResponse
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"orly.dev/app/realy/interfaces"
|
||||
)
|
||||
|
||||
type Operations struct{ interfaces.Server }
|
||||
|
||||
// NewOperations creates a new openapi.Operations..
|
||||
func NewOperations(s interfaces.Server) (ep *Operations) {
|
||||
return &Operations{Server: s}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package openapi
|
||||
|
||||
// import (
|
||||
// "net/http"
|
||||
//
|
||||
// "github.com/danielgtaylor/huma/v2"
|
||||
//
|
||||
// "orly.dev/utils/context"
|
||||
// "orly.dev/realy/helpers"
|
||||
// "orly.dev/store"
|
||||
// )
|
||||
//
|
||||
// // ConfigurationSetInput is the parameters for HTTP API method to set Configuration.
|
||||
// type ConfigurationSetInput struct {
|
||||
// Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"true"`
|
||||
// Body *store.Configuration `doc:"the new configuration"`
|
||||
// }
|
||||
//
|
||||
// // ConfigurationGetInput is the parameters for HTTP API method to get Configuration.
|
||||
// type ConfigurationGetInput struct {
|
||||
// Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"true"`
|
||||
// Accept string `header:"Accept" default:"application/json" enum:"application/json" required:"true"`
|
||||
// }
|
||||
//
|
||||
// // ConfigurationGetOutput is the result of getting Configuration.
|
||||
// type ConfigurationGetOutput struct {
|
||||
// Body store.Configuration `doc:"the current configuration"`
|
||||
// }
|
||||
//
|
||||
// // RegisterConfigurationSet implements the HTTP API for setting Configuration.
|
||||
// func (x *Operations) RegisterConfigurationSet(api huma.API) {
|
||||
// name := "ConfigurationSet"
|
||||
// description := "Set the configuration"
|
||||
// path := "/configuration/set"
|
||||
// scopes := []string{"admin", "write"}
|
||||
// method := http.MethodPost
|
||||
// huma.Register(api, huma.Operation{
|
||||
// OperationID: name,
|
||||
// Summary: name,
|
||||
// Path: path,
|
||||
// Method: method,
|
||||
// Tags: []string{"admin"},
|
||||
// Description: helpers.GenerateDescription(description, scopes),
|
||||
// Security: []map[string][]string{{"auth": scopes}},
|
||||
// }, func(ctx context.T, input *ConfigurationSetInput) (wgh *struct{}, err error) {
|
||||
// log.I.S(input)
|
||||
// r := ctx.Value("http-request").(*http.Request)
|
||||
// // w := ctx.Value("http-response").(http.ResponseWriter)
|
||||
// // rr := GetRemoteFromReq(r)
|
||||
// authed, _ := x.AdminAuth(r)
|
||||
// if !authed {
|
||||
// // pubkey = ev.Pubkey
|
||||
// err = huma.Error401Unauthorized("authorization required")
|
||||
// return
|
||||
// }
|
||||
// sto := x.Storage()
|
||||
// if c, ok := sto.(store.Configurationer); ok {
|
||||
// if err = c.SetConfiguration(input.Body); chk.E(err) {
|
||||
// return
|
||||
// }
|
||||
// x.SetConfiguration(input.Body)
|
||||
// }
|
||||
// return
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // RegisterConfigurationGet implements the HTTP API for getting the Configuration.
|
||||
// func (x *Operations) RegisterConfigurationGet(api huma.API) {
|
||||
// name := "ConfigurationGet"
|
||||
// description := "Fetch the current configuration"
|
||||
// path := "/configuration/get"
|
||||
// scopes := []string{"admin", "read"}
|
||||
// method := http.MethodGet
|
||||
// huma.Register(api, huma.Operation{
|
||||
// OperationID: name,
|
||||
// Summary: name,
|
||||
// Path: path,
|
||||
// Method: method,
|
||||
// Tags: []string{"admin"},
|
||||
// Description: helpers.GenerateDescription(description, scopes),
|
||||
// Security: []map[string][]string{{"auth": scopes}},
|
||||
// }, func(ctx context.T, input *ConfigurationGetInput) (output *ConfigurationGetOutput,
|
||||
// err error) {
|
||||
// r := ctx.Value("http-request").(*http.Request)
|
||||
// authed, _ := x.AdminAuth(r)
|
||||
// if !authed {
|
||||
// err = huma.Error401Unauthorized("authorization required")
|
||||
// return
|
||||
// }
|
||||
// output = &ConfigurationGetOutput{Body: x.Configuration()}
|
||||
// // }
|
||||
// return
|
||||
// })
|
||||
// }
|
||||
@@ -1,51 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/app/realy/helpers"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
// DisconnectInput is the parameters for triggering the disconnection of all open websockets.
|
||||
type DisconnectInput struct {
|
||||
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"true"`
|
||||
}
|
||||
|
||||
// DisconnectOutput is the result type for the Disconnect HTTP API method.
|
||||
type DisconnectOutput struct{}
|
||||
|
||||
// RegisterDisconnect is the implementation of the HTTP API Disconnect method.
|
||||
func (x *Operations) RegisterDisconnect(api huma.API) {
|
||||
name := "Disconnect"
|
||||
description := "Close all open nip-01 websockets"
|
||||
path := "/disconnect"
|
||||
scopes := []string{"admin"}
|
||||
method := http.MethodGet
|
||||
huma.Register(
|
||||
api, huma.Operation{
|
||||
OperationID: name,
|
||||
Summary: name,
|
||||
Path: path,
|
||||
Method: method,
|
||||
Tags: []string{"admin"},
|
||||
Description: helpers.GenerateDescription(description, scopes),
|
||||
Security: []map[string][]string{{"auth": scopes}},
|
||||
DefaultStatus: 204,
|
||||
}, func(ctx context.T, input *DisconnectInput) (
|
||||
wgh *DisconnectOutput, err error,
|
||||
) {
|
||||
// r := ctx.Value("http-request").(*http.Request)
|
||||
// authed, _ := x.AdminAuth(r)
|
||||
// if !authed {
|
||||
// // pubkey = ev.Pubkey
|
||||
// err = huma.Error401Unauthorized("authorization required")
|
||||
// return
|
||||
// }
|
||||
x.Disconnect()
|
||||
return
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"orly.dev/app/realy/helpers"
|
||||
"orly.dev/crypto/sha256"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/log"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/encoders/filter"
|
||||
"orly.dev/encoders/hex"
|
||||
"orly.dev/encoders/ints"
|
||||
"orly.dev/encoders/kind"
|
||||
"orly.dev/encoders/tag"
|
||||
"orly.dev/protocol/httpauth"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
// EventInput is the parameters for the Event HTTP API method.
|
||||
type EventInput struct {
|
||||
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"false"`
|
||||
RawBody []byte
|
||||
}
|
||||
|
||||
// EventOutput is the return parameters for the HTTP API Event method.
|
||||
type EventOutput struct{ Body string }
|
||||
|
||||
// RegisterEvent is the implementatino of the HTTP API Event method.
|
||||
func (x *Operations) RegisterEvent(api huma.API) {
|
||||
name := "Event"
|
||||
description := "Submit an event"
|
||||
path := "/event"
|
||||
scopes := []string{"user", "write"}
|
||||
method := http.MethodPost
|
||||
huma.Register(
|
||||
api, huma.Operation{
|
||||
OperationID: name,
|
||||
Summary: name,
|
||||
Path: path,
|
||||
Method: method,
|
||||
Tags: []string{"events"},
|
||||
Description: helpers.GenerateDescription(description, scopes),
|
||||
Security: []map[string][]string{{"auth": scopes}},
|
||||
}, func(ctx context.T, input *EventInput) (
|
||||
output *EventOutput, err error,
|
||||
) {
|
||||
r := ctx.Value("http-request").(*http.Request)
|
||||
// w := ctx.Value("http-response").(http.ResponseWriter)
|
||||
rr := helpers.GetRemoteFromReq(r)
|
||||
ev := &event.E{}
|
||||
if _, err = ev.Unmarshal(input.RawBody); chk.E(err) {
|
||||
err = huma.Error406NotAcceptable(err.Error())
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
sto := x.Storage()
|
||||
if sto == nil {
|
||||
panic("no event store has been set to store event")
|
||||
}
|
||||
// advancedDeleter, _ := sto.(relay.AdvancedDeleter)
|
||||
var valid bool
|
||||
var pubkey []byte
|
||||
valid, pubkey, err = httpauth.CheckAuth(r)
|
||||
// missing := !errors.Is(err, httpauth.ErrMissingKey)
|
||||
// if there is an error but not that the token is missing, or there is no error
|
||||
// but the signature is invalid, return error that request is unauthorized.
|
||||
if err != nil && !errors.Is(err, httpauth.ErrMissingKey) {
|
||||
err = huma.Error400BadRequest(err.Error())
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
if !valid {
|
||||
err = huma.Error401Unauthorized("Authorization header is invalid")
|
||||
return
|
||||
}
|
||||
// if there was auth, or no auth, check the relay policy allows accepting the
|
||||
// event (no auth with auth required or auth not valid for action can apply
|
||||
// here).
|
||||
// accept, notice, after := x.AcceptEvent(ctx, ev, r, rr, pubkey)
|
||||
// if !accept {
|
||||
// err = huma.Error401Unauthorized(notice)
|
||||
// return
|
||||
// }
|
||||
if !bytes.Equal(ev.GetIDBytes(), ev.Id) {
|
||||
err = huma.Error400BadRequest("event id is computed incorrectly")
|
||||
return
|
||||
}
|
||||
if ok, err = ev.Verify(); chk.T(err) {
|
||||
err = huma.Error400BadRequest("failed to verify signature")
|
||||
return
|
||||
} else if !ok {
|
||||
err = huma.Error400BadRequest("signature is invalid")
|
||||
return
|
||||
}
|
||||
if ev.Kind.K == kind.Deletion.K {
|
||||
log.I.F("delete event\n%s", ev.Serialize())
|
||||
for _, t := range ev.Tags.ToSliceOfTags() {
|
||||
var res []*event.E
|
||||
if t.Len() >= 2 {
|
||||
switch {
|
||||
case bytes.Equal(t.Key(), []byte("e")):
|
||||
evId := make([]byte, sha256.Size)
|
||||
if _, err = hex.DecBytes(
|
||||
evId, t.Value(),
|
||||
); chk.E(err) {
|
||||
continue
|
||||
}
|
||||
res, err = sto.QueryEvents(
|
||||
ctx, &filter.F{Ids: tag.New(evId)},
|
||||
)
|
||||
if err != nil {
|
||||
err = huma.Error500InternalServerError(err.Error())
|
||||
return
|
||||
}
|
||||
for i := range res {
|
||||
if res[i].Kind.Equal(kind.Deletion) {
|
||||
err = huma.Error409Conflict("not processing or storing delete event containing delete event references")
|
||||
}
|
||||
if !bytes.Equal(res[i].Pubkey, ev.Pubkey) {
|
||||
err = huma.Error409Conflict("cannot delete other users' events (delete by e tag)")
|
||||
return
|
||||
}
|
||||
}
|
||||
case bytes.Equal(t.Key(), []byte("a")):
|
||||
split := bytes.Split(t.Value(), []byte{':'})
|
||||
if len(split) != 3 {
|
||||
continue
|
||||
}
|
||||
var pk []byte
|
||||
if pk, err = hex.DecAppend(
|
||||
nil, split[1],
|
||||
); chk.E(err) {
|
||||
err = huma.Error400BadRequest(
|
||||
fmt.Sprintf(
|
||||
"delete event a tag pubkey value invalid: %s",
|
||||
t.Value(),
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
kin := ints.New(uint16(0))
|
||||
if _, err = kin.Unmarshal(split[0]); chk.E(err) {
|
||||
err = huma.Error400BadRequest(
|
||||
fmt.Sprintf(
|
||||
"delete event a tag kind value invalid: %s",
|
||||
t.Value(),
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
kk := kind.New(kin.Uint16())
|
||||
if kk.Equal(kind.Deletion) {
|
||||
err = huma.Error403Forbidden("delete event kind may not be deleted")
|
||||
return
|
||||
}
|
||||
if !kk.IsParameterizedReplaceable() {
|
||||
err = huma.Error403Forbidden("delete tags with a tags containing non-parameterized-replaceable events cannot be processed")
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(pk, ev.Pubkey) {
|
||||
log.I.S(pk, ev.Pubkey, ev)
|
||||
err = huma.Error403Forbidden("cannot delete other users' events (delete by a tag)")
|
||||
return
|
||||
}
|
||||
f := filter.New()
|
||||
f.Kinds.K = []*kind.T{kk}
|
||||
f.Authors.Append(pk)
|
||||
f.Tags.AppendTags(
|
||||
tag.New(
|
||||
[]byte{'#', 'd'}, split[2],
|
||||
),
|
||||
)
|
||||
res, err = sto.QueryEvents(ctx, f)
|
||||
if err != nil {
|
||||
err = huma.Error500InternalServerError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(res) < 1 {
|
||||
continue
|
||||
}
|
||||
var resTmp []*event.E
|
||||
for _, v := range res {
|
||||
if ev.CreatedAt.U64() >= v.CreatedAt.U64() {
|
||||
resTmp = append(resTmp, v)
|
||||
}
|
||||
}
|
||||
res = resTmp
|
||||
for _, target := range res {
|
||||
if target.Kind.K == kind.Deletion.K {
|
||||
err = huma.Error403Forbidden(
|
||||
fmt.Sprintf(
|
||||
"cannot delete delete event %s", ev.Id,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
if target.CreatedAt.Int() > ev.CreatedAt.Int() {
|
||||
// todo: shouldn't this be an error?
|
||||
log.I.F(
|
||||
"not deleting\n%d%\nbecause delete event is older\n%d",
|
||||
target.CreatedAt.Int(), ev.CreatedAt.Int(),
|
||||
)
|
||||
continue
|
||||
}
|
||||
if !bytes.Equal(target.Pubkey, ev.Pubkey) {
|
||||
err = huma.Error403Forbidden("only author can delete event")
|
||||
return
|
||||
}
|
||||
// if advancedDeleter != nil {
|
||||
// advancedDeleter.BeforeDelete(ctx, t.Value(), ev.Pubkey)
|
||||
// }
|
||||
// Instead of deleting the event, we'll just add the deletion event
|
||||
// The query logic will filter out deleted events
|
||||
// if advancedDeleter != nil {
|
||||
// advancedDeleter.AfterDelete(t.Value(), ev.Pubkey)
|
||||
// }
|
||||
}
|
||||
res = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
var reason []byte
|
||||
ok, reason = x.AddEvent(ctx, x.Relay(), ev, r, rr, pubkey)
|
||||
// return the response whether true or false and any reason if false
|
||||
if ok {
|
||||
} else {
|
||||
err = huma.Error500InternalServerError(string(reason))
|
||||
}
|
||||
// if after != nil {
|
||||
// // do this in the background and let the http response close
|
||||
// go after()
|
||||
// }
|
||||
output = &EventOutput{"event accepted"}
|
||||
return
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"orly.dev/app/realy/helpers"
|
||||
"orly.dev/crypto/sha256"
|
||||
"orly.dev/utils/chk"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"orly.dev/encoders/hex"
|
||||
"orly.dev/encoders/tag"
|
||||
"orly.dev/interfaces/store"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
// EventsInput is the parameters for an Events HTTP API method. Basically an array of eventid.T.
|
||||
type EventsInput struct {
|
||||
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"false"`
|
||||
Body []string `doc:"list of event Ids"`
|
||||
}
|
||||
|
||||
// RegisterEvents is the implementation of the HTTP API for Events.
|
||||
func (x *Operations) RegisterEvents(api huma.API) {
|
||||
name := "Events"
|
||||
description := "Returns the full events from a list of event Ids as a line structured JSON."
|
||||
path := "/events"
|
||||
scopes := []string{"user", "read"}
|
||||
method := http.MethodPost
|
||||
huma.Register(
|
||||
api, huma.Operation{
|
||||
OperationID: name,
|
||||
Summary: name,
|
||||
Path: path,
|
||||
Method: method,
|
||||
Tags: []string{"events"},
|
||||
Description: helpers.GenerateDescription(description, scopes),
|
||||
Security: []map[string][]string{{"auth": scopes}},
|
||||
DefaultStatus: 204,
|
||||
}, func(ctx context.T, input *EventsInput) (
|
||||
output *huma.StreamResponse, err error,
|
||||
) {
|
||||
// log.I.S(input)
|
||||
// if len(input.Body) == 10000 {
|
||||
// err = huma.Error400BadRequest(
|
||||
// "cannot process more than 10000 events in a request")
|
||||
// return
|
||||
// }
|
||||
// var authrequired bool
|
||||
// if len(input.Body) > 1000 {
|
||||
// authrequired = true
|
||||
// }
|
||||
// r := ctx.Value("http-request").(*http.Request)
|
||||
// var valid bool
|
||||
// var pubkey []byte
|
||||
// valid, pubkey, err = httpauth.CheckAuth(r)
|
||||
// // if there is an error but not that the token is missing, or there is no error
|
||||
// // but the signature is invalid, return error that request is unauthorized.
|
||||
// if err != nil && !errors.Is(err, httpauth.ErrMissingKey) {
|
||||
// err = huma.Error400BadRequest(err.Error())
|
||||
// return
|
||||
// }
|
||||
// err = nil
|
||||
// if authrequired && len(pubkey) != schnorr.PubKeyBytesLen {
|
||||
// err = huma.Error400BadRequest(
|
||||
// "cannot process more than 1000 events in a request without being authenticated")
|
||||
// return
|
||||
// }
|
||||
// if authrequired && valid {
|
||||
// if len(x.Owners()) < 1 {
|
||||
// err = huma.Error400BadRequest(
|
||||
// "cannot process more than 1000 events in a request without auth enabled")
|
||||
// return
|
||||
// }
|
||||
// if rl, ok := x.Relay().(*app.Relay); ok {
|
||||
// rl.Lock()
|
||||
// // we only allow the first level of the allowed users this kind of access
|
||||
// if _, ok = rl.OwnersFollowed[string(pubkey)]; !ok {
|
||||
// err = huma.Error403Forbidden(
|
||||
// fmt.Sprintf(
|
||||
// "authenticated user %0x does not have permission for this request (owners can use export)",
|
||||
// pubkey))
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if !valid {
|
||||
// err = huma.Error401Unauthorized("Authorization header is invalid")
|
||||
// return
|
||||
// }
|
||||
sto := x.Storage()
|
||||
var evIds [][]byte
|
||||
for _, id := range input.Body {
|
||||
var idb []byte
|
||||
if idb, err = hex.Dec(id); chk.E(err) {
|
||||
err = huma.Error422UnprocessableEntity(err.Error())
|
||||
return
|
||||
}
|
||||
if len(idb) != sha256.Size {
|
||||
err = huma.Error422UnprocessableEntity(
|
||||
fmt.Sprintf(
|
||||
"event Id must be 64 hex characters: '%s'", id,
|
||||
),
|
||||
)
|
||||
}
|
||||
evIds = append(evIds, idb)
|
||||
}
|
||||
if idsWriter, ok := sto.(store.GetIdsWriter); ok {
|
||||
output = &huma.StreamResponse{
|
||||
func(ctx huma.Context) {
|
||||
if err = idsWriter.FetchIds(
|
||||
x.Context(), tag.New(evIds...),
|
||||
ctx.BodyWriter(),
|
||||
); chk.E(err) {
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
return
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/app/realy/helpers"
|
||||
"orly.dev/utils/log"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
// ExportInput is the parameters for the HTTP API Export method.
|
||||
type ExportInput struct {
|
||||
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"true"`
|
||||
}
|
||||
|
||||
// ExportOutput is the return value of Export. It usually will be line structured JSON. In
|
||||
// future there may be more output formats.
|
||||
type ExportOutput struct{ RawBody []byte }
|
||||
|
||||
// RegisterExport implements the Export HTTP API method.
|
||||
func (x *Operations) RegisterExport(api huma.API) {
|
||||
name := "Export"
|
||||
description := "Export all events (only works with NIP-98/JWT capable client, will not work with UI)"
|
||||
path := "/export"
|
||||
scopes := []string{"admin", "read"}
|
||||
method := http.MethodGet
|
||||
huma.Register(
|
||||
api, huma.Operation{
|
||||
OperationID: name,
|
||||
Summary: name,
|
||||
Path: path,
|
||||
Method: method,
|
||||
Tags: []string{"admin"},
|
||||
Description: helpers.GenerateDescription(description, scopes),
|
||||
Security: []map[string][]string{{"auth": scopes}},
|
||||
}, func(ctx context.T, input *ExportInput) (
|
||||
resp *huma.StreamResponse, err error,
|
||||
) {
|
||||
// r := ctx.Value("http-request").(*http.Request)
|
||||
// rr := helpers.GetRemoteFromReq(r)
|
||||
// log.I.F("processing export from %s", rr)
|
||||
// // w := ctx.Value("http-response").(http.ResponseWriter)
|
||||
// authed, pubkey := x.AdminAuth(r)
|
||||
// if !authed {
|
||||
// // pubkey = ev.Pubkey
|
||||
// err = huma.Error401Unauthorized("Not Authorized")
|
||||
// return
|
||||
// }
|
||||
// log.I.F("export of event data requested on admin port from %s pubkey %0x",
|
||||
// rr, pubkey)
|
||||
sto := x.Storage()
|
||||
resp = &huma.StreamResponse{
|
||||
func(ctx huma.Context) {
|
||||
ctx.SetHeader("Content-Type", "application/nostr+jsonl")
|
||||
sto.Export(x.Context(), ctx.BodyWriter())
|
||||
if f, ok := ctx.BodyWriter().(http.Flusher); ok {
|
||||
f.Flush()
|
||||
} else {
|
||||
log.W.F("error: unable to flush")
|
||||
}
|
||||
},
|
||||
}
|
||||
return
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/app/realy/helpers"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/log"
|
||||
"sort"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"orly.dev/encoders/filter"
|
||||
"orly.dev/encoders/filters"
|
||||
"orly.dev/encoders/hex"
|
||||
"orly.dev/encoders/kind"
|
||||
"orly.dev/encoders/kinds"
|
||||
"orly.dev/encoders/tag"
|
||||
"orly.dev/encoders/tags"
|
||||
"orly.dev/encoders/timestamp"
|
||||
"orly.dev/interfaces/store"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
// SimpleFilter is the main parts of a filter.F that relate to event store indexes.
|
||||
type SimpleFilter struct {
|
||||
Kinds []int `json:"kinds,omitempty" doc:"array of kind numbers to match on"`
|
||||
Authors []string `json:"authors,omitempty" doc:"array of author pubkeys to match on (hex encoded)"`
|
||||
Tags [][]string `json:"tags,omitempty" doc:"array of tags to match on (first key of each '#x' and terms to match from the second field of the event tag)"`
|
||||
}
|
||||
|
||||
// FilterInput is the parameters for a Filter HTTP API call.
|
||||
type FilterInput struct {
|
||||
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"false"`
|
||||
Since int64 `query:"since" doc:"timestamp of the oldest events to return (inclusive)"`
|
||||
Until int64 `query:"until" doc:"timestamp of the newest events to return (inclusive)"`
|
||||
Limit uint `query:"limit" doc:"maximum number of results to return"`
|
||||
Sort string `query:"sort" enum:"asc,desc" default:"desc" doc:"sort order by created_at timestamp"`
|
||||
Body SimpleFilter `body:"filter" doc:"filter criteria to match for events to return"`
|
||||
}
|
||||
|
||||
// ToFilter converts a SimpleFilter input to a regular nostr filter.F.
|
||||
func (fi FilterInput) ToFilter() (f *filter.F, err error) {
|
||||
f = filter.New()
|
||||
var ks []*kind.T
|
||||
for _, k := range fi.Body.Kinds {
|
||||
ks = append(ks, kind.New(k))
|
||||
}
|
||||
f.Kinds = kinds.New(ks...)
|
||||
var as [][]byte
|
||||
for _, a := range fi.Body.Authors {
|
||||
var b []byte
|
||||
if b, err = hex.Dec(a); chk.E(err) {
|
||||
return
|
||||
}
|
||||
as = append(as, b)
|
||||
}
|
||||
f.Authors = tag.New(as...)
|
||||
var ts []*tag.T
|
||||
for _, t := range fi.Body.Tags {
|
||||
ts = append(ts, tag.New(t...))
|
||||
}
|
||||
f.Tags = tags.New(ts...)
|
||||
if fi.Limit != 0 {
|
||||
f.Limit = &fi.Limit
|
||||
}
|
||||
if fi.Since != 0 {
|
||||
f.Since = timestamp.New(fi.Since)
|
||||
}
|
||||
if fi.Until != 0 {
|
||||
f.Until = timestamp.New(fi.Until)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FilterOutput is a list of event Ids that match the query in the sort order requested.
|
||||
type FilterOutput struct {
|
||||
Body []string `doc:"list of event Ids that mach the query in the sort order requested"`
|
||||
}
|
||||
|
||||
// RegisterFilter is the implementation of the HTTP API Filter method.
|
||||
func (x *Operations) RegisterFilter(api huma.API) {
|
||||
name := "Filter"
|
||||
description := "Search for events and receive a sorted list of event Ids (one of authors, kinds or tags must be present)"
|
||||
path := "/filter"
|
||||
scopes := []string{"user", "read"}
|
||||
method := http.MethodPost
|
||||
huma.Register(
|
||||
api, huma.Operation{
|
||||
OperationID: name,
|
||||
Summary: name,
|
||||
Path: path,
|
||||
Method: method,
|
||||
Tags: []string{"events"},
|
||||
Description: helpers.GenerateDescription(description, scopes),
|
||||
Security: []map[string][]string{{"auth": scopes}},
|
||||
}, func(ctx context.T, input *FilterInput) (
|
||||
output *FilterOutput, err error,
|
||||
) {
|
||||
log.I.S(input)
|
||||
var f *filter.F
|
||||
if f, err = input.ToFilter(); chk.E(err) {
|
||||
err = huma.Error422UnprocessableEntity(err.Error())
|
||||
return
|
||||
}
|
||||
log.I.F("%s", f.Marshal(nil))
|
||||
// r := ctx.Value("http-request").(*http.Request)
|
||||
// rr := helpers.GetRemoteFromReq(r)
|
||||
// if len(input.Body.Authors) < 1 && len(input.Body.Kinds) < 1 && len(input.Body.Tags) < 1 {
|
||||
// err = huma.Error400BadRequest(
|
||||
// "cannot process filter with none of Authors/Kinds/Tags")
|
||||
// return
|
||||
// }
|
||||
// var valid bool
|
||||
// var pubkey []byte
|
||||
// valid, pubkey, err = httpauth.CheckAuth(r)
|
||||
// if there is an error but not that the token is missing, or there is no error
|
||||
// but the signature is invalid, return error that request is unauthorized.
|
||||
// if err != nil && !errors.Is(err, httpauth.ErrMissingKey) {
|
||||
// err = huma.Error400BadRequest(err.Error())
|
||||
// return
|
||||
// }
|
||||
// err = nil
|
||||
// if !valid {
|
||||
// err = huma.Error401Unauthorized("Authorization header is invalid")
|
||||
// return
|
||||
// }
|
||||
allowed := filters.New(f)
|
||||
// if accepter, ok := x.Relay().(relay.ReqAcceptor); ok {
|
||||
// var accepted, modified bool
|
||||
// allowed, accepted, modified = accepter.AcceptReq(x.Context(), r, nil,
|
||||
// filters.New(f), pubkey)
|
||||
// if !accepted {
|
||||
// err = huma.Error401Unauthorized("auth to get access for this filter")
|
||||
// return
|
||||
// } else if modified {
|
||||
// log.D.F("filter modified %s", allowed.F[0])
|
||||
// }
|
||||
// }
|
||||
// if len(allowed.F) == 0 {
|
||||
// err = huma.Error401Unauthorized("all kinds in event restricted; auth to get access for this filter")
|
||||
// return
|
||||
// }
|
||||
// if f.Kinds.IsPrivileged() {
|
||||
// if auther, ok := x.Relay().(relay.Authenticator); ok && auther.AuthRequired() {
|
||||
// log.F.F("privileged request\n%s", f.Serialize())
|
||||
// senders := f.Authors
|
||||
// receivers := f.Tags.GetAll(tag.New("#p"))
|
||||
// switch {
|
||||
// case len(pubkey) == 0:
|
||||
// err = huma.Error401Unauthorized("auth required for processing request due to presence of privileged kinds (DMs, app specific data)")
|
||||
// return
|
||||
// case senders.Contains(pubkey) || receivers.ContainsAny([]byte("#p"),
|
||||
// tag.New(pubkey)):
|
||||
// log.F.F("user %0x from %s allowed to query for privileged event",
|
||||
// pubkey, rr)
|
||||
// default:
|
||||
// err = huma.Error403Forbidden(fmt.Sprintf(
|
||||
// "authenticated user %0x does not have authorization for "+
|
||||
// "requested filters", pubkey))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
sto := x.Storage()
|
||||
var ok bool
|
||||
var quer store.Querier
|
||||
if quer, ok = sto.(store.Querier); !ok {
|
||||
err = huma.Error501NotImplemented("simple filter request not implemented")
|
||||
return
|
||||
}
|
||||
var evs []store.IdPkTs
|
||||
if evs, err = quer.QueryForIds(
|
||||
x.Context(), allowed.F[0],
|
||||
); chk.E(err) {
|
||||
err = huma.Error500InternalServerError(
|
||||
"error querying for events", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
if input.Limit > 0 {
|
||||
evs = evs[:input.Limit]
|
||||
}
|
||||
switch input.Sort {
|
||||
case "asc":
|
||||
sort.Slice(
|
||||
evs, func(i, j int) bool {
|
||||
return evs[i].Ts < evs[j].Ts
|
||||
},
|
||||
)
|
||||
case "desc":
|
||||
sort.Slice(
|
||||
evs, func(i, j int) bool {
|
||||
return evs[i].Ts > evs[j].Ts
|
||||
},
|
||||
)
|
||||
}
|
||||
// if len(pubkey) > 0 {
|
||||
// // remove events from results if we find the user's mute list, that are present
|
||||
// // on this list
|
||||
// var mutes event.S
|
||||
// if mutes, err = sto.QueryEvents(x.Context(), &filter.F{Authors: tag.New(pubkey),
|
||||
// Kinds: kinds.New(kind.MuteList)}); !chk.E(err) {
|
||||
// var mutePubs [][]byte
|
||||
// for _, ev := range mutes {
|
||||
// for _, t := range ev.Tags.ToSliceOfTags() {
|
||||
// if bytes.Equal(t.Key(), []byte("p")) {
|
||||
// var p []byte
|
||||
// if p, err = hex.Dec(string(t.Value())); chk.E(err) {
|
||||
// continue
|
||||
// }
|
||||
// mutePubs = append(mutePubs, p)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// var tmp []store.IdTsPk
|
||||
// next:
|
||||
// for _, ev := range evs {
|
||||
// for _, pk := range mutePubs {
|
||||
// if bytes.Equal(ev.Pub, pk) {
|
||||
// continue next
|
||||
// }
|
||||
// }
|
||||
// tmp = append(tmp, ev)
|
||||
// }
|
||||
// // log.I.ToSliceOfBytes("done")
|
||||
// evs = tmp
|
||||
// }
|
||||
// }
|
||||
output = &FilterOutput{}
|
||||
for _, ev := range evs {
|
||||
output.Body = append(output.Body, hex.Enc(ev.Id))
|
||||
}
|
||||
return
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"net/http"
|
||||
"orly.dev/app/realy/helpers"
|
||||
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
// ImportInput is the parameters of an import operation, authentication and the stream of line
|
||||
// structured JSON events.
|
||||
type ImportInput struct {
|
||||
Auth string `header:"Authorization" doc:"nostr nip-98 token for authentication" required:"true"`
|
||||
RawBody []byte
|
||||
}
|
||||
|
||||
// ImportOutput is nothing, basically, a 204 or 200 status is expected.
|
||||
type ImportOutput struct{}
|
||||
|
||||
// RegisterImport is the implementation of the Import operation.
|
||||
func (x *Operations) RegisterImport(api huma.API) {
|
||||
name := "Import"
|
||||
description := "Import events from line structured JSON (jsonl)"
|
||||
path := "/import"
|
||||
scopes := []string{"admin", "write"}
|
||||
method := http.MethodPost
|
||||
huma.Register(
|
||||
api, huma.Operation{
|
||||
OperationID: name,
|
||||
Summary: name,
|
||||
Path: path,
|
||||
Method: method,
|
||||
Tags: []string{"admin"},
|
||||
Description: helpers.GenerateDescription(description, scopes),
|
||||
Security: []map[string][]string{{"auth": scopes}},
|
||||
DefaultStatus: 204,
|
||||
},
|
||||
func(ctx context.T, input *ImportInput) (wgh *ImportOutput, err error) {
|
||||
// r := ctx.Value("http-request").(*http.Request)
|
||||
// rr := helpers.GetRemoteFromReq(r)
|
||||
// authed, pubkey := x.AdminAuth(r, time.Minute*10)
|
||||
// if !authed {
|
||||
// // pubkey = ev.Pubkey
|
||||
// err = huma.Error401Unauthorized(
|
||||
// fmt.Sprintf("user %0x not authorized for action", pubkey))
|
||||
// return
|
||||
// }
|
||||
sto := x.Storage()
|
||||
if len(input.RawBody) > 0 {
|
||||
read := bytes.NewBuffer(input.RawBody)
|
||||
sto.Import(read)
|
||||
// if realy, ok := x.Relay().(*app.Relay); ok {
|
||||
// realy.ZeroLists()
|
||||
// realy.CheckOwnerLists(context.Bg())
|
||||
// }
|
||||
// } else {
|
||||
// log.I.F("import of event data requested on admin port from %s pubkey %0x", rr,
|
||||
// pubkey)
|
||||
// read := io.LimitReader(r.Body, r.ContentLength)
|
||||
// sto.Import(read)
|
||||
// if realy, ok := x.Relay().(*app.Relay); ok {
|
||||
// realy.ZeroLists()
|
||||
// realy.CheckOwnerLists(context.Bg())
|
||||
// }
|
||||
}
|
||||
return
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/app/realy/helpers"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/log"
|
||||
"strings"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"orly.dev/interfaces/store"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
// NukeInput is the parameters for the HTTP API method nuke. Note that it has a confirmation
|
||||
// header that must be provided to prevent accidental invocation of this method.
|
||||
type NukeInput struct {
|
||||
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"true"`
|
||||
Confirm string `header:"X-Confirm" doc:"must put 'Yes I Am Sure' in this field as confirmation"`
|
||||
}
|
||||
|
||||
// NukeOutput is basically nothing, a 200 or 204 HTTP status response is normal.
|
||||
type NukeOutput struct{}
|
||||
|
||||
// RegisterNuke is the implementation of the Wipe HTTP API method.
|
||||
func (x *Operations) RegisterNuke(api huma.API) {
|
||||
name := "Wipe"
|
||||
description := "Wipe all events in the database"
|
||||
path := "/nuke"
|
||||
scopes := []string{"admin", "write"}
|
||||
method := http.MethodGet
|
||||
huma.Register(
|
||||
api, huma.Operation{
|
||||
OperationID: name,
|
||||
Summary: name,
|
||||
Path: path,
|
||||
Method: method,
|
||||
Tags: []string{"admin"},
|
||||
Description: helpers.GenerateDescription(description, scopes),
|
||||
Security: []map[string][]string{{"auth": scopes}},
|
||||
DefaultStatus: 204,
|
||||
}, func(ctx context.T, input *NukeInput) (wgh *NukeOutput, err error) {
|
||||
// r := ctx.Value("http-request").(*http.Request)
|
||||
// // w := ctx.Value("http-response").(http.ResponseWriter)
|
||||
// rr := helpers.GetRemoteFromReq(r)
|
||||
// authed, pubkey := x.AdminAuth(r)
|
||||
// if !authed {
|
||||
// // pubkey = ev.Pubkey
|
||||
// err = huma.Error401Unauthorized("user not authorized for action")
|
||||
// return
|
||||
// }
|
||||
if input.Confirm != "Yes I Am Sure" {
|
||||
err = huma.Error403Forbidden("Confirm missing or incorrect")
|
||||
return
|
||||
}
|
||||
// log.I.F("database nuke request from %s pubkey %0x", rr, pubkey)
|
||||
sto := x.Storage()
|
||||
if nuke, ok := sto.(store.Wiper); ok {
|
||||
log.I.F("rescanning")
|
||||
if err = nuke.Wipe(); chk.E(err) {
|
||||
if strings.HasPrefix(err.Error(), "Value log GC attempt") {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"orly.dev/app/realy/helpers"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/log"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
// RelayInput is the parameters for the Event HTTP API method.
|
||||
type RelayInput struct {
|
||||
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"false"`
|
||||
RawBody []byte
|
||||
}
|
||||
|
||||
// RelayOutput is the return parameters for the HTTP API Relay method.
|
||||
type RelayOutput struct{ Body string }
|
||||
|
||||
// RegisterRelay is the implementatino of the HTTP API Relay method.
|
||||
func (x *Operations) RegisterRelay(api huma.API) {
|
||||
name := "relay"
|
||||
description := "relay an event, don't store it"
|
||||
path := "/relay"
|
||||
scopes := []string{"user"}
|
||||
method := http.MethodPost
|
||||
huma.Register(
|
||||
api, huma.Operation{
|
||||
OperationID: name,
|
||||
Summary: name,
|
||||
Path: path,
|
||||
Method: method,
|
||||
Tags: []string{"events"},
|
||||
Description: helpers.GenerateDescription(description, scopes),
|
||||
Security: []map[string][]string{{"auth": scopes}},
|
||||
}, func(ctx context.T, input *RelayInput) (
|
||||
output *RelayOutput, err error,
|
||||
) {
|
||||
log.I.S(input)
|
||||
// r := ctx.Value("http-request").(*http.Request)
|
||||
// rr := helpers.GetRemoteFromReq(r)
|
||||
// var valid bool
|
||||
// var pubkey []byte
|
||||
// valid, pubkey, err = httpauth.CheckAuth(r)
|
||||
// // if there is an error but not that the token is missing, or there is no error
|
||||
// // but the signature is invalid, return error that request is unauthorized.
|
||||
// if err != nil && !errors.Is(err, httpauth.ErrMissingKey) {
|
||||
// err = huma.Error400BadRequest(err.Error())
|
||||
// return
|
||||
// }
|
||||
// err = nil
|
||||
// if !valid {
|
||||
// err = huma.Error401Unauthorized("Authorization header is invalid")
|
||||
// return
|
||||
// }
|
||||
var ok bool
|
||||
// if there was auth, or no auth, check the relay policy allows accepting the
|
||||
// event (no auth with auth required or auth not valid for action can apply
|
||||
// here).
|
||||
ev := &event.E{}
|
||||
if _, err = ev.Unmarshal(input.RawBody); chk.E(err) {
|
||||
err = huma.Error406NotAcceptable(err.Error())
|
||||
return
|
||||
}
|
||||
// accept, notice, _ := x.AcceptEvent(ctx, ev, r, rr, pubkey)
|
||||
// if !accept {
|
||||
// err = huma.Error401Unauthorized(notice)
|
||||
// return
|
||||
// }
|
||||
if !bytes.Equal(ev.GetIDBytes(), ev.Id) {
|
||||
err = huma.Error400BadRequest("event id is computed incorrectly")
|
||||
return
|
||||
}
|
||||
if ok, err = ev.Verify(); chk.T(err) {
|
||||
err = huma.Error400BadRequest("failed to verify signature")
|
||||
return
|
||||
} else if !ok {
|
||||
err = huma.Error400BadRequest("signature is invalid")
|
||||
return
|
||||
}
|
||||
// var authRequired bool
|
||||
// var ar relay.Authenticator
|
||||
// if ar, ok = x.Relay().(relay.Authenticator); ok {
|
||||
// authRequired = ar.AuthRequired()
|
||||
// }
|
||||
x.Publisher().Deliver(ev)
|
||||
return
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/app/realy/helpers"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/log"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"orly.dev/interfaces/store"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
type RescanInput struct {
|
||||
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"true"`
|
||||
}
|
||||
|
||||
type RescanOutput struct{}
|
||||
|
||||
func (x *Operations) RegisterRescan(api huma.API) {
|
||||
name := "Rescan"
|
||||
description := "Rescan all events and rewrite their indexes (to enable new indexes on old events)"
|
||||
path := "/rescan"
|
||||
scopes := []string{"admin"}
|
||||
method := http.MethodGet
|
||||
huma.Register(
|
||||
api, huma.Operation{
|
||||
OperationID: name,
|
||||
Summary: name,
|
||||
Path: path,
|
||||
Method: method,
|
||||
Tags: []string{"admin"},
|
||||
Description: helpers.GenerateDescription(description, scopes),
|
||||
Security: []map[string][]string{{"auth": scopes}},
|
||||
DefaultStatus: 204,
|
||||
},
|
||||
func(ctx context.T, input *RescanInput) (wgh *RescanOutput, err error) {
|
||||
// r := ctx.Value("http-request").(*http.Request)
|
||||
// rr := helpers.GetRemoteFromReq(r)
|
||||
// authed, pubkey := x.AdminAuth(r)
|
||||
// if !authed {
|
||||
// err = huma.Error401Unauthorized("not authorized")
|
||||
// return
|
||||
// }
|
||||
// log.I.F("index rescan requested on admin port from %s pubkey %0x",
|
||||
// rr, pubkey)
|
||||
sto := x.Storage()
|
||||
if rescanner, ok := sto.(store.Rescanner); ok {
|
||||
log.I.F("rescanning")
|
||||
if err = rescanner.Rescan(); chk.E(err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/app/realy/helpers"
|
||||
"time"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
type ShutdownInput struct {
|
||||
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"true"`
|
||||
}
|
||||
|
||||
type ShutdownOutput struct{}
|
||||
|
||||
func (x *Operations) RegisterShutdown(api huma.API) {
|
||||
name := "Shutdown"
|
||||
description := "Shutdown relay"
|
||||
path := "/shutdown"
|
||||
scopes := []string{"admin"}
|
||||
method := http.MethodGet
|
||||
huma.Register(
|
||||
api, huma.Operation{
|
||||
OperationID: name,
|
||||
Summary: name,
|
||||
Path: path,
|
||||
Method: method,
|
||||
Tags: []string{"admin"},
|
||||
Description: helpers.GenerateDescription(description, scopes),
|
||||
Security: []map[string][]string{{"auth": scopes}},
|
||||
DefaultStatus: 204,
|
||||
}, func(ctx context.T, input *ShutdownInput) (
|
||||
wgh *ShutdownOutput, err error,
|
||||
) {
|
||||
// r := ctx.Value("http-request").(*http.Request)
|
||||
// authed, _ := x.AdminAuth(r)
|
||||
// if !authed {
|
||||
// err = huma.Error401Unauthorized("authorization required")
|
||||
// return
|
||||
// }
|
||||
go func() {
|
||||
time.Sleep(time.Second)
|
||||
x.Shutdown()
|
||||
}()
|
||||
return
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/app/realy/helpers"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/log"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/danielgtaylor/huma/v2/sse"
|
||||
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/encoders/filter"
|
||||
"orly.dev/encoders/filters"
|
||||
"orly.dev/encoders/hex"
|
||||
"orly.dev/encoders/kind"
|
||||
"orly.dev/encoders/kinds"
|
||||
"orly.dev/encoders/tag"
|
||||
"orly.dev/encoders/tags"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
type SubscribeInput struct {
|
||||
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"false"`
|
||||
Accept string `header:"Accept" default:"text/event-stream" enum:"text/event-stream" required:"true"`
|
||||
// ContentType string `header:"Content-Type" default:"text/event-stream" enum:"text/event-stream" required:"true"`
|
||||
Body SimpleFilter `body:"filter" doc:"filter criteria to match for events to return"`
|
||||
}
|
||||
|
||||
func (fi SubscribeInput) ToFilter() (f *filter.F, err error) {
|
||||
f = filter.New()
|
||||
var ks []*kind.T
|
||||
for _, k := range fi.Body.Kinds {
|
||||
ks = append(ks, kind.New(k))
|
||||
}
|
||||
f.Kinds = kinds.New(ks...)
|
||||
var as [][]byte
|
||||
for _, a := range fi.Body.Authors {
|
||||
var b []byte
|
||||
if b, err = hex.Dec(a); chk.E(err) {
|
||||
return
|
||||
}
|
||||
as = append(as, b)
|
||||
}
|
||||
f.Authors = tag.New(as...)
|
||||
var ts []*tag.T
|
||||
for _, t := range fi.Body.Tags {
|
||||
ts = append(ts, tag.New(t...))
|
||||
}
|
||||
f.Tags = tags.New(ts...)
|
||||
return
|
||||
}
|
||||
|
||||
func (x *Operations) RegisterSubscribe(api huma.API) {
|
||||
name := "Subscribe"
|
||||
description := "Subscribe for newly published events by author, kind or tags; empty also allowed, which just sends all incoming events - uses Server Sent Events format for compatibility with standard libraries."
|
||||
path := "/subscribe"
|
||||
scopes := []string{"user", "read"}
|
||||
method := http.MethodPost
|
||||
sse.Register(
|
||||
api, huma.Operation{
|
||||
OperationID: name,
|
||||
Summary: name,
|
||||
Path: path,
|
||||
Method: method,
|
||||
Tags: []string{"events"},
|
||||
Description: helpers.GenerateDescription(description, scopes),
|
||||
Security: []map[string][]string{{"auth": scopes}},
|
||||
},
|
||||
map[string]any{
|
||||
"event": event.J{},
|
||||
},
|
||||
func(ctx context.T, input *SubscribeInput, send sse.Sender) {
|
||||
log.I.S(input)
|
||||
var err error
|
||||
var f *filter.F
|
||||
if f, err = input.ToFilter(); chk.E(err) {
|
||||
err = huma.Error422UnprocessableEntity(err.Error())
|
||||
return
|
||||
}
|
||||
log.I.F("%s", f.Marshal(nil))
|
||||
r := ctx.Value("http-request").(*http.Request)
|
||||
// rr := helpers.GetRemoteFromReq(r)
|
||||
// var valid bool
|
||||
// var pubkey []byte
|
||||
// valid, pubkey, err = httpauth.CheckAuth(r)
|
||||
// // if there is an error but not that the token is missing, or there is no error
|
||||
// // but the signature is invalid, return error that request is unauthorized.
|
||||
// if err != nil && !errors.Is(err, httpauth.ErrMissingKey) {
|
||||
// err = huma.Error400BadRequest(err.Error())
|
||||
// return
|
||||
// }
|
||||
// err = nil
|
||||
// if !valid {
|
||||
// err = huma.Error401Unauthorized("Authorization header is invalid")
|
||||
// return
|
||||
// }
|
||||
allowed := filters.New(f)
|
||||
// if accepter, ok := x.Relay().(relay.ReqAcceptor); ok {
|
||||
// var accepted, modified bool
|
||||
// allowed, accepted, modified = accepter.AcceptReq(x.Context(), r, nil,
|
||||
// filters.New(f),
|
||||
// pubkey)
|
||||
// if !accepted {
|
||||
// err = huma.Error401Unauthorized("auth to get access for this filter")
|
||||
// return
|
||||
// } else if modified {
|
||||
// log.D.F("filter modified %s", allowed.F[0])
|
||||
// }
|
||||
// }
|
||||
if len(allowed.F) == 0 {
|
||||
err = huma.Error401Unauthorized("all kinds in event restricted; auth to get access for this filter")
|
||||
return
|
||||
}
|
||||
// if f.Kinds.IsPrivileged() {
|
||||
// if auther, ok := x.Relay().(relay.Authenticator); ok && auther.AuthRequired() {
|
||||
// log.F.F("privileged request\n%s", f.Serialize())
|
||||
// senders := f.Authors
|
||||
// receivers := f.Tags.GetAll(tag.New("#p"))
|
||||
// switch {
|
||||
// case len(pubkey) == 0:
|
||||
// err = huma.Error401Unauthorized("auth required for processing request due to presence of privileged kinds (DMs, app specific data)")
|
||||
// return
|
||||
// case senders.Contains(pubkey) || receivers.ContainsAny([]byte("#p"),
|
||||
// tag.New(pubkey)):
|
||||
// log.F.F("user %0x from %s allowed to query for privileged event",
|
||||
// pubkey, rr)
|
||||
// default:
|
||||
// err = huma.Error403Forbidden(fmt.Sprintf(
|
||||
// "authenticated user %0x does not have authorization for "+
|
||||
// "requested filters", pubkey))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// register the filter with the listeners
|
||||
receiver := make(event.C, 32)
|
||||
x.Publisher().Receive(
|
||||
&H{
|
||||
Ctx: r.Context(),
|
||||
Receiver: receiver,
|
||||
// Pubkey: pubkey,
|
||||
Filter: f,
|
||||
},
|
||||
)
|
||||
out:
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
break out
|
||||
case ev := <-receiver:
|
||||
if err = send.Data(ev.ToEventJ()); chk.E(err) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/danielgtaylor/huma/v2/adapters/humago"
|
||||
)
|
||||
|
||||
// ExposeMiddleware adds the http.Request and http.ResponseWriter to the context
|
||||
// for the Operations handler.
|
||||
func ExposeMiddleware(ctx huma.Context, next func(huma.Context)) {
|
||||
// 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, name, version, description string) (api huma.API) {
|
||||
config := huma.DefaultConfig(name, version)
|
||||
config.Info.Description = description
|
||||
config.DocsPath = ""
|
||||
router.ServeMux.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="/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
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"orly.dev/app/realy/publish/publisher"
|
||||
"sync"
|
||||
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/encoders/filter"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
const Type = "openapi"
|
||||
|
||||
// H is the control structure for a HTTP SSE subscription, including the filter, authed
|
||||
// pubkey and a channel to send the events to.
|
||||
type H struct {
|
||||
// Ctx is the http.Request context of the subscriber, this enables garbage
|
||||
// collecting the subscriptions from http.
|
||||
Ctx context.T
|
||||
// Receiver is a channel that the listener sends subscription events to for http
|
||||
// subscribe endpoint.
|
||||
Receiver event.C
|
||||
// // Pubkey is the pubkey authed to this subscription
|
||||
// Pubkey []byte
|
||||
// Filter is the filter associated with the http subscription
|
||||
Filter *filter.F
|
||||
}
|
||||
|
||||
func (h *H) Type() string { return Type }
|
||||
|
||||
// Map is a collection of H TTP subscriptions.
|
||||
type Map map[*H]struct{}
|
||||
|
||||
type S struct {
|
||||
// Map is the map of subscriptions from the http api.
|
||||
Map
|
||||
// HLock is the mutex that locks the Map.
|
||||
Mx sync.Mutex
|
||||
}
|
||||
|
||||
var _ publisher.I = &S{}
|
||||
|
||||
func New() *S { return &S{Map: make(Map)} }
|
||||
|
||||
func (p *S) Type() string { return Type }
|
||||
|
||||
func (p *S) Receive(msg publisher.Message) {
|
||||
if m, ok := msg.(*H); ok {
|
||||
p.Mx.Lock()
|
||||
p.Map[m] = struct{}{}
|
||||
p.Mx.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *S) Deliver(ev *event.E) {
|
||||
p.Mx.Lock()
|
||||
var subs []*H
|
||||
for sub := range p.Map {
|
||||
// check if the subscription's subscriber is still alive
|
||||
select {
|
||||
case <-sub.Ctx.Done():
|
||||
subs = append(subs, sub)
|
||||
default:
|
||||
}
|
||||
}
|
||||
for _, sub := range subs {
|
||||
delete(p.Map, sub)
|
||||
}
|
||||
subs = subs[:0]
|
||||
for sub := range p.Map {
|
||||
// if auth required, check the subscription pubkey matches
|
||||
// if !publicReadable {
|
||||
// if authRequired && len(sub.Pubkey) == 0 {
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
// if the filter doesn't match, skip
|
||||
if !sub.Filter.Matches(ev) {
|
||||
continue
|
||||
}
|
||||
// // if the filter is privileged and the user doesn't have matching auth, skip
|
||||
// if ev.Kind.IsPrivileged() {
|
||||
// ab := sub.Pubkey
|
||||
// var containsPubkey bool
|
||||
// if ev.Tags != nil {
|
||||
// containsPubkey = ev.Tags.ContainsAny([]byte{'p'}, tag.New(ab))
|
||||
// }
|
||||
// if !bytes.Equal(ev.Pubkey, ab) || containsPubkey {
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
// send the event to the subscriber
|
||||
sub.Receiver <- ev
|
||||
}
|
||||
p.Mx.Unlock()
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import "net/http"
|
||||
|
||||
type ServeMux struct {
|
||||
*http.ServeMux
|
||||
}
|
||||
|
||||
func NewServeMux() *ServeMux {
|
||||
return &ServeMux{http.NewServeMux()}
|
||||
}
|
||||
|
||||
func (c *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
if r.Method == http.MethodOptions {
|
||||
return
|
||||
}
|
||||
c.ServeMux.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -23,7 +23,8 @@ type NIP struct {
|
||||
// N returns the number of a nostr "improvement" possibility.
|
||||
func (n NIP) N() int { return n.Number }
|
||||
|
||||
// GetList converts a NIP into a number.List of simple numbers, sorted in ascending order.
|
||||
// GetList converts a NIP into a number.List of simple numbers, sorted in
|
||||
// ascending order.
|
||||
func GetList(items ...NIP) (n number.List) {
|
||||
for _, item := range items {
|
||||
n = append(n, item.N())
|
||||
@@ -32,7 +33,8 @@ func GetList(items ...NIP) (n number.List) {
|
||||
return
|
||||
}
|
||||
|
||||
// this is the list of all nips and their titles for use in supported_nips field
|
||||
// this is the list of all nips and their titles for use in the supported_nips
|
||||
// field
|
||||
var (
|
||||
BasicProtocol = NIP{"Basic protocol flow description", 1}
|
||||
NIP1 = BasicProtocol
|
||||
@@ -175,20 +177,20 @@ var NIPMap = map[int]NIP{
|
||||
|
||||
// Limits are rules about what is acceptable for events and filters on a relay.
|
||||
type Limits struct {
|
||||
// MaxMessageLength is the maximum number of bytes for incoming JSON
|
||||
// that the relay will attempt to decode and act upon. When you send large
|
||||
// MaxMessageLength is the maximum number of bytes for incoming JSON that
|
||||
// the relay will attempt to decode and act upon. When you send large
|
||||
// subscriptions, you will be limited by this value. It also effectively
|
||||
// limits the maximum size of any event. Value is calculated from [ to ] and
|
||||
// is after UTF-8 serialization (so some unicode characters will cost 2-3
|
||||
// is after UTF-8 serialization (so some Unicode characters will cost 2-3
|
||||
// bytes). It is equal to the maximum size of the WebSocket message frame.
|
||||
MaxMessageLength int `json:"max_message_length,omitempty"`
|
||||
// MaxSubscriptions is total number of subscriptions that may be active on a
|
||||
// single websocket connection to this relay. It's possible that
|
||||
// MaxSubscriptions is the total number of subscriptions that may be active
|
||||
// on a single websocket connection to this relay. It's possible that
|
||||
// authenticated clients with a (paid) relationship to the relay may have
|
||||
// higher limits.
|
||||
MaxSubscriptions int `json:"max_subscriptions,omitempty"`
|
||||
// MaxFilter is maximum number of filter values in each subscription. Must
|
||||
// be one or higher.
|
||||
// MaxFilter is the maximum number of filter values in each subscription.
|
||||
// Must be one or higher.
|
||||
MaxFilters int `json:"max_filters,omitempty"`
|
||||
// MaxLimit is the relay server will clamp each filter's limit value to this
|
||||
// number. This means the client won't be able to get more than this number
|
||||
@@ -210,17 +212,17 @@ type Limits struct {
|
||||
// MinPowDifficulty new events will require at least this difficulty of PoW,
|
||||
// based on NIP-13, or they will be rejected by this server.
|
||||
MinPowDifficulty int `json:"min_pow_difficulty,omitempty"`
|
||||
// AuthRequired means the realy requires NIP-42 authentication to happen
|
||||
// AuthRequired means the relay requires NIP-42 authentication to happen
|
||||
// before a new connection may perform any other action. Even if set to
|
||||
// False, authentication may be required for specific actions.
|
||||
AuthRequired bool `json:"auth_required"`
|
||||
// PaymentRequired this realy requires payment before a new connection may
|
||||
// PaymentRequired this relay requires payment before a new connection may
|
||||
// perform any action.
|
||||
PaymentRequired bool `json:"payment_required"`
|
||||
// RestrictedWrites this realy requires some kind of condition to be
|
||||
// fulfilled in order to accept events (not necessarily, but including
|
||||
// RestrictedWrites means this relay requires some kind of condition to be
|
||||
// fulfilled to accept events (not necessarily, but including
|
||||
// payment_required and min_pow_difficulty). This should only be set to true
|
||||
// when users are expected to know the realy policy before trying to write
|
||||
// when users are expected to know the relay policy before trying to write
|
||||
// to it -- like belonging to a special pubkey-based whitelist or writing
|
||||
// only events of a specific niche kind or content. Normal anti-spam
|
||||
// heuristics, for example, do not qualify.q
|
||||
@@ -241,13 +243,14 @@ type Sub struct {
|
||||
Period int `json:"period"`
|
||||
}
|
||||
|
||||
// Pub is a limitation for what you can store on the relay as a kinds.T and the cost (for???).
|
||||
// Pub is a limitation for what you can store on the relay as a kinds.T and the
|
||||
// cost (for???).
|
||||
type Pub struct {
|
||||
Kinds kinds.T `json:"kinds"`
|
||||
Payment
|
||||
}
|
||||
|
||||
// T is the realy information document.
|
||||
// T is the relay information document.
|
||||
type T struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
@@ -268,7 +271,7 @@ type T struct {
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// NewInfo populates the nips map and if an Info structure is provided it is
|
||||
// NewInfo populates the nips map, and if an Info structure is provided, it is
|
||||
// used and its nips map is populated if it isn't already.
|
||||
func NewInfo(inf *T) (info *T) {
|
||||
if inf != nil {
|
||||
|
||||
23
protocol/servemux/serveMux.go
Normal file
23
protocol/servemux/serveMux.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package servemux
|
||||
|
||||
import "net/http"
|
||||
|
||||
type S struct {
|
||||
*http.ServeMux
|
||||
}
|
||||
|
||||
func NewServeMux() *S {
|
||||
return &S{http.NewServeMux()}
|
||||
}
|
||||
|
||||
func (c *S) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
|
||||
w.Header().Set(
|
||||
"Access-Control-Allow-Headers", "Content-Type, Authorization",
|
||||
)
|
||||
if r.Method == http.MethodOptions {
|
||||
return
|
||||
}
|
||||
c.ServeMux.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
package socketapi
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"net/http"
|
||||
"orly.dev/crypto/ec/bech32"
|
||||
"orly.dev/encoders/bech32encoding"
|
||||
"orly.dev/utils/chk"
|
||||
|
||||
"github.com/fasthttp/websocket"
|
||||
"net/http"
|
||||
|
||||
"orly.dev/protocol/ws"
|
||||
)
|
||||
@@ -17,23 +12,24 @@ const (
|
||||
DefaultChallengeLength = 16
|
||||
)
|
||||
|
||||
// GetListener generates a new ws.Listener with a new challenge for a subscriber.
|
||||
// GetListener generates a new ws.Listener with a new challenge for a
|
||||
// subscriber.
|
||||
func GetListener(conn *websocket.Conn, req *http.Request) (w *ws.Listener) {
|
||||
var err error
|
||||
cb := make([]byte, DefaultChallengeLength)
|
||||
if _, err = rand.Read(cb); chk.E(err) {
|
||||
panic(err)
|
||||
}
|
||||
var b5 []byte
|
||||
if b5, err = bech32encoding.ConvertForBech32(cb); chk.E(err) {
|
||||
return
|
||||
}
|
||||
var encoded []byte
|
||||
if encoded, err = bech32.Encode(
|
||||
[]byte(DefaultChallengeHRP), b5,
|
||||
); chk.E(err) {
|
||||
return
|
||||
}
|
||||
w = ws.NewListener(conn, req, encoded)
|
||||
// var err error
|
||||
// cb := make([]byte, DefaultChallengeLength)
|
||||
// if _, err = rand.Read(cb); chk.E(err) {
|
||||
// panic(err)
|
||||
// }
|
||||
// var b5 []byte
|
||||
// if b5, err = bech32encoding.ConvertForBech32(cb); chk.E(err) {
|
||||
// return
|
||||
// }
|
||||
// var encoded []byte
|
||||
// if encoded, err = bech32.Encode(
|
||||
// []byte(DefaultChallengeHRP), b5,
|
||||
// ); chk.E(err) {
|
||||
// return
|
||||
// }
|
||||
w = ws.NewListener(conn, req)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,52 +1,13 @@
|
||||
package socketapi
|
||||
|
||||
import (
|
||||
"orly.dev/app/realy/interfaces"
|
||||
"orly.dev/interfaces/server"
|
||||
)
|
||||
|
||||
func (a *A) HandleAuth(
|
||||
req []byte,
|
||||
srv interfaces.Server,
|
||||
srv server.S,
|
||||
) (msg []byte) {
|
||||
|
||||
// if auther, ok := srv.Relay().(relay.Authenticator); ok && auther.AuthRequired() {
|
||||
// svcUrl := auther.ServiceUrl(a.Req())
|
||||
// if svcUrl == "" {
|
||||
// return
|
||||
// }
|
||||
// log.T.F("received auth response,%s", req)
|
||||
// var err error
|
||||
// var rem []byte
|
||||
// env := authenvelope.NewResponse()
|
||||
// if rem, err = env.Unmarshal(req); chk.E(err) {
|
||||
// return
|
||||
// }
|
||||
// if len(rem) > 0 {
|
||||
// log.I.F("extra '%s'", rem)
|
||||
// }
|
||||
// var valid bool
|
||||
// if valid, err = auth.Validate(env.Event, []byte(a.Challenge()),
|
||||
// svcUrl); chk.E(err) {
|
||||
// e := err.Error()
|
||||
// if err = okenvelope.NewFrom(env.Event.Id, false,
|
||||
// normalize.Error.F(err.Error())).Write(a.Listener); chk.E(err) {
|
||||
// return []byte(err.Error())
|
||||
// }
|
||||
// return normalize.Error.F(e)
|
||||
// } else if !valid {
|
||||
// if err = okenvelope.NewFrom(env.Event.Id, false,
|
||||
// normalize.Error.F("failed to authenticate")).Write(a.Listener); chk.E(err) {
|
||||
// return []byte(err.Error())
|
||||
// }
|
||||
// return normalize.Restricted.F("auth response does not validate")
|
||||
// } else {
|
||||
// if err = okenvelope.NewFrom(env.Event.Id, true,
|
||||
// []byte{}).Write(a.Listener); chk.E(err) {
|
||||
// return
|
||||
// }
|
||||
// log.D.F("%s authed to pubkey,%0x", a.RealRemote(), env.Event.Pubkey)
|
||||
// a.SetAuthed(string(env.Event.Pubkey))
|
||||
// }
|
||||
// }
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
package socketapi
|
||||
|
||||
import (
|
||||
"orly.dev/app/realy/interfaces"
|
||||
"orly.dev/encoders/envelopes/closeenvelope"
|
||||
"orly.dev/interfaces/server"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/log"
|
||||
)
|
||||
|
||||
// HandleClose processes a CLOSE envelope, intended to cancel a specific
|
||||
// subscription, and notifies the server to handle the cancellation.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// - req: A byte slice containing the raw CLOSE envelope data to process.
|
||||
//
|
||||
// - srv: The server instance responsible for managing subscription
|
||||
// operations, such as cancellation.
|
||||
//
|
||||
// Return values:
|
||||
//
|
||||
// - note: A byte slice containing an error message if issues occur during
|
||||
// processing; otherwise, an empty slice.
|
||||
//
|
||||
// Expected behavior:
|
||||
//
|
||||
// The method parses and validates the CLOSE envelope. If valid, it cancels the
|
||||
// corresponding subscription by notifying the server's publisher. If the
|
||||
// envelope is malformed or the subscription ID is missing, an error message is
|
||||
// returned instead. Logs any remaining unprocessed data for diagnostics.
|
||||
func (a *A) HandleClose(
|
||||
req []byte,
|
||||
srv interfaces.Server,
|
||||
srv server.S,
|
||||
) (note []byte) {
|
||||
var err error
|
||||
var rem []byte
|
||||
|
||||
@@ -2,7 +2,6 @@ package socketapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"orly.dev/app/realy/interfaces"
|
||||
"orly.dev/crypto/sha256"
|
||||
"orly.dev/encoders/envelopes/eventenvelope"
|
||||
"orly.dev/encoders/envelopes/okenvelope"
|
||||
@@ -13,13 +12,15 @@ import (
|
||||
"orly.dev/encoders/ints"
|
||||
"orly.dev/encoders/kind"
|
||||
"orly.dev/encoders/tag"
|
||||
"orly.dev/interfaces/server"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/context"
|
||||
"orly.dev/utils/log"
|
||||
"orly.dev/utils/normalize"
|
||||
)
|
||||
|
||||
// sendResponse is a helper function to send an okenvelope response and handle errors
|
||||
// sendResponse is a helper function to send an okenvelope response and handle
|
||||
// errors
|
||||
func (a *A) sendResponse(eventID []byte, ok bool, reason ...[]byte) error {
|
||||
var r []byte
|
||||
if len(reason) > 0 {
|
||||
@@ -28,8 +29,35 @@ func (a *A) sendResponse(eventID []byte, ok bool, reason ...[]byte) error {
|
||||
return okenvelope.NewFrom(eventID, ok, r).Write(a.Listener)
|
||||
}
|
||||
|
||||
// HandleEvent processes an incoming event request, validates its structure,
|
||||
// checks its signature, and performs necessary actions such as storing,
|
||||
// deleting, or responding to the event.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// - c: A context object used for managing deadlines, cancellation signals,
|
||||
// and other request-scoped values.
|
||||
//
|
||||
// - req: A byte slice representing the raw request containing the event
|
||||
// details to be processed.
|
||||
//
|
||||
// - srv: An interface representing the server context, providing access to
|
||||
// storage and other server-level utilities.
|
||||
//
|
||||
// Return values:
|
||||
//
|
||||
// - msg: A byte slice representing the response message generated by the
|
||||
// method. The content depends on the processing outcome.
|
||||
//
|
||||
// Expected behavior:
|
||||
//
|
||||
// The method validates the event's ID, checks its signature, and verifies its
|
||||
// type. It ensures only authorized users can delete events and manages
|
||||
// conflicts effectively. It interacts with storage for querying, updating, or
|
||||
// deleting events while responding to the client based on the evaluation
|
||||
// results. Returns immediately if any verification or operation fails.
|
||||
func (a *A) HandleEvent(
|
||||
c context.T, req []byte, srv interfaces.Server,
|
||||
c context.T, req []byte, srv server.S,
|
||||
) (msg []byte) {
|
||||
|
||||
log.T.F("handleEvent %s %s", a.RealRemote(), req)
|
||||
@@ -294,7 +322,7 @@ func (a *A) HandleEvent(
|
||||
}
|
||||
res = nil
|
||||
}
|
||||
// Send success response after processing all deletions
|
||||
// Send a success response after processing all deletions
|
||||
if err = a.sendResponse(env.Id, true); chk.E(err) {
|
||||
return
|
||||
}
|
||||
@@ -325,7 +353,6 @@ func (a *A) HandleEvent(
|
||||
ok, reason = srv.AddEvent(
|
||||
c, rl, env.E, a.Req(), a.RealRemote(), nil,
|
||||
)
|
||||
|
||||
log.I.F("event %0x added %v, %s", env.E.Id, ok, reason)
|
||||
if err = a.sendResponse(env.Id, ok, reason); chk.E(err) {
|
||||
return
|
||||
|
||||
@@ -13,6 +13,20 @@ import (
|
||||
"orly.dev/encoders/envelopes/reqenvelope"
|
||||
)
|
||||
|
||||
// HandleMessage processes an incoming message, identifies its type, and
|
||||
// delegates handling to the appropriate method based on the message's envelope
|
||||
// type.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// - msg: A byte slice representing the raw message to be processed.
|
||||
//
|
||||
// Expected behavior:
|
||||
//
|
||||
// The method identifies the message type by examining its envelope label and
|
||||
// passes the message payload to the corresponding handler function. If the type
|
||||
// is unrecognized, it logs an error and generates an appropriate notice
|
||||
// message. Handles errors in message identification or writing responses.
|
||||
func (a *A) HandleMessage(msg []byte) {
|
||||
var notice []byte
|
||||
var err error
|
||||
@@ -24,23 +38,18 @@ func (a *A) HandleMessage(msg []byte) {
|
||||
// rl := a.Relay()
|
||||
switch t {
|
||||
case eventenvelope.L:
|
||||
notice = a.HandleEvent(a.Context(), rem, a.Server)
|
||||
notice = a.HandleEvent(a.Context(), rem, a.S)
|
||||
case reqenvelope.L:
|
||||
notice = a.HandleReq(
|
||||
a.Context(), rem,
|
||||
// a.Options().SkipEventFunc,
|
||||
a.Server,
|
||||
a.S,
|
||||
)
|
||||
case closeenvelope.L:
|
||||
notice = a.HandleClose(rem, a.Server)
|
||||
notice = a.HandleClose(rem, a.S)
|
||||
case authenvelope.L:
|
||||
notice = a.HandleAuth(rem, a.Server)
|
||||
notice = a.HandleAuth(rem, a.S)
|
||||
default:
|
||||
// if wsh, ok := rl.(relay.WebSocketHandler); ok {
|
||||
// wsh.HandleUnknownType(a.Listener, t, rem)
|
||||
// } else {
|
||||
notice = []byte(fmt.Sprintf("unknown envelope type %s\n%s", t, rem))
|
||||
// }
|
||||
}
|
||||
if len(notice) > 0 {
|
||||
log.D.F("notice->%s %s", a.RealRemote(), notice)
|
||||
|
||||
@@ -2,12 +2,12 @@ package socketapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"orly.dev/app/realy/interfaces"
|
||||
"orly.dev/app/realy/pointers"
|
||||
"orly.dev/encoders/envelopes/closedenvelope"
|
||||
"orly.dev/interfaces/server"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/log"
|
||||
"orly.dev/utils/normalize"
|
||||
"orly.dev/utils/pointers"
|
||||
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
|
||||
@@ -18,8 +18,34 @@ import (
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
// HandleReq processes a raw request, parses its envelope, validates filters,
|
||||
// and interacts with the server storage and subscription mechanisms to query
|
||||
// events or manage subscriptions.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// - c: A context object used for managing deadlines, cancellation signals,
|
||||
// and other request-scoped values.
|
||||
//
|
||||
// - req: A byte slice representing the raw request data to be processed.
|
||||
//
|
||||
// - srv: An interface representing the server, providing access to storage
|
||||
// and subscription management.
|
||||
//
|
||||
// Return values:
|
||||
//
|
||||
// - r: A byte slice containing the response or error message generated
|
||||
// during processing.
|
||||
//
|
||||
// Expected behavior:
|
||||
//
|
||||
// The method parses and validates the incoming request envelope, querying
|
||||
// events from the server storage based on filters provided. It sends results
|
||||
// through the associated subscription or writes error messages to the listener.
|
||||
// If the subscription should be canceled due to completed query results, it
|
||||
// generates and sends a closure envelope.
|
||||
func (a *A) HandleReq(
|
||||
c context.T, req []byte, srv interfaces.Server,
|
||||
c context.T, req []byte, srv server.S,
|
||||
) (r []byte) {
|
||||
log.I.F("REQ:\n%s", req)
|
||||
sto := srv.Storage()
|
||||
@@ -67,13 +93,14 @@ func (a *A) HandleReq(
|
||||
}
|
||||
receiver := make(event.C, 32)
|
||||
cancel := true
|
||||
// if the query was for just Ids we know there cannot be any more results, so cancel the subscription.
|
||||
// if the query was for just Ids, we know there cannot be any more results,
|
||||
// so cancel the subscription.
|
||||
for _, f := range allowed.F {
|
||||
if f.Ids.Len() < 1 {
|
||||
cancel = false
|
||||
break
|
||||
}
|
||||
// also, if we received the limit amount of events, subscription ded
|
||||
// also, if we received the limit number of events, subscription ded
|
||||
if pointers.Present(f.Limit) {
|
||||
if len(events) < int(*f.Limit) {
|
||||
cancel = false
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package socketapi
|
||||
|
||||
import (
|
||||
"orly.dev/app/realy/interfaces"
|
||||
"orly.dev/interfaces/server"
|
||||
"orly.dev/utils/log"
|
||||
"time"
|
||||
|
||||
@@ -10,8 +10,31 @@ import (
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
// Pinger sends periodic WebSocket ping messages to ensure the connection is
|
||||
// alive and responsive. It terminates the connection if pings fail or the
|
||||
// context is canceled.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// - ctx: A context object used to monitor cancellation signals and
|
||||
// manage termination of the method execution.
|
||||
//
|
||||
// - ticker: A time.Ticker object that triggers periodic pings based on
|
||||
// its configured interval.
|
||||
//
|
||||
// - cancel: A context.CancelFunc called to gracefully terminate operations
|
||||
// associated with the WebSocket connection.
|
||||
//
|
||||
// - s: An interface representing the server context, allowing interactions
|
||||
// related to the connection.
|
||||
//
|
||||
// Expected behavior:
|
||||
//
|
||||
// The method writes ping messages to the WebSocket connection at intervals
|
||||
// dictated by the ticker. If the ping write fails or the context is canceled,
|
||||
// it stops the ticker, invokes the cancel function, and closes the connection.
|
||||
func (a *A) Pinger(
|
||||
ctx context.T, ticker *time.Ticker, cancel context.F, s interfaces.Server,
|
||||
ctx context.T, ticker *time.Ticker, cancel context.F, s server.S,
|
||||
) {
|
||||
defer func() {
|
||||
cancel()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package socketapi
|
||||
|
||||
import (
|
||||
"orly.dev/app/realy/publish/publisher"
|
||||
"orly.dev/interfaces/publisher"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/log"
|
||||
"regexp"
|
||||
@@ -19,15 +19,16 @@ var (
|
||||
NIP20prefixmatcher = regexp.MustCompile(`^\w+: `)
|
||||
)
|
||||
|
||||
// Map is a map of filters associated with a collection of ws.Listener connections.
|
||||
// Map is a map of filters associated with a collection of ws.Listener
|
||||
// connections.
|
||||
type Map map[*ws.Listener]map[string]*filters.T
|
||||
|
||||
type W struct {
|
||||
*ws.Listener
|
||||
// If Cancel is true, this is a close command.
|
||||
Cancel bool
|
||||
// Id is the subscription Id. If Cancel is true, cancel the named subscription, otherwise,
|
||||
// cancel the publisher for the socket.
|
||||
// Id is the subscription Id. If Cancel is true, cancel the named
|
||||
// subscription, otherwise, cancel the publisher for the socket.
|
||||
Id string
|
||||
Receiver event.C
|
||||
Filters *filters.T
|
||||
@@ -72,13 +73,11 @@ func (p *S) Receive(msg publisher.Message) {
|
||||
if subs, ok := p.Map[m.Listener]; !ok {
|
||||
subs = make(map[string]*filters.T)
|
||||
subs[m.Id] = m.Filters
|
||||
// log.I.S(p.Map)
|
||||
p.Map[m.Listener] = subs
|
||||
log.T.F(
|
||||
"created new subscription for %s, %s", m.Listener.RealRemote(),
|
||||
m.Filters.Marshal(nil),
|
||||
)
|
||||
// log.I.S(m.Listener, p.Map)
|
||||
} else {
|
||||
subs[m.Id] = m.Filters
|
||||
log.T.F(
|
||||
@@ -99,27 +98,9 @@ func (p *S) Deliver(ev *event.E) {
|
||||
log.T.F(
|
||||
"subscriber %s\n%s", w.RealRemote(), subscriber.Marshal(nil),
|
||||
)
|
||||
// if !publicReadable {
|
||||
// if authRequired && !w.IsAuthed() {
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
if !subscriber.Match(ev) {
|
||||
continue
|
||||
}
|
||||
// if ev.Kind.IsPrivileged() {
|
||||
// ab := w.AuthedBytes()
|
||||
// var containsPubkey bool
|
||||
// if ev.Tags != nil {
|
||||
// containsPubkey = ev.Tags.ContainsAny([]byte{'p'}, tag.New(ab))
|
||||
// }
|
||||
// if !bytes.Equal(ev.Pubkey, ab) || containsPubkey {
|
||||
// if ab == nil {
|
||||
// continue
|
||||
// }
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
var res *eventenvelope.Result
|
||||
if res, err = eventenvelope.NewResultWith(id, ev); chk.E(err) {
|
||||
continue
|
||||
@@ -133,7 +114,8 @@ func (p *S) Deliver(ev *event.E) {
|
||||
p.Mx.Unlock()
|
||||
}
|
||||
|
||||
// removeSubscriberId removes a specific subscription from a subscriber websocket.
|
||||
// removeSubscriberId removes a specific subscription from a subscriber
|
||||
// websocket.
|
||||
func (p *S) removeSubscriberId(ws *ws.Listener, id string) {
|
||||
p.Mx.Lock()
|
||||
var subs map[string]*filters.T
|
||||
|
||||
@@ -2,7 +2,8 @@ package socketapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/app/realy/interfaces"
|
||||
"orly.dev/app/realy/helpers"
|
||||
"orly.dev/interfaces/server"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/log"
|
||||
"strings"
|
||||
@@ -22,18 +23,38 @@ const (
|
||||
DefaultMaxMessageSize = 1 * units.Mb
|
||||
)
|
||||
|
||||
// A is a composite type that integrates a context, a websocket Listener, and a
|
||||
// server interface to manage WebSocket-based server communication. It is
|
||||
// designed to handle message processing, authentication, and event dispatching
|
||||
// in its operations.
|
||||
type A struct {
|
||||
Ctx context.T
|
||||
*ws.Listener
|
||||
interfaces.Server
|
||||
// ClientsMu *sync.Mutex
|
||||
// Clients map[*websocket.Conn]struct{}
|
||||
server.S
|
||||
}
|
||||
|
||||
func (a *A) Serve(w http.ResponseWriter, r *http.Request, s interfaces.Server) {
|
||||
|
||||
// Serve handles an incoming WebSocket request by upgrading the HTTP request,
|
||||
// managing the WebSocket connection, and delegating received messages for
|
||||
// processing.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// - w: The HTTP response writer used to manage the connection upgrade.
|
||||
//
|
||||
// - r: The HTTP request object that is being upgraded to a WebSocket
|
||||
// connection.
|
||||
//
|
||||
// - s: The server context object that manages request lifecycle and state.
|
||||
//
|
||||
// Expected behavior:
|
||||
//
|
||||
// The method upgrades the HTTP connection to a WebSocket connection, sets up
|
||||
// read and write limits, handles pings and pongs for keeping the connection
|
||||
// alive, and processes incoming messages. It ensures proper cleanup of
|
||||
// resources on connection termination or cancellation, adhering to the given
|
||||
// context's lifecycle.
|
||||
func (a *A) Serve(w http.ResponseWriter, r *http.Request, s server.S) {
|
||||
var err error
|
||||
|
||||
ticker := time.NewTicker(DefaultPingWait)
|
||||
var cancel context.F
|
||||
a.Ctx, cancel = context.Cancel(s.Context())
|
||||
@@ -43,27 +64,17 @@ func (a *A) Serve(w http.ResponseWriter, r *http.Request, s interfaces.Server) {
|
||||
log.E.F("failed to upgrade websocket: %v", err)
|
||||
return
|
||||
}
|
||||
// a.ClientsMu.Lock()
|
||||
// a.Clients[conn] = struct{}{}
|
||||
// a.ClientsMu.Unlock()
|
||||
a.Listener = GetListener(conn, r)
|
||||
|
||||
defer func() {
|
||||
cancel()
|
||||
ticker.Stop()
|
||||
// a.ClientsMu.Lock()
|
||||
// if _, ok := a.Clients[a.Listener.Conn]; ok {
|
||||
a.Publisher().Receive(
|
||||
&W{
|
||||
Cancel: true,
|
||||
Listener: a.Listener,
|
||||
},
|
||||
)
|
||||
// delete(a.Clients, a.Listener.Conn)
|
||||
chk.E(a.Listener.Conn.Close())
|
||||
// a.Publisher().removeSubscriber(a.Listener)
|
||||
// }
|
||||
// a.ClientsMu.Unlock()
|
||||
}()
|
||||
conn.SetReadLimit(DefaultMaxMessageSize)
|
||||
chk.E(conn.SetReadDeadline(time.Now().Add(DefaultPongWait)))
|
||||
@@ -73,17 +84,7 @@ func (a *A) Serve(w http.ResponseWriter, r *http.Request, s interfaces.Server) {
|
||||
return nil
|
||||
},
|
||||
)
|
||||
// if a.Server.AuthRequired() {
|
||||
// a.Listener.RequestAuth()
|
||||
// }
|
||||
// if a.Listener.AuthRequested() && len(a.Listener.Authed()) == 0 {
|
||||
// log.I.F("requesting auth from client from %s", a.Listener.RealRemote())
|
||||
// if err = authenvelope.NewChallengeWith(a.Listener.Challenge()).Write(a.Listener); chk.E(err) {
|
||||
// return
|
||||
// }
|
||||
// // return
|
||||
// }
|
||||
go a.Pinger(a.Ctx, ticker, cancel, a.Server)
|
||||
go a.Pinger(a.Ctx, ticker, cancel, a.S)
|
||||
var message []byte
|
||||
var typ int
|
||||
for {
|
||||
@@ -96,8 +97,7 @@ func (a *A) Serve(w http.ResponseWriter, r *http.Request, s interfaces.Server) {
|
||||
return
|
||||
default:
|
||||
}
|
||||
typ, message, err = conn.ReadMessage()
|
||||
if chk.E(err) {
|
||||
if typ, message, err = conn.ReadMessage(); err != nil {
|
||||
if strings.Contains(
|
||||
err.Error(), "use of closed network connection",
|
||||
) {
|
||||
@@ -112,7 +112,7 @@ func (a *A) Serve(w http.ResponseWriter, r *http.Request, s interfaces.Server) {
|
||||
) {
|
||||
log.W.F(
|
||||
"unexpected close error from %s: %v",
|
||||
a.Listener.Request.Header.Get("X-Forwarded-For"), err,
|
||||
helpers.GetRemoteFromReq(r), err,
|
||||
)
|
||||
}
|
||||
return
|
||||
|
||||
@@ -6,7 +6,12 @@ import (
|
||||
"github.com/fasthttp/websocket"
|
||||
)
|
||||
|
||||
var Upgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024,
|
||||
// Upgrader is a preconfigured instance of websocket.Upgrader used to upgrade
|
||||
// HTTP connections to WebSocket connections with specific buffer sizes and a
|
||||
// permissive origin-checking function.
|
||||
var Upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024, WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
}}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -36,21 +36,35 @@ import (
|
||||
var subscriptionIDCounter atomic.Int32
|
||||
|
||||
type Client struct {
|
||||
closeMutex sync.Mutex
|
||||
URL string
|
||||
RequestHeader http.Header // e.g. for origin header
|
||||
Connection *Connection
|
||||
Subscriptions *xsync.MapOf[string, *Subscription]
|
||||
ConnectionError error
|
||||
connectionContext context.T // will be canceled when the connection closes
|
||||
connectionContextCancel context.F
|
||||
challenge []byte // NIP-42 challenge, we only keep the last
|
||||
notices chan []byte // NIP-01 NOTICEs
|
||||
okCallbacks *xsync.MapOf[string, func(bool, string)]
|
||||
writeQueue chan writeRequest
|
||||
closeMutex sync.Mutex
|
||||
|
||||
URL string
|
||||
|
||||
RequestHeader http.Header // e.g. for origin header
|
||||
|
||||
Connection *Connection
|
||||
|
||||
Subscriptions *xsync.MapOf[string, *Subscription]
|
||||
|
||||
ConnectionError error
|
||||
|
||||
connectionContext context.T // will be canceled when the connection closes
|
||||
|
||||
connectionContextCancel context.F
|
||||
|
||||
challenge []byte // NIP-42 challenge, we only keep the last
|
||||
|
||||
notices chan []byte // NIP-01 NOTICEs
|
||||
|
||||
okCallbacks *xsync.MapOf[string, func(bool, string)]
|
||||
|
||||
writeQueue chan writeRequest
|
||||
|
||||
subscriptionChannelCloseQueue chan *Subscription
|
||||
signatureChecker func(*event.E) bool
|
||||
AssumeValid bool // this will skip verifying signatures for events received from this relay
|
||||
|
||||
signatureChecker func(*event.E) bool
|
||||
|
||||
AssumeValid bool // this will skip verifying signatures for events received from this relay
|
||||
}
|
||||
|
||||
type writeRequest struct {
|
||||
@@ -58,7 +72,8 @@ type writeRequest struct {
|
||||
answer chan error
|
||||
}
|
||||
|
||||
// NewRelay returns a new relay. The relay connection will be closed when the context is canceled.
|
||||
// NewRelay returns a new relay. The relay connection will be closed when the
|
||||
// context is canceled.
|
||||
func NewRelay(c context.T, url string, opts ...RelayOption) *Client {
|
||||
ctx, cancel := context.Cancel(c)
|
||||
r := &Client{
|
||||
@@ -81,8 +96,9 @@ func NewRelay(c context.T, url string, opts ...RelayOption) *Client {
|
||||
return r
|
||||
}
|
||||
|
||||
// RelayConnect returns a relay object connected to url. Once successfully connected, cancelling
|
||||
// ctx has no effect. To close the connection, call r.Close().
|
||||
// RelayConnect returns a relay object connected to url. Once successfully
|
||||
// connected, cancelling ctx has no effect. To close the connection, call
|
||||
// r.Close().
|
||||
func RelayConnect(ctx context.T, url string, opts ...RelayOption) (
|
||||
*Client, error,
|
||||
) {
|
||||
@@ -101,8 +117,8 @@ var (
|
||||
_ RelayOption = (WithSignatureChecker)(nil)
|
||||
)
|
||||
|
||||
// WithNoticeHandler just takes notices and is expected to do something with them. when not
|
||||
// given, defaults to logging the notices.
|
||||
// WithNoticeHandler just takes notices and is expected to do something with
|
||||
// them. when not given, defaults to logging the notices.
|
||||
type WithNoticeHandler func(notice []byte)
|
||||
|
||||
func (nh WithNoticeHandler) ApplyRelayOption(r *Client) {
|
||||
@@ -114,8 +130,8 @@ func (nh WithNoticeHandler) ApplyRelayOption(r *Client) {
|
||||
}()
|
||||
}
|
||||
|
||||
// WithSignatureChecker must be a function that checks the signature of an event and returns
|
||||
// true or false.
|
||||
// WithSignatureChecker must be a function that checks the signature of an event
|
||||
// and returns true or false.
|
||||
type WithSignatureChecker func(*event.E) bool
|
||||
|
||||
func (sc WithSignatureChecker) ApplyRelayOption(r *Client) {
|
||||
@@ -133,16 +149,18 @@ func (r *Client) Context() context.T { return r.connectionContext }
|
||||
// IsConnected returns true if the connection to this relay seems to be active.
|
||||
func (r *Client) IsConnected() bool { return r.connectionContext.Err() == nil }
|
||||
|
||||
// Connect tries to establish a websocket connection to r.URL. If the context expires before the
|
||||
// connection is complete, an error is returned. Once successfully connected, context expiration
|
||||
// has no effect: call r.Close to close the connection.
|
||||
// Connect tries to establish a websocket connection to r.URL. If the context
|
||||
// expires before the connection is complete, an error is returned. Once
|
||||
// successfully connected, context expiration has no effect: call r.Close to
|
||||
// close the connection.
|
||||
//
|
||||
// The underlying relay connection will use a background context. If you want to pass a custom
|
||||
// context to the underlying relay connection, use NewRelay() and then Client.Connect().
|
||||
// The underlying relay connection will use a background context. If you want to
|
||||
// pass a custom context to the underlying relay connection, use NewRelay() and
|
||||
// then Client.Connect().
|
||||
func (r *Client) Connect(c context.T) error { return r.ConnectWithTLS(c, nil) }
|
||||
|
||||
// ConnectWithTLS tries to establish a secured websocket connection to r.URL using customized
|
||||
// tls.Config (CA's, etc).
|
||||
// ConnectWithTLS tries to establish a secured websocket connection to r.URL
|
||||
// using customized tls.Config (CA's, etc.).
|
||||
func (r *Client) ConnectWithTLS(ctx context.T, tlsConfig *tls.Config) error {
|
||||
if r.connectionContext == nil || r.Subscriptions == nil {
|
||||
return errorf.E("relay must be initialized with a call to NewRelay()")
|
||||
@@ -265,7 +283,8 @@ func (r *Client) ConnectWithTLS(ctx context.T, tlsConfig *tls.Config) error {
|
||||
)
|
||||
continue
|
||||
} else {
|
||||
// check if the event matches the desired filter, ignore otherwise
|
||||
// check if the event matches the desired filter, ignore
|
||||
// otherwise
|
||||
if !sub.Filters.Match(env.Event) {
|
||||
log.D.F(
|
||||
"{%s} filter does not match: %v ~ %v\n", r.URL,
|
||||
@@ -273,7 +292,8 @@ func (r *Client) ConnectWithTLS(ctx context.T, tlsConfig *tls.Config) error {
|
||||
)
|
||||
continue
|
||||
}
|
||||
// check signature, ignore invalid, except from trusted (AssumeValid) relays
|
||||
// check signature, ignore invalid, except from trusted
|
||||
// (AssumeValid) relays
|
||||
if !r.AssumeValid {
|
||||
if ok = r.signatureChecker(env.Event); !ok {
|
||||
log.E.F(
|
||||
@@ -283,7 +303,8 @@ func (r *Client) ConnectWithTLS(ctx context.T, tlsConfig *tls.Config) error {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// dispatch this to the internal .events channel of the subscription
|
||||
// dispatch this to the internal .events channel of the
|
||||
// subscription
|
||||
sub.dispatchEvent(env.Event)
|
||||
}
|
||||
case eoseenvelope.L:
|
||||
@@ -340,14 +361,16 @@ func (r *Client) Write(msg []byte) <-chan error {
|
||||
return ch
|
||||
}
|
||||
|
||||
// Publish sends an "EVENT" command to the relay r as in NIP-01 and waits for an OK response.
|
||||
// Publish sends an "EVENT" command to the relay r as in NIP-01 and waits for an
|
||||
// OK response.
|
||||
func (r *Client) Publish(c context.T, ev *event.E) error {
|
||||
return r.publish(
|
||||
c, ev,
|
||||
)
|
||||
}
|
||||
|
||||
// Auth sends an "AUTH" command client->relay as in NIP-42 and waits for an OK response.
|
||||
// Auth sends an "AUTH" command client->relay as in NIP-42 and waits for an OK
|
||||
// response.
|
||||
func (r *Client) Auth(c context.T, sign signer.I) error {
|
||||
authEvent := auth.CreateUnsigned(sign.Pub(), r.challenge, r.URL)
|
||||
if err := authEvent.Sign(sign); chk.T(err) {
|
||||
@@ -367,7 +390,8 @@ func (r *Client) publish(ctx context.T, ev *event.E) (err error) {
|
||||
)
|
||||
defer cancel()
|
||||
} else {
|
||||
// otherwise make the context cancellable so we can stop everything upon receiving an "OK"
|
||||
// otherwise make the context cancellable so we can stop everything upon
|
||||
// receiving an "OK"
|
||||
ctx, cancel = context.Cancel(ctx)
|
||||
defer cancel()
|
||||
}
|
||||
@@ -402,7 +426,8 @@ func (r *Client) publish(ctx context.T, ev *event.E) (err error) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// this will be called when we get an OK or when the context has been canceled
|
||||
// this will be called when we get an OK or when the context has
|
||||
// been canceled
|
||||
if gotOk {
|
||||
return err
|
||||
}
|
||||
@@ -414,12 +439,13 @@ func (r *Client) publish(ctx context.T, ev *event.E) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe sends a "REQ" command to the relay r as in NIP-01.
|
||||
// Events are returned through the channel sub.Events.
|
||||
// The subscription is closed when context ctx is cancelled ("CLOSE" in NIP-01).
|
||||
// Subscribe sends a "REQ" command to the relay r as in NIP-01. Events are
|
||||
// returned through the channel sub.Events. The subscription is closed when
|
||||
// context ctx is cancelled ("CLOSE" in NIP-01).
|
||||
//
|
||||
// Remember to cancel subscriptions, either by calling `.Unsub()` on them or ensuring their `context.Context` will be canceled at some point.
|
||||
// Failure to do that will result in a huge number of halted goroutines being created.
|
||||
// Remember to cancel subscriptions, either by calling `.Unsub()` on them or
|
||||
// ensuring their `context.Context` will be canceled at some point. Failure to
|
||||
// do that will result in a huge number of halted goroutines being created.
|
||||
func (r *Client) Subscribe(
|
||||
c context.T, ff *filters.T,
|
||||
opts ...SubscriptionOption,
|
||||
@@ -438,8 +464,9 @@ func (r *Client) Subscribe(
|
||||
|
||||
// PrepareSubscription creates a subscription, but doesn't fire it.
|
||||
//
|
||||
// Remember to cancel subscriptions, either by calling `.Unsub()` on them or ensuring their `context.Context` will be canceled at some point.
|
||||
// Failure to do that will result in a huge number of halted goroutines being created.
|
||||
// Remember to cancel subscriptions, either by calling `.Unsub()` on them or
|
||||
// ensuring their `context.Context` will be canceled at some point. Failure to
|
||||
// do that will result in a huge number of halted goroutines being created.
|
||||
func (r *Client) PrepareSubscription(
|
||||
c context.T, ff *filters.T,
|
||||
opts ...SubscriptionOption,
|
||||
@@ -469,8 +496,8 @@ func (r *Client) PrepareSubscription(
|
||||
return sub
|
||||
}
|
||||
|
||||
// QuerySync is only used in tests. The realy query method is synchronous now anyway (it ensures
|
||||
// sort order is respected).
|
||||
// QuerySync is only used in tests. The realy query method is synchronous now
|
||||
// anyway (it ensures sort order is respected).
|
||||
func (r *Client) QuerySync(
|
||||
ctx context.T, f *filter.F,
|
||||
opts ...SubscriptionOption,
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestPublish(t *testing.T) {
|
||||
t.Fatalf("textNote.Sign: %v", err)
|
||||
}
|
||||
// fake relay server
|
||||
var mu sync.Mutex // guards published to satisfy go test -race
|
||||
var mu sync.Mutex // guards published to satisfy `go test -race`
|
||||
var published bool
|
||||
ws := newWebsocketServer(
|
||||
func(conn *websocket.Conn) {
|
||||
@@ -65,10 +65,9 @@ func TestPublish(t *testing.T) {
|
||||
if raw[1], err = env.Unmarshal(raw[1]); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// event := parseEventMessage(t, raw)
|
||||
if !bytes.Equal(env.T.Serialize(), textNote.Serialize()) {
|
||||
if !bytes.Equal(env.E.Serialize(), textNote.Serialize()) {
|
||||
t.Errorf(
|
||||
"received event:\n%s\nwant:\n%s", env.T.Serialize(),
|
||||
"received event:\n%s\nwant:\n%s", env.E.Serialize(),
|
||||
textNote.Serialize(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ func NewConnection(
|
||||
if err != nil {
|
||||
return nil, errorf.E("failed to dial: %w", err)
|
||||
}
|
||||
|
||||
enableCompression := false
|
||||
state := ws.StateClientSide
|
||||
for _, extension := range hs.Extensions {
|
||||
@@ -59,7 +58,6 @@ func NewConnection(
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// reader
|
||||
var flateReader *wsflate.Reader
|
||||
var msgStateR wsflate.MessageState
|
||||
@@ -72,7 +70,6 @@ func NewConnection(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
controlHandler := wsutil.ControlFrameHandler(conn, ws.StateClientSide)
|
||||
reader := &wsutil.Reader{
|
||||
Source: conn,
|
||||
@@ -83,7 +80,6 @@ func NewConnection(
|
||||
&msgStateR,
|
||||
},
|
||||
}
|
||||
|
||||
// writer
|
||||
var flateWriter *wsflate.Writer
|
||||
var msgStateW wsflate.MessageState
|
||||
@@ -100,10 +96,8 @@ func NewConnection(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
writer := wsutil.NewWriter(conn, state, ws.OpText)
|
||||
writer.SetExtensions(&msgStateW)
|
||||
|
||||
return &Connection{
|
||||
conn: conn,
|
||||
enableCompression: enableCompression,
|
||||
@@ -124,7 +118,6 @@ func (cn *Connection) WriteMessage(c context.T, data []byte) error {
|
||||
return errors.New("context canceled")
|
||||
default:
|
||||
}
|
||||
|
||||
if cn.msgStateW.IsCompressed() && cn.enableCompression {
|
||||
cn.flateWriter.Reset(cn.writer)
|
||||
if _, err := io.Copy(
|
||||
@@ -141,11 +134,9 @@ func (cn *Connection) WriteMessage(c context.T, data []byte) error {
|
||||
return errorf.E("failed to write message: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cn.writer.Flush(); chk.T(err) {
|
||||
return errorf.E("failed to flush writer: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -157,13 +148,11 @@ func (cn *Connection) ReadMessage(c context.T, buf io.Writer) error {
|
||||
return errors.New("context canceled")
|
||||
default:
|
||||
}
|
||||
|
||||
h, err := cn.reader.NextFrame()
|
||||
if err != nil {
|
||||
cn.conn.Close()
|
||||
return errorf.E("failed to advance frame: %w", err)
|
||||
}
|
||||
|
||||
if h.OpCode.IsControl() {
|
||||
if err := cn.controlHandler(h, cn.reader); chk.T(err) {
|
||||
return errorf.E("failed to handle control frame: %w", err)
|
||||
@@ -172,12 +161,10 @@ func (cn *Connection) ReadMessage(c context.T, buf io.Writer) error {
|
||||
h.OpCode == ws.OpText {
|
||||
break
|
||||
}
|
||||
|
||||
if err := cn.reader.Discard(); chk.T(err) {
|
||||
return errorf.E("failed to discard: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if cn.msgStateR.IsCompressed() && cn.enableCompression {
|
||||
cn.flateReader.Reset(cn.reader)
|
||||
if _, err := io.Copy(buf, cn.flateReader); chk.T(err) {
|
||||
@@ -188,7 +175,6 @@ func (cn *Connection) ReadMessage(c context.T, buf io.Writer) error {
|
||||
return errorf.E("failed to read message: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package ws provides both relay and client websocket implementations including
|
||||
// a pool for fanning out to multiple relays, and managing subscriptions.
|
||||
// Package ws provides both relay and client websocket implementations,
|
||||
// including a pool for fanning out to multiple relays and managing
|
||||
// subscriptions.
|
||||
package ws
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/fasthttp/websocket"
|
||||
|
||||
"orly.dev/app/realy/helpers"
|
||||
"orly.dev/utils/atomic"
|
||||
)
|
||||
|
||||
@@ -16,45 +17,23 @@ type Listener struct {
|
||||
mutex sync.Mutex
|
||||
Conn *websocket.Conn
|
||||
Request *http.Request
|
||||
// challenge atomic.String
|
||||
remote atomic.String
|
||||
// authed atomic.String
|
||||
// authRequested atomic.Bool
|
||||
remote atomic.String
|
||||
}
|
||||
|
||||
// NewListener creates a new Listener for listening for inbound connections for
|
||||
// a relay.
|
||||
func NewListener(
|
||||
conn *websocket.Conn,
|
||||
req *http.Request,
|
||||
challenge []byte,
|
||||
) (ws *Listener) {
|
||||
func NewListener(conn *websocket.Conn, req *http.Request) (ws *Listener) {
|
||||
ws = &Listener{Conn: conn, Request: req}
|
||||
// ws.challenge.Store(string(challenge))
|
||||
// ws.authRequested.Store(false)
|
||||
ws.setRemoteFromReq(req)
|
||||
return
|
||||
}
|
||||
|
||||
func (ws *Listener) setRemoteFromReq(r *http.Request) {
|
||||
var rr string
|
||||
// reverse proxy should populate this field so we see the remote not the
|
||||
// proxy
|
||||
rem := r.Header.Get("X-Forwarded-For")
|
||||
if rem == "" {
|
||||
rr = r.RemoteAddr
|
||||
} else {
|
||||
splitted := strings.Split(rem, " ")
|
||||
if len(splitted) == 1 {
|
||||
rr = splitted[0]
|
||||
}
|
||||
if len(splitted) == 2 {
|
||||
rr = splitted[1]
|
||||
}
|
||||
// in case upstream doesn't set this, or we are directly listening
|
||||
// instead of via reverse proxy or just if the header field is missing,
|
||||
// put the connection remote address into the websocket state data.
|
||||
}
|
||||
// Use the helper function to get the remote address
|
||||
rr := helpers.GetRemoteFromReq(r)
|
||||
|
||||
// If the helper function couldn't determine the remote address, fall back
|
||||
// to the connection's remote address
|
||||
if rr == "" {
|
||||
// if that fails, fall back to the remote (probably the proxy, unless
|
||||
// the relay is actually directly listening)
|
||||
|
||||
@@ -125,7 +125,7 @@ func (pool *Pool) EnsureRelay(url string) (*Client, error) {
|
||||
return relay, nil
|
||||
} else {
|
||||
var err error
|
||||
// we use this ctx here so when the pool dies everything dies
|
||||
// we use this ctx here, so when the pool dies everything dies
|
||||
ctx, cancel := context.Timeout(pool.Context, time.Second*15)
|
||||
defer cancel()
|
||||
|
||||
@@ -219,7 +219,7 @@ func (pool *Pool) subMany(
|
||||
case evt, more := <-sub.Events:
|
||||
if !more {
|
||||
// this means the connection was closed for weird
|
||||
// reasons, like the server shut down so we will
|
||||
// reasons, like the server shutdown, so we will
|
||||
// update the filters here to include only events
|
||||
// seem from now on and try to reconnect until we
|
||||
// succeed
|
||||
@@ -283,7 +283,7 @@ func (pool *Pool) subMany(
|
||||
// we will go back to the beginning of the loop and try to
|
||||
// connect again and again until the context is canceled
|
||||
time.Sleep(interval)
|
||||
interval = interval * 17 / 10 // the next time we try we will wait longer
|
||||
interval = interval * 17 / 10 // the next time we try, we will wait longer
|
||||
}
|
||||
}(url)
|
||||
}
|
||||
@@ -355,7 +355,7 @@ func (pool *Pool) subManyEose(
|
||||
reason,
|
||||
"auth-required:",
|
||||
) && pool.authHandler != nil && !hasAuthed {
|
||||
// client is requesting auth. if we can we will perform
|
||||
// client is requesting auth. if we can, we will perform
|
||||
// auth and try again
|
||||
err = client.Auth(ctx, pool.authHandler())
|
||||
if err == nil {
|
||||
@@ -437,7 +437,8 @@ func (pool *Pool) BatchedSubMany(
|
||||
return pool.batchedSubMany(c, dfs, pool.subMany)
|
||||
}
|
||||
|
||||
// BatchedSubManyEose is like BatchedSubMany, but ends upon receiving EOSE from all relays.
|
||||
// BatchedSubManyEose is like BatchedSubMany, but ends upon receiving EOSE from
|
||||
// all relays.
|
||||
func (pool *Pool) BatchedSubManyEose(
|
||||
c context.T, dfs []DirectedFilters,
|
||||
) chan IncomingEvent {
|
||||
|
||||
@@ -138,12 +138,12 @@ func (sub *Subscription) dispatchClosed(reason string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Unsub closes the subscription, sending "CLOSE" to realy as in NIP-01. Unsub()
|
||||
// Unsub closes the subscription, sending "CLOSE" to relay as in NIP-01. Unsub()
|
||||
// also closes the channel sub.Events and makes a new one.
|
||||
func (sub *Subscription) Unsub() {
|
||||
// cancel the context (if it's not canceled already)
|
||||
sub.cancel()
|
||||
// mark the subscription as closed and send a CLOSE to the realy (naïve
|
||||
// mark the subscription as closed and send a CLOSE to the relay (naïve
|
||||
// sync.Once implementation)
|
||||
if sub.live.CompareAndSwap(true, false) {
|
||||
sub.Close()
|
||||
@@ -171,7 +171,7 @@ func (sub *Subscription) Sub(_ context.T, ff *filters.T) {
|
||||
sub.Fire()
|
||||
}
|
||||
|
||||
// Fire sends the "REQ" command to the realy.
|
||||
// Fire sends the "REQ" command to the relay.
|
||||
func (sub *Subscription) Fire() (err error) {
|
||||
id := sub.GetID()
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ func TestNestedSubscriptions(t *testing.T) {
|
||||
for {
|
||||
select {
|
||||
case event := <-sub.Events:
|
||||
// now fetch author of this
|
||||
// now fetch the author of this
|
||||
var lim uint = 1
|
||||
sub, err := rl.Subscribe(
|
||||
context.Bg(),
|
||||
@@ -103,7 +103,8 @@ func TestNestedSubscriptions(t *testing.T) {
|
||||
for {
|
||||
select {
|
||||
case <-sub.Events:
|
||||
// do another subscription here in "sync" mode, just so we're sure things are not blocking
|
||||
// do another subscription here in "sync" mode, just so
|
||||
// we're sure things are not blocking
|
||||
rl.QuerySync(context.Bg(), &filter.F{Limit: &lim})
|
||||
|
||||
n.Add(1)
|
||||
|
||||
@@ -7,16 +7,21 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// EnsureDir checks if a file could be written to a path and creates the necessary
|
||||
// directories if they don't exist. It ensures that all parent directories in the
|
||||
// path are created with the appropriate permissions.
|
||||
// EnsureDir checks if a file could be written to a path and creates the
|
||||
// necessary directories if they don't exist. It ensures that all parent
|
||||
// directories in the path are created with the appropriate permissions.
|
||||
//
|
||||
// Parameters:
|
||||
// - fileName: The full path to the file for which directories need to be created.
|
||||
//
|
||||
// Behavior:
|
||||
// - fileName: The full path to the file for which directories need to be
|
||||
// created.
|
||||
//
|
||||
// Expected behavior:
|
||||
//
|
||||
// - Extracts the directory path from the fileName.
|
||||
//
|
||||
// - Checks if the directory exists.
|
||||
//
|
||||
// - If the directory doesn't exist, creates it and all parent directories.
|
||||
func EnsureDir(fileName string) (merr error) {
|
||||
dirName := filepath.Dir(fileName)
|
||||
@@ -33,15 +38,21 @@ func EnsureDir(fileName string) (merr error) {
|
||||
// FileExists reports whether the named file or directory exists.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// - filePath: The full path to the file or directory to check.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// - bool: true if the file or directory exists, false otherwise.
|
||||
//
|
||||
// Behavior:
|
||||
//
|
||||
// - Uses os.Stat to check if the file or directory exists.
|
||||
//
|
||||
// - Returns true if the file exists and can be accessed.
|
||||
// - Returns false if the file doesn't exist or cannot be accessed due to permissions.
|
||||
//
|
||||
// - Returns false if the file doesn't exist or cannot be accessed due to
|
||||
// permissions.
|
||||
func FileExists(filePath string) bool {
|
||||
_, e := os.Stat(filePath)
|
||||
return e == nil
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Package apputil provides some simple filesystem functions
|
||||
package apputil
|
||||
6
utils/env/config.go
vendored
6
utils/env/config.go
vendored
@@ -9,12 +9,12 @@ import (
|
||||
)
|
||||
|
||||
// Env is a key/value map used to represent environment variables. This is
|
||||
// implemented for go-simpler.org library.
|
||||
// implemented for the go-simpler.org library.
|
||||
type Env map[string]string
|
||||
|
||||
// GetEnv reads a file expected to represent a collection of KEY=value in
|
||||
// standard shell environment variable format - ie, key usually in all upper
|
||||
// case no spaces and words separated by underscore, value can have any
|
||||
// standard shell environment variable format - i.e., key usually in all upper
|
||||
// case no spaces and words separated by underscore; value can have any
|
||||
// separator, but usually comma, for an array of values.
|
||||
func GetEnv(path string) (env Env, err error) {
|
||||
var s []byte
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"orly.dev/utils/qu"
|
||||
)
|
||||
|
||||
// HandlerWithSource is an interrupt handling closure and the source location that it was sent
|
||||
// from.
|
||||
// HandlerWithSource is an interrupt handling closure and the source location
|
||||
// that it was sent from.
|
||||
type HandlerWithSource struct {
|
||||
Source string
|
||||
Fn func()
|
||||
@@ -35,20 +35,20 @@ var (
|
||||
// ShutdownRequestChan is a channel that can receive shutdown requests
|
||||
ShutdownRequestChan = qu.T()
|
||||
|
||||
// addHandlerChan is used to add an interrupt handler to the list of handlers to be invoked
|
||||
// on SIGINT (Ctrl+C) signals.
|
||||
// addHandlerChan is used to add an interrupt handler to the list of
|
||||
// handlers to be invoked on SIGINT (Ctrl+C) signals.
|
||||
addHandlerChan = make(chan HandlerWithSource)
|
||||
|
||||
// HandlersDone is closed after all interrupt handlers run the first time an interrupt is
|
||||
// signaled.
|
||||
// HandlersDone is closed after all interrupt handlers run the first time an
|
||||
// interrupt is signaled.
|
||||
HandlersDone = make(qu.C)
|
||||
|
||||
interruptCallbacks []func()
|
||||
interruptCallbackSources []string
|
||||
)
|
||||
|
||||
// Listener listens for interrupt signals, registers interrupt callbacks, and responds to custom
|
||||
// shutdown signals as required
|
||||
// Listener listens for interrupt signals, registers interrupt callbacks, and
|
||||
// responds to custom shutdown signals as required
|
||||
func Listener() {
|
||||
invokeCallbacks := func() {
|
||||
// run handlers in LIFO order.
|
||||
@@ -96,8 +96,8 @@ out:
|
||||
|
||||
// AddHandler adds a handler to call when a SIGINT (Ctrl+C) is received.
|
||||
func AddHandler(handler func()) {
|
||||
// Create the channel and start the main interrupt handler which invokes all other callbacks
|
||||
// and exits if not already done.
|
||||
// Create the channel and start the main interrupt handler which invokes all
|
||||
// other callbacks and exits if not already done.
|
||||
_, loc, line, _ := runtime.Caller(1)
|
||||
msg := fmt.Sprintf("%s:%d", loc, line)
|
||||
if ch == nil {
|
||||
@@ -130,8 +130,8 @@ func Request() {
|
||||
}
|
||||
}
|
||||
|
||||
// GoroutineDump returns a string with the current goroutine dump in order to show what's going
|
||||
// on in case of timeout.
|
||||
// GoroutineDump returns a string with the current goroutine dump in order to
|
||||
// show what's going on in case of timeout.
|
||||
func GoroutineDump() string {
|
||||
buf := make([]byte, 1<<18)
|
||||
n := runtime.Stack(buf, true)
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"github.com/kardianos/osext"
|
||||
)
|
||||
|
||||
// Restart uses syscall.Exec to restart the process. MacOS and Windows are not implemented,
|
||||
// currently.
|
||||
// Restart uses syscall.Exec to restart the process. macOS and Windows are not
|
||||
// implemented, currently.
|
||||
func Restart() {
|
||||
log.D.Ln("restarting")
|
||||
file, e := osext.Executable()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package lol (log of location) is a simple logging library that prints a high precision unix
|
||||
// timestamp and the source location of a log print to make tracing errors simpler. Includes a
|
||||
// set of logging levels and the ability to filter out higher log levels for a more quiet
|
||||
// output.
|
||||
// Package lol (log of location) is a simple logging library that prints a high
|
||||
// precision unix timestamp and the source location of a log print to make
|
||||
// tracing errors simpler. Includes a set of logging levels and the ability to
|
||||
// filter out higher log levels for a more quiet output.
|
||||
package lol
|
||||
|
||||
import (
|
||||
@@ -38,10 +38,10 @@ var LevelNames = []string{
|
||||
}
|
||||
|
||||
type (
|
||||
// LevelPrinter defines a set of terminal printing primitives that output with extra data,
|
||||
// time, log logLevelList, and code location
|
||||
// LevelPrinter defines a set of terminal printing primitives that output
|
||||
// with extra data, time, log logLevelList, and code location
|
||||
|
||||
// Ln prints lists of interfaces with spaces in between
|
||||
// Ln prints lists of server with spaces in between
|
||||
Ln func(a ...interface{})
|
||||
// F prints like fmt.Println surrounded []byte log details
|
||||
F func(format string, a ...interface{})
|
||||
@@ -203,8 +203,9 @@ func getTracer() (fn func(funcName string, variables ...any)) {
|
||||
for _, v := range variables {
|
||||
vars += spew.Sdump(v)
|
||||
}
|
||||
fmt.Fprintf(Writer, "%s %s %s\n%s",
|
||||
//TimeStamper(),
|
||||
fmt.Fprintf(
|
||||
Writer, "%s %s %s\n%s",
|
||||
// TimeStamper(),
|
||||
LevelSpecs[Trace].Colorizer(LevelSpecs[Trace].Name),
|
||||
funcName,
|
||||
loc,
|
||||
@@ -228,7 +229,8 @@ func GetPrinter(l int32, writer io.Writer, skip int) LevelPrinter {
|
||||
if Level.Load() < l {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(writer,
|
||||
fmt.Fprintf(
|
||||
writer,
|
||||
"%s%s %s %s\n",
|
||||
msgCol(TimeStamper()),
|
||||
LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
|
||||
@@ -240,7 +242,8 @@ func GetPrinter(l int32, writer io.Writer, skip int) LevelPrinter {
|
||||
if Level.Load() < l {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(writer,
|
||||
fmt.Fprintf(
|
||||
writer,
|
||||
"%s%s %s %s\n",
|
||||
msgCol(TimeStamper()),
|
||||
LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
|
||||
@@ -252,7 +255,8 @@ func GetPrinter(l int32, writer io.Writer, skip int) LevelPrinter {
|
||||
if Level.Load() < l {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(writer,
|
||||
fmt.Fprintf(
|
||||
writer,
|
||||
"%s%s %s %s\n",
|
||||
msgCol(TimeStamper()),
|
||||
LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
|
||||
@@ -264,7 +268,8 @@ func GetPrinter(l int32, writer io.Writer, skip int) LevelPrinter {
|
||||
if Level.Load() < l {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(writer,
|
||||
fmt.Fprintf(
|
||||
writer,
|
||||
"%s%s %s %s\n",
|
||||
msgCol(TimeStamper()),
|
||||
LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
|
||||
@@ -277,7 +282,8 @@ func GetPrinter(l int32, writer io.Writer, skip int) LevelPrinter {
|
||||
return e != nil
|
||||
}
|
||||
if e != nil {
|
||||
fmt.Fprintf(writer,
|
||||
fmt.Fprintf(
|
||||
writer,
|
||||
"%s%s %s %s\n",
|
||||
msgCol(TimeStamper()),
|
||||
LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
|
||||
@@ -290,7 +296,8 @@ func GetPrinter(l int32, writer io.Writer, skip int) LevelPrinter {
|
||||
},
|
||||
Err: func(format string, a ...interface{}) error {
|
||||
if Level.Load() >= l {
|
||||
fmt.Fprintf(writer,
|
||||
fmt.Fprintf(
|
||||
writer,
|
||||
"%s%s %s %s\n",
|
||||
msgCol(TimeStamper()),
|
||||
LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
|
||||
@@ -311,7 +318,11 @@ func GetNullPrinter() LevelPrinter {
|
||||
S: func(a ...interface{}) {},
|
||||
C: func(closure func() string) {},
|
||||
Chk: func(e error) bool { return e != nil },
|
||||
Err: func(format string, a ...interface{}) error { return fmt.Errorf(format, a...) },
|
||||
Err: func(
|
||||
format string, a ...interface{},
|
||||
) error {
|
||||
return fmt.Errorf(format, a...)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,7 +363,17 @@ func TimeStamper() (s string) {
|
||||
if NoTimeStamp.Load() {
|
||||
return
|
||||
}
|
||||
return time.Now().Format("2006-01-02T15:04:05Z07:00.000 ")
|
||||
ts := time.Now().Format("150405.000000")
|
||||
ds := time.Now().Format("2006-01-02")
|
||||
s += color.New(color.FgBlue).Sprint(ds[0:4])
|
||||
s += color.New(color.FgHiBlue).Sprint(ds[5:7])
|
||||
s += color.New(color.FgBlue).Sprint(ds[8:])
|
||||
s += color.New(color.FgHiBlue).Sprint(ts[0:2])
|
||||
s += color.New(color.FgBlue).Sprint(ts[2:4])
|
||||
s += color.New(color.FgHiBlue).Sprint(ts[4:6])
|
||||
s += color.New(color.FgBlue).Sprint(ts[7:])
|
||||
s += " "
|
||||
return
|
||||
}
|
||||
|
||||
// var wd, _ = os.Getwd()
|
||||
|
||||
@@ -35,12 +35,12 @@ func URL[V string | []byte](v V) (b []byte) {
|
||||
}
|
||||
u = bytes.TrimSpace(u)
|
||||
u = bytes.ToLower(u)
|
||||
// if address has a port number, we can probably assume it is insecure
|
||||
// if the address has a port number, we can probably assume it is insecure
|
||||
// websocket as most public or production relays have a domain name and a
|
||||
// well known port 80 or 443 and thus no port number.
|
||||
// well-known port 80 or 443 and thus no port number.
|
||||
//
|
||||
// if a protocol prefix is present, we assume it is already complete.
|
||||
// Converting http/s to websocket equivalent will be done later anyway.
|
||||
// Converting http/s to websocket-equivalent will be done later anyway.
|
||||
if bytes.Contains(u, []byte(":")) &&
|
||||
!(hp(u, HTTP) || hp(u, HTTPS) || hp(u, WS) || hp(u, WSS)) {
|
||||
|
||||
@@ -55,7 +55,7 @@ func URL[V string | []byte](v V) (b []byte) {
|
||||
_, err := p.Unmarshal(split[1])
|
||||
if chk.E(err) {
|
||||
log.D.F("Error normalizing URL '%s': %s", u, err)
|
||||
// again, without an error we must return nil
|
||||
// again, without an error, we must return nil
|
||||
return
|
||||
}
|
||||
if p.Uint64() > 65535 {
|
||||
@@ -74,8 +74,8 @@ func URL[V string | []byte](v V) (b []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// if prefix isn't specified as http/s or websocket, assume secure websocket
|
||||
// and add wss prefix (this is the most common).
|
||||
// if the prefix isn't specified as http/s or websocket, assume secure
|
||||
// websocket and add wss prefix (this is the most common).
|
||||
if !(hp(u, HTTP) || hp(u, HTTPS) || hp(u, WS) || hp(u, WSS)) {
|
||||
u = append(WSS, u...)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"orly.dev/utils/lol"
|
||||
)
|
||||
|
||||
// C is your basic empty struct signalling channel
|
||||
// C is your basic empty struct signal channel
|
||||
type C chan struct{}
|
||||
|
||||
var (
|
||||
@@ -42,8 +42,8 @@ func lc(cl func() string) {
|
||||
}
|
||||
}
|
||||
|
||||
// T creates an unbuffered chan struct{} for trigger and quit signalling (momentary and breaker
|
||||
// switches)
|
||||
// T creates an unbuffered chan struct{} for trigger and quit signalling
|
||||
// (momentary and breaker switches)
|
||||
func T() C {
|
||||
mx.Lock()
|
||||
defer mx.Unlock()
|
||||
@@ -56,9 +56,10 @@ func T() C {
|
||||
return o
|
||||
}
|
||||
|
||||
// Ts creates a buffered chan struct{} which is specifically intended for signalling without
|
||||
// blocking, generally one is the size of buffer to be used, though there might be conceivable
|
||||
// cases where the channel should accept more signals without blocking the caller
|
||||
// Ts creates a buffered chan struct{} which is specifically intended for
|
||||
// signalling without blocking, generally one is the size of buffer to be used,
|
||||
// though there might be conceivable cases where the channel should accept more
|
||||
// signals without blocking the caller
|
||||
func Ts(n int) C {
|
||||
mx.Lock()
|
||||
defer mx.Unlock()
|
||||
@@ -197,7 +198,8 @@ func PrintChanState() {
|
||||
mx.Unlock()
|
||||
}
|
||||
|
||||
// GetOpenUnbufferedChanCount returns the number of qu channels that are still open
|
||||
// GetOpenUnbufferedChanCount returns the number of qu channels that are still
|
||||
// open
|
||||
func GetOpenUnbufferedChanCount() (o int) {
|
||||
mx.Lock()
|
||||
var c int
|
||||
@@ -219,7 +221,8 @@ func GetOpenUnbufferedChanCount() (o int) {
|
||||
return
|
||||
}
|
||||
|
||||
// GetOpenBufferedChanCount returns the number of qu channels that are still open
|
||||
// GetOpenBufferedChanCount returns the number of qu channels that are still
|
||||
// open
|
||||
func GetOpenBufferedChanCount() (o int) {
|
||||
mx.Lock()
|
||||
var c int
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Package realy_lol is a nostr library, relay and associated tools.
|
||||
package version
|
||||
Reference in New Issue
Block a user