Remove unused protocol packages and interfaces and clean up code

This commit is contained in:
2025-07-16 07:46:19 +01:00
parent dca748edee
commit 3aa56ebe66
93 changed files with 682 additions and 2662 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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{}

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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)
}
}
}

View File

@@ -1,2 +0,0 @@
// Package cmd contains the executable applications of the realy suite.
package cmd

View File

@@ -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 == "" {

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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)

View File

@@ -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"

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -1,7 +0,0 @@
package listener
type I interface {
Write(p []byte) (n int, err error)
Close() error
Remote() string
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -1,6 +0,0 @@
package nwc
type Error struct {
Code []byte
Message []byte
}

View File

@@ -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}
}

View File

@@ -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}
}

View File

@@ -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 {
}

View File

@@ -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}
}

View File

@@ -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}
}

View File

@@ -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}
}

View File

@@ -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}
}

View File

@@ -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,
}
}

View File

@@ -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"),
}

View File

@@ -1 +0,0 @@
package nwc

View File

@@ -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")
}

View File

@@ -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}}
}

View File

@@ -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,
}
}

View File

@@ -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
}

View File

@@ -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}
}

View File

@@ -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
// })
// }

View File

@@ -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
},
)
}

View File

@@ -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
},
)
}

View File

@@ -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
},
)
}

View File

@@ -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
},
)
}

View File

@@ -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
},
)
}

View File

@@ -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
},
)
}

View File

@@ -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
},
)
}

View File

@@ -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
},
)
}

View File

@@ -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
},
)
}

View File

@@ -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
},
)
}

View File

@@ -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
},
)
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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 {

View 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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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
}}
},
}

View File

@@ -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,

View File

@@ -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(),
)
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -1,2 +0,0 @@
// Package apputil provides some simple filesystem functions
package apputil

6
utils/env/config.go vendored
View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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...)
}

View File

@@ -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

View File

@@ -1,2 +0,0 @@
// Package realy_lol is a nostr library, relay and associated tools.
package version