mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-06-11 01:10:48 +02:00
docstrings for many functions.
This commit is contained in:
parent
a82780e82e
commit
5bfaed2740
@ -12,10 +12,12 @@ import (
|
|||||||
ws "github.com/coder/websocket"
|
ws "github.com/coder/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Connection represents a websocket connection to a Nostr relay.
|
||||||
type Connection struct {
|
type Connection struct {
|
||||||
conn *ws.Conn
|
conn *ws.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewConnection creates a new websocket connection to a Nostr relay.
|
||||||
func NewConnection(ctx context.Context, url string, requestHeader http.Header, tlsConfig *tls.Config) (*Connection, error) {
|
func NewConnection(ctx context.Context, url string, requestHeader http.Header, tlsConfig *tls.Config) (*Connection, error) {
|
||||||
c, _, err := ws.Dial(ctx, url, getConnectionOptions(requestHeader, tlsConfig))
|
c, _, err := ws.Dial(ctx, url, getConnectionOptions(requestHeader, tlsConfig))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -29,6 +31,7 @@ func NewConnection(ctx context.Context, url string, requestHeader http.Header, t
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteMessage writes arbitrary bytes to the websocket connection.
|
||||||
func (c *Connection) WriteMessage(ctx context.Context, data []byte) error {
|
func (c *Connection) WriteMessage(ctx context.Context, data []byte) error {
|
||||||
if err := c.conn.Write(ctx, ws.MessageText, data); err != nil {
|
if err := c.conn.Write(ctx, ws.MessageText, data); err != nil {
|
||||||
return fmt.Errorf("failed to write message: %w", err)
|
return fmt.Errorf("failed to write message: %w", err)
|
||||||
@ -37,6 +40,7 @@ func (c *Connection) WriteMessage(ctx context.Context, data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadMessage reads arbitrary bytes from the websocket connection into the provided buffer.
|
||||||
func (c *Connection) ReadMessage(ctx context.Context, buf io.Writer) error {
|
func (c *Connection) ReadMessage(ctx context.Context, buf io.Writer) error {
|
||||||
_, reader, err := c.conn.Reader(ctx)
|
_, reader, err := c.conn.Reader(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -48,10 +52,12 @@ func (c *Connection) ReadMessage(ctx context.Context, buf io.Writer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close closes the websocket connection.
|
||||||
func (c *Connection) Close() error {
|
func (c *Connection) Close() error {
|
||||||
return c.conn.Close(ws.StatusNormalClosure, "")
|
return c.conn.Close(ws.StatusNormalClosure, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ping sends a ping message to the websocket connection.
|
||||||
func (c *Connection) Ping(ctx context.Context) error {
|
func (c *Connection) Ping(ctx context.Context) error {
|
||||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Millisecond*800, errors.New("ping took too long"))
|
ctx, cancel := context.WithTimeoutCause(ctx, time.Millisecond*800, errors.New("ping took too long"))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
13
envelopes.go
13
envelopes.go
@ -27,6 +27,7 @@ var (
|
|||||||
UnknownLabel = errors.New("unknown envelope label")
|
UnknownLabel = errors.New("unknown envelope label")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ParseMessageSIMD parses a message using the experimental simdjson-go library.
|
||||||
func ParseMessageSIMD(message []byte, reuse *simdjson.ParsedJson) (Envelope, error) {
|
func ParseMessageSIMD(message []byte, reuse *simdjson.ParsedJson) (Envelope, error) {
|
||||||
parsed, err := simdjson.Parse(message, reuse)
|
parsed, err := simdjson.Parse(message, reuse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -75,6 +76,7 @@ func ParseMessageSIMD(message []byte, reuse *simdjson.ParsedJson) (Envelope, err
|
|||||||
return v, err
|
return v, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseMessage parses a message into an Envelope.
|
||||||
func ParseMessage(message []byte) Envelope {
|
func ParseMessage(message []byte) Envelope {
|
||||||
firstComma := bytes.Index(message, []byte{','})
|
firstComma := bytes.Index(message, []byte{','})
|
||||||
if firstComma == -1 {
|
if firstComma == -1 {
|
||||||
@ -115,6 +117,7 @@ func ParseMessage(message []byte) Envelope {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Envelope is the interface for all nostr message envelopes.
|
||||||
type Envelope interface {
|
type Envelope interface {
|
||||||
Label() string
|
Label() string
|
||||||
UnmarshalJSON([]byte) error
|
UnmarshalJSON([]byte) error
|
||||||
@ -122,6 +125,7 @@ type Envelope interface {
|
|||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnvelopeSIMD extends Envelope with SIMD unmarshaling capability.
|
||||||
type EnvelopeSIMD interface {
|
type EnvelopeSIMD interface {
|
||||||
Envelope
|
Envelope
|
||||||
UnmarshalSIMD(simdjson.Iter) error
|
UnmarshalSIMD(simdjson.Iter) error
|
||||||
@ -138,6 +142,7 @@ var (
|
|||||||
_ Envelope = (*AuthEnvelope)(nil)
|
_ Envelope = (*AuthEnvelope)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// EventEnvelope represents an EVENT message.
|
||||||
type EventEnvelope struct {
|
type EventEnvelope struct {
|
||||||
SubscriptionID *string
|
SubscriptionID *string
|
||||||
Event
|
Event
|
||||||
@ -191,6 +196,7 @@ func (v EventEnvelope) MarshalJSON() ([]byte, error) {
|
|||||||
return w.BuildBytes()
|
return w.BuildBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReqEnvelope represents a REQ message.
|
||||||
type ReqEnvelope struct {
|
type ReqEnvelope struct {
|
||||||
SubscriptionID string
|
SubscriptionID string
|
||||||
Filters
|
Filters
|
||||||
@ -268,6 +274,7 @@ func (v ReqEnvelope) MarshalJSON() ([]byte, error) {
|
|||||||
return w.BuildBytes()
|
return w.BuildBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountEnvelope represents a COUNT message.
|
||||||
type CountEnvelope struct {
|
type CountEnvelope struct {
|
||||||
SubscriptionID string
|
SubscriptionID string
|
||||||
Filters
|
Filters
|
||||||
@ -392,6 +399,7 @@ func (v CountEnvelope) MarshalJSON() ([]byte, error) {
|
|||||||
return w.BuildBytes()
|
return w.BuildBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NoticeEnvelope represents a NOTICE message.
|
||||||
type NoticeEnvelope string
|
type NoticeEnvelope string
|
||||||
|
|
||||||
func (_ NoticeEnvelope) Label() string { return "NOTICE" }
|
func (_ NoticeEnvelope) Label() string { return "NOTICE" }
|
||||||
@ -426,6 +434,7 @@ func (v NoticeEnvelope) MarshalJSON() ([]byte, error) {
|
|||||||
return w.BuildBytes()
|
return w.BuildBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EOSEEnvelope represents an EOSE (End of Stored Events) message.
|
||||||
type EOSEEnvelope string
|
type EOSEEnvelope string
|
||||||
|
|
||||||
func (_ EOSEEnvelope) Label() string { return "EOSE" }
|
func (_ EOSEEnvelope) Label() string { return "EOSE" }
|
||||||
@ -460,6 +469,7 @@ func (v EOSEEnvelope) MarshalJSON() ([]byte, error) {
|
|||||||
return w.BuildBytes()
|
return w.BuildBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloseEnvelope represents a CLOSE message.
|
||||||
type CloseEnvelope string
|
type CloseEnvelope string
|
||||||
|
|
||||||
func (_ CloseEnvelope) Label() string { return "CLOSE" }
|
func (_ CloseEnvelope) Label() string { return "CLOSE" }
|
||||||
@ -496,6 +506,7 @@ func (v CloseEnvelope) MarshalJSON() ([]byte, error) {
|
|||||||
return w.BuildBytes()
|
return w.BuildBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClosedEnvelope represents a CLOSED message.
|
||||||
type ClosedEnvelope struct {
|
type ClosedEnvelope struct {
|
||||||
SubscriptionID string
|
SubscriptionID string
|
||||||
Reason string
|
Reason string
|
||||||
@ -539,6 +550,7 @@ func (v ClosedEnvelope) MarshalJSON() ([]byte, error) {
|
|||||||
return w.BuildBytes()
|
return w.BuildBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OKEnvelope represents an OK message.
|
||||||
type OKEnvelope struct {
|
type OKEnvelope struct {
|
||||||
EventID string
|
EventID string
|
||||||
OK bool
|
OK bool
|
||||||
@ -597,6 +609,7 @@ func (v OKEnvelope) MarshalJSON() ([]byte, error) {
|
|||||||
return w.BuildBytes()
|
return w.BuildBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuthEnvelope represents an AUTH message.
|
||||||
type AuthEnvelope struct {
|
type AuthEnvelope struct {
|
||||||
Challenge *string
|
Challenge *string
|
||||||
Event Event
|
Event Event
|
||||||
|
9
event.go
9
event.go
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/mailru/easyjson"
|
"github.com/mailru/easyjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Event represents a Nostr event.
|
||||||
type Event struct {
|
type Event struct {
|
||||||
ID string
|
ID string
|
||||||
PubKey string
|
PubKey string
|
||||||
@ -18,19 +19,18 @@ type Event struct {
|
|||||||
Sig string
|
Sig string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event Stringer interface, just returns the raw JSON as a string.
|
|
||||||
func (evt Event) String() string {
|
func (evt Event) String() string {
|
||||||
j, _ := easyjson.Marshal(evt)
|
j, _ := easyjson.Marshal(evt)
|
||||||
return string(j)
|
return string(j)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetID serializes and returns the event ID as a string.
|
// GetID computes the event ID abd returns it as a hex string.
|
||||||
func (evt *Event) GetID() string {
|
func (evt *Event) GetID() string {
|
||||||
h := sha256.Sum256(evt.Serialize())
|
h := sha256.Sum256(evt.Serialize())
|
||||||
return hex.EncodeToString(h[:])
|
return hex.EncodeToString(h[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckID checks if the implied ID matches the given ID
|
// CheckID checks if the implied ID matches the given ID more efficiently.
|
||||||
func (evt *Event) CheckID() bool {
|
func (evt *Event) CheckID() bool {
|
||||||
ser := evt.Serialize()
|
ser := evt.Serialize()
|
||||||
h := sha256.Sum256(ser)
|
h := sha256.Sum256(ser)
|
||||||
@ -52,8 +52,7 @@ func (evt *Event) CheckID() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize outputs a byte array that can be hashed/signed to identify/authenticate.
|
// Serialize outputs a byte array that can be hashed to produce the canonical event "id".
|
||||||
// JSON encoding as defined in RFC4627.
|
|
||||||
func (evt *Event) Serialize() []byte {
|
func (evt *Event) Serialize() []byte {
|
||||||
// the serialization process is just putting everything into a JSON array
|
// the serialization process is just putting everything into a JSON array
|
||||||
// so the order is kept. See NIP-01
|
// so the order is kept. See NIP-01
|
||||||
|
28
keyer.go
28
keyer.go
@ -1,20 +1,38 @@
|
|||||||
package nostr
|
package nostr
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Keyer is an interface for signing events and performing cryptographic operations.
|
||||||
|
// It abstracts away the details of key management, allowing for different implementations
|
||||||
|
// such as in-memory keys, hardware wallets, or remote signing services (bunker).
|
||||||
type Keyer interface {
|
type Keyer interface {
|
||||||
|
// Signer provides event signing capabilities
|
||||||
Signer
|
Signer
|
||||||
|
|
||||||
|
// Cipher provides encryption and decryption capabilities (NIP-44)
|
||||||
Cipher
|
Cipher
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Signer provides basic public key signing methods.
|
// Signer is an interface for signing events.
|
||||||
type Signer interface {
|
type Signer interface {
|
||||||
GetPublicKey(context.Context) (string, error)
|
// SignEvent signs the provided event, setting its ID, PubKey, and Sig fields.
|
||||||
SignEvent(context.Context, *Event) error
|
// The context can be used for operations that may require user interaction or
|
||||||
|
// network access, such as with remote signers.
|
||||||
|
SignEvent(ctx context.Context, evt *Event) error
|
||||||
|
|
||||||
|
// GetPublicKey returns the public key associated with this signer.
|
||||||
|
GetPublicKey(ctx context.Context) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Cipher provides NIP-44 encryption and decryption methods.
|
// Cipher is an interface for encrypting and decrypting messages with NIP-44
|
||||||
type Cipher interface {
|
type Cipher interface {
|
||||||
|
// Encrypt encrypts a plaintext message for a recipient.
|
||||||
|
// Returns the encrypted message as a base64-encoded string.
|
||||||
Encrypt(ctx context.Context, plaintext string, recipientPublicKey string) (base64ciphertext string, err error)
|
Encrypt(ctx context.Context, plaintext string, recipientPublicKey string) (base64ciphertext string, err error)
|
||||||
|
|
||||||
|
// Decrypt decrypts a base64-encoded ciphertext from a sender.
|
||||||
|
// Returns the decrypted plaintext.
|
||||||
Decrypt(ctx context.Context, base64ciphertext string, senderPublicKey string) (plaintext string, err error)
|
Decrypt(ctx context.Context, base64ciphertext string, senderPublicKey string) (plaintext string, err error)
|
||||||
}
|
}
|
||||||
|
@ -9,15 +9,20 @@ import (
|
|||||||
"github.com/nbd-wtf/go-nostr/nip46"
|
"github.com/nbd-wtf/go-nostr/nip46"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BunkerSigner is a signer that asks a bunker using NIP-46 every time it needs to do an operation.
|
// BunkerSigner is a signer that delegates operations to a remote bunker using NIP-46.
|
||||||
|
// It communicates with the bunker for all cryptographic operations rather than
|
||||||
|
// handling the private key locally.
|
||||||
type BunkerSigner struct {
|
type BunkerSigner struct {
|
||||||
bunker *nip46.BunkerClient
|
bunker *nip46.BunkerClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewBunkerSignerFromBunkerClient creates a new BunkerSigner from an existing BunkerClient.
|
||||||
func NewBunkerSignerFromBunkerClient(bc *nip46.BunkerClient) BunkerSigner {
|
func NewBunkerSignerFromBunkerClient(bc *nip46.BunkerClient) BunkerSigner {
|
||||||
return BunkerSigner{bc}
|
return BunkerSigner{bc}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPublicKey retrieves the public key from the remote bunker.
|
||||||
|
// It uses a timeout to prevent hanging indefinitely.
|
||||||
func (bs BunkerSigner) GetPublicKey(ctx context.Context) (string, error) {
|
func (bs BunkerSigner) GetPublicKey(ctx context.Context) (string, error) {
|
||||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*30, errors.New("get_public_key took too long"))
|
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*30, errors.New("get_public_key took too long"))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@ -28,16 +33,20 @@ func (bs BunkerSigner) GetPublicKey(ctx context.Context) (string, error) {
|
|||||||
return pk, nil
|
return pk, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignEvent sends the event to the remote bunker for signing.
|
||||||
|
// It uses a timeout to prevent hanging indefinitely.
|
||||||
func (bs BunkerSigner) SignEvent(ctx context.Context, evt *nostr.Event) error {
|
func (bs BunkerSigner) SignEvent(ctx context.Context, evt *nostr.Event) error {
|
||||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*30, errors.New("sign_event took too long"))
|
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*30, errors.New("sign_event took too long"))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return bs.bunker.SignEvent(ctx, evt)
|
return bs.bunker.SignEvent(ctx, evt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts a plaintext message for a recipient using the remote bunker.
|
||||||
func (bs BunkerSigner) Encrypt(ctx context.Context, plaintext string, recipient string) (string, error) {
|
func (bs BunkerSigner) Encrypt(ctx context.Context, plaintext string, recipient string) (string, error) {
|
||||||
return bs.bunker.NIP44Encrypt(ctx, recipient, plaintext)
|
return bs.bunker.NIP44Encrypt(ctx, recipient, plaintext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts a base64-encoded ciphertext from a sender using the remote bunker.
|
||||||
func (bs BunkerSigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (plaintext string, err error) {
|
func (bs BunkerSigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (plaintext string, err error) {
|
||||||
return bs.bunker.NIP44Encrypt(ctx, sender, base64ciphertext)
|
return bs.bunker.NIP44Encrypt(ctx, sender, base64ciphertext)
|
||||||
}
|
}
|
||||||
|
@ -9,13 +9,18 @@ import (
|
|||||||
"github.com/nbd-wtf/go-nostr/nip49"
|
"github.com/nbd-wtf/go-nostr/nip49"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EncryptedKeySigner is a signer that must always ask the user for a password before every operation.
|
// EncryptedKeySigner is a signer that must ask the user for a password before every operation.
|
||||||
|
// It stores the private key in encrypted form (NIP-49) and uses a callback to request the password
|
||||||
|
// when needed for operations.
|
||||||
type EncryptedKeySigner struct {
|
type EncryptedKeySigner struct {
|
||||||
ncryptsec string
|
ncryptsec string
|
||||||
pk string
|
pk string
|
||||||
callback func(context.Context) string
|
callback func(context.Context) string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPublicKey returns the public key associated with this signer.
|
||||||
|
// If the public key is not cached, it will decrypt the private key using the password
|
||||||
|
// callback to derive the public key.
|
||||||
func (es *EncryptedKeySigner) GetPublicKey(ctx context.Context) (string, error) {
|
func (es *EncryptedKeySigner) GetPublicKey(ctx context.Context) (string, error) {
|
||||||
if es.pk != "" {
|
if es.pk != "" {
|
||||||
return es.pk, nil
|
return es.pk, nil
|
||||||
@ -33,6 +38,8 @@ func (es *EncryptedKeySigner) GetPublicKey(ctx context.Context) (string, error)
|
|||||||
return pk, nil
|
return pk, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignEvent signs the provided event by first decrypting the private key
|
||||||
|
// using the password callback, then signing the event with the decrypted key.
|
||||||
func (es *EncryptedKeySigner) SignEvent(ctx context.Context, evt *nostr.Event) error {
|
func (es *EncryptedKeySigner) SignEvent(ctx context.Context, evt *nostr.Event) error {
|
||||||
password := es.callback(ctx)
|
password := es.callback(ctx)
|
||||||
sk, err := nip49.Decrypt(es.ncryptsec, password)
|
sk, err := nip49.Decrypt(es.ncryptsec, password)
|
||||||
@ -43,6 +50,8 @@ func (es *EncryptedKeySigner) SignEvent(ctx context.Context, evt *nostr.Event) e
|
|||||||
return evt.Sign(sk)
|
return evt.Sign(sk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts a plaintext message for a recipient using NIP-44.
|
||||||
|
// It first decrypts the private key using the password callback.
|
||||||
func (es EncryptedKeySigner) Encrypt(ctx context.Context, plaintext string, recipient string) (c64 string, err error) {
|
func (es EncryptedKeySigner) Encrypt(ctx context.Context, plaintext string, recipient string) (c64 string, err error) {
|
||||||
password := es.callback(ctx)
|
password := es.callback(ctx)
|
||||||
sk, err := nip49.Decrypt(es.ncryptsec, password)
|
sk, err := nip49.Decrypt(es.ncryptsec, password)
|
||||||
@ -56,6 +65,8 @@ func (es EncryptedKeySigner) Encrypt(ctx context.Context, plaintext string, reci
|
|||||||
return nip44.Encrypt(plaintext, ck)
|
return nip44.Encrypt(plaintext, ck)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts a base64-encoded ciphertext from a sender using NIP-44.
|
||||||
|
// It first decrypts the private key using the password callback.
|
||||||
func (es EncryptedKeySigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (plaintext string, err error) {
|
func (es EncryptedKeySigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (plaintext string, err error) {
|
||||||
password := es.callback(ctx)
|
password := es.callback(ctx)
|
||||||
sk, err := nip49.Decrypt(es.ncryptsec, password)
|
sk, err := nip49.Decrypt(es.ncryptsec, password)
|
||||||
|
22
keyer/lib.go
22
keyer/lib.go
@ -22,19 +22,37 @@ var (
|
|||||||
_ nostr.Keyer = (*ManualSigner)(nil)
|
_ nostr.Keyer = (*ManualSigner)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SignerOptions contains configuration options for creating a new signer.
|
||||||
type SignerOptions struct {
|
type SignerOptions struct {
|
||||||
|
// BunkerClientSecretKey is the secret key used for the bunker client
|
||||||
BunkerClientSecretKey string
|
BunkerClientSecretKey string
|
||||||
|
|
||||||
|
// BunkerSignTimeout is the timeout duration for bunker signing operations
|
||||||
BunkerSignTimeout time.Duration
|
BunkerSignTimeout time.Duration
|
||||||
|
|
||||||
|
// BunkerAuthHandler is called when authentication is needed for bunker operations
|
||||||
BunkerAuthHandler func(string)
|
BunkerAuthHandler func(string)
|
||||||
|
|
||||||
// if a PasswordHandler is provided the key will be stored encrypted and this function will be called
|
// PasswordHandler is called when an operation needs access to the encrypted key.
|
||||||
|
// If provided, the key will be stored encrypted and this function will be called
|
||||||
// every time an operation needs access to the key so the user can be prompted.
|
// every time an operation needs access to the key so the user can be prompted.
|
||||||
PasswordHandler func(context.Context) string
|
PasswordHandler func(context.Context) string
|
||||||
|
|
||||||
// if instead a Password is provided along with a ncryptsec, then the key will be decrypted and stored in plaintext.
|
// Password is used along with ncryptsec to decrypt the key.
|
||||||
|
// If provided, the key will be decrypted and stored in plaintext.
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new Keyer implementation based on the input string format.
|
||||||
|
// It supports various input formats:
|
||||||
|
// - ncryptsec: Creates an EncryptedKeySigner or KeySigner depending on options
|
||||||
|
// - NIP-46 bunker URL or NIP-05 identifier: Creates a BunkerSigner
|
||||||
|
// - nsec: Creates a KeySigner
|
||||||
|
// - hex private key: Creates a KeySigner
|
||||||
|
//
|
||||||
|
// The context is used for operations that may require network access.
|
||||||
|
// The pool is used for relay connections when needed.
|
||||||
|
// Options are used for additional pieces required for EncryptedKeySigner and BunkerSigner.
|
||||||
func New(ctx context.Context, pool *nostr.SimplePool, input string, opts *SignerOptions) (nostr.Keyer, error) {
|
func New(ctx context.Context, pool *nostr.SimplePool, input string, opts *SignerOptions) (nostr.Keyer, error) {
|
||||||
if opts == nil {
|
if opts == nil {
|
||||||
opts = &SignerOptions{}
|
opts = &SignerOptions{}
|
||||||
|
@ -6,28 +6,40 @@ import (
|
|||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ManualSigner is a signer that doesn't really do anything, it just calls the functions given to it.
|
// ManualSigner is a signer that delegates all operations to user-provided functions.
|
||||||
// It can be used when an app for some reason wants to ask the user to manually provide a signed event
|
// It can be used when an app wants to ask the user or some custom server to manually provide a
|
||||||
// by copy-and-paste, for example.
|
// signed event or an encrypted or decrypted payload by copy-and-paste, for example, or when the
|
||||||
|
// app wants to implement custom signing logic.
|
||||||
type ManualSigner struct {
|
type ManualSigner struct {
|
||||||
|
// ManualGetPublicKey is called when the public key is needed
|
||||||
ManualGetPublicKey func(context.Context) (string, error)
|
ManualGetPublicKey func(context.Context) (string, error)
|
||||||
|
|
||||||
|
// ManualSignEvent is called when an event needs to be signed
|
||||||
ManualSignEvent func(context.Context, *nostr.Event) error
|
ManualSignEvent func(context.Context, *nostr.Event) error
|
||||||
|
|
||||||
|
// ManualEncrypt is called when a message needs to be encrypted
|
||||||
ManualEncrypt func(ctx context.Context, plaintext string, recipientPublicKey string) (base64ciphertext string, err error)
|
ManualEncrypt func(ctx context.Context, plaintext string, recipientPublicKey string) (base64ciphertext string, err error)
|
||||||
|
|
||||||
|
// ManualDecrypt is called when a message needs to be decrypted
|
||||||
ManualDecrypt func(ctx context.Context, base64ciphertext string, senderPublicKey string) (plaintext string, err error)
|
ManualDecrypt func(ctx context.Context, base64ciphertext string, senderPublicKey string) (plaintext string, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignEvent delegates event signing to the ManualSignEvent function.
|
||||||
func (ms ManualSigner) SignEvent(ctx context.Context, evt *nostr.Event) error {
|
func (ms ManualSigner) SignEvent(ctx context.Context, evt *nostr.Event) error {
|
||||||
return ms.ManualSignEvent(ctx, evt)
|
return ms.ManualSignEvent(ctx, evt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPublicKey delegates public key retrieval to the ManualGetPublicKey function.
|
||||||
func (ms ManualSigner) GetPublicKey(ctx context.Context) (string, error) {
|
func (ms ManualSigner) GetPublicKey(ctx context.Context) (string, error) {
|
||||||
return ms.ManualGetPublicKey(ctx)
|
return ms.ManualGetPublicKey(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encrypt delegates encryption to the ManualEncrypt function.
|
||||||
func (ms ManualSigner) Encrypt(ctx context.Context, plaintext string, recipient string) (c64 string, err error) {
|
func (ms ManualSigner) Encrypt(ctx context.Context, plaintext string, recipient string) (c64 string, err error) {
|
||||||
return ms.ManualEncrypt(ctx, plaintext, recipient)
|
return ms.ManualEncrypt(ctx, plaintext, recipient)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decrypt delegates decryption to the ManualDecrypt function.
|
||||||
func (ms ManualSigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (plaintext string, err error) {
|
func (ms ManualSigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (plaintext string, err error) {
|
||||||
return ms.ManualDecrypt(ctx, base64ciphertext, sender)
|
return ms.ManualDecrypt(ctx, base64ciphertext, sender)
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,8 @@ import (
|
|||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Keysigner is a signer that holds the private key in memory and can do all the operations instantly and easily.
|
// KeySigner is a signer that holds the private key in memory and can perform
|
||||||
|
// all operations instantly and easily.
|
||||||
type KeySigner struct {
|
type KeySigner struct {
|
||||||
sk string
|
sk string
|
||||||
pk string
|
pk string
|
||||||
@ -16,6 +17,8 @@ type KeySigner struct {
|
|||||||
conversationKeys *xsync.MapOf[string, [32]byte]
|
conversationKeys *xsync.MapOf[string, [32]byte]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewPlainKeySigner creates a new KeySigner from a private key.
|
||||||
|
// Returns an error if the private key is invalid.
|
||||||
func NewPlainKeySigner(sec string) (KeySigner, error) {
|
func NewPlainKeySigner(sec string) (KeySigner, error) {
|
||||||
pk, err := nostr.GetPublicKey(sec)
|
pk, err := nostr.GetPublicKey(sec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -24,9 +27,15 @@ func NewPlainKeySigner(sec string) (KeySigner, error) {
|
|||||||
return KeySigner{sec, pk, xsync.NewMapOf[string, [32]byte]()}, nil
|
return KeySigner{sec, pk, xsync.NewMapOf[string, [32]byte]()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignEvent signs the provided event with the signer's private key.
|
||||||
|
// It sets the event's ID, PubKey, and Sig fields.
|
||||||
func (ks KeySigner) SignEvent(ctx context.Context, evt *nostr.Event) error { return evt.Sign(ks.sk) }
|
func (ks KeySigner) SignEvent(ctx context.Context, evt *nostr.Event) error { return evt.Sign(ks.sk) }
|
||||||
|
|
||||||
|
// GetPublicKey returns the public key associated with this signer.
|
||||||
func (ks KeySigner) GetPublicKey(ctx context.Context) (string, error) { return ks.pk, nil }
|
func (ks KeySigner) GetPublicKey(ctx context.Context) (string, error) { return ks.pk, nil }
|
||||||
|
|
||||||
|
// Encrypt encrypts a plaintext message for a recipient using NIP-44.
|
||||||
|
// It caches conversation keys for efficiency in repeated operations.
|
||||||
func (ks KeySigner) Encrypt(ctx context.Context, plaintext string, recipient string) (string, error) {
|
func (ks KeySigner) Encrypt(ctx context.Context, plaintext string, recipient string) (string, error) {
|
||||||
ck, ok := ks.conversationKeys.Load(recipient)
|
ck, ok := ks.conversationKeys.Load(recipient)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -40,6 +49,8 @@ func (ks KeySigner) Encrypt(ctx context.Context, plaintext string, recipient str
|
|||||||
return nip44.Encrypt(plaintext, ck)
|
return nip44.Encrypt(plaintext, ck)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts a base64-encoded ciphertext from a sender using NIP-44.
|
||||||
|
// It caches conversation keys for efficiency in repeated operations.
|
||||||
func (ks KeySigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (string, error) {
|
func (ks KeySigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (string, error) {
|
||||||
ck, ok := ks.conversationKeys.Load(sender)
|
ck, ok := ks.conversationKeys.Load(sender)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
18
pointers.go
18
pointers.go
@ -6,9 +6,18 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Pointer is an interface for different types of Nostr pointers.
|
||||||
|
//
|
||||||
|
// In this context, a "pointer" is a reference to an event or profile potentially including
|
||||||
|
// relays and other metadata that might help find it.
|
||||||
type Pointer interface {
|
type Pointer interface {
|
||||||
|
// AsTagReference returns the pointer as a string as it would be seen in the value of a tag (i.e. the tag's second item).
|
||||||
AsTagReference() string
|
AsTagReference() string
|
||||||
|
|
||||||
|
// AsTag converts the pointer with all the information available to a tag that can be included in events.
|
||||||
AsTag() Tag
|
AsTag() Tag
|
||||||
|
|
||||||
|
// AsFilter converts the pointer to a Filter that can be used to query for it on relays.
|
||||||
AsFilter() Filter
|
AsFilter() Filter
|
||||||
MatchesEvent(Event) bool
|
MatchesEvent(Event) bool
|
||||||
}
|
}
|
||||||
@ -19,11 +28,13 @@ var (
|
|||||||
_ Pointer = (*EntityPointer)(nil)
|
_ Pointer = (*EntityPointer)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ProfilePointer represents a pointer to a Nostr profile.
|
||||||
type ProfilePointer struct {
|
type ProfilePointer struct {
|
||||||
PublicKey string `json:"pubkey"`
|
PublicKey string `json:"pubkey"`
|
||||||
Relays []string `json:"relays,omitempty"`
|
Relays []string `json:"relays,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProfilePointerFromTag creates a ProfilePointer from a "p" tag (but it doesn't have to be necessarily a "p" tag, could be something else).
|
||||||
func ProfilePointerFromTag(refTag Tag) (ProfilePointer, error) {
|
func ProfilePointerFromTag(refTag Tag) (ProfilePointer, error) {
|
||||||
pk := (refTag)[1]
|
pk := (refTag)[1]
|
||||||
if !IsValidPublicKey(pk) {
|
if !IsValidPublicKey(pk) {
|
||||||
@ -41,6 +52,7 @@ func ProfilePointerFromTag(refTag Tag) (ProfilePointer, error) {
|
|||||||
return pointer, nil
|
return pointer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MatchesEvent checks if the pointer matches an event.
|
||||||
func (ep ProfilePointer) MatchesEvent(_ Event) bool { return false }
|
func (ep ProfilePointer) MatchesEvent(_ Event) bool { return false }
|
||||||
func (ep ProfilePointer) AsTagReference() string { return ep.PublicKey }
|
func (ep ProfilePointer) AsTagReference() string { return ep.PublicKey }
|
||||||
func (ep ProfilePointer) AsFilter() Filter { return Filter{Authors: []string{ep.PublicKey}} }
|
func (ep ProfilePointer) AsFilter() Filter { return Filter{Authors: []string{ep.PublicKey}} }
|
||||||
@ -52,6 +64,7 @@ func (ep ProfilePointer) AsTag() Tag {
|
|||||||
return Tag{"p", ep.PublicKey}
|
return Tag{"p", ep.PublicKey}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EventPointer represents a pointer to a nostr event.
|
||||||
type EventPointer struct {
|
type EventPointer struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Relays []string `json:"relays,omitempty"`
|
Relays []string `json:"relays,omitempty"`
|
||||||
@ -59,6 +72,7 @@ type EventPointer struct {
|
|||||||
Kind int `json:"kind,omitempty"`
|
Kind int `json:"kind,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EventPointerFromTag creates an EventPointer from an "e" tag (but it could be other tag name, it isn't checked).
|
||||||
func EventPointerFromTag(refTag Tag) (EventPointer, error) {
|
func EventPointerFromTag(refTag Tag) (EventPointer, error) {
|
||||||
id := (refTag)[1]
|
id := (refTag)[1]
|
||||||
if !IsValid32ByteHex(id) {
|
if !IsValid32ByteHex(id) {
|
||||||
@ -83,6 +97,7 @@ func (ep EventPointer) MatchesEvent(evt Event) bool { return evt.ID == ep.ID }
|
|||||||
func (ep EventPointer) AsTagReference() string { return ep.ID }
|
func (ep EventPointer) AsTagReference() string { return ep.ID }
|
||||||
func (ep EventPointer) AsFilter() Filter { return Filter{IDs: []string{ep.ID}} }
|
func (ep EventPointer) AsFilter() Filter { return Filter{IDs: []string{ep.ID}} }
|
||||||
|
|
||||||
|
// AsTag converts the pointer to a Tag.
|
||||||
func (ep EventPointer) AsTag() Tag {
|
func (ep EventPointer) AsTag() Tag {
|
||||||
if len(ep.Relays) > 0 {
|
if len(ep.Relays) > 0 {
|
||||||
if ep.Author != "" {
|
if ep.Author != "" {
|
||||||
@ -94,6 +109,7 @@ func (ep EventPointer) AsTag() Tag {
|
|||||||
return Tag{"e", ep.ID}
|
return Tag{"e", ep.ID}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EntityPointer represents a pointer to a nostr entity (addressable event).
|
||||||
type EntityPointer struct {
|
type EntityPointer struct {
|
||||||
PublicKey string `json:"pubkey"`
|
PublicKey string `json:"pubkey"`
|
||||||
Kind int `json:"kind,omitempty"`
|
Kind int `json:"kind,omitempty"`
|
||||||
@ -101,6 +117,7 @@ type EntityPointer struct {
|
|||||||
Relays []string `json:"relays,omitempty"`
|
Relays []string `json:"relays,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EntityPointerFromTag creates an EntityPointer from an "a" tag (but it doesn't check if the tag is really "a", it could be anything).
|
||||||
func EntityPointerFromTag(refTag Tag) (EntityPointer, error) {
|
func EntityPointerFromTag(refTag Tag) (EntityPointer, error) {
|
||||||
spl := strings.SplitN(refTag[1], ":", 3)
|
spl := strings.SplitN(refTag[1], ":", 3)
|
||||||
if len(spl) != 3 {
|
if len(spl) != 3 {
|
||||||
@ -129,6 +146,7 @@ func EntityPointerFromTag(refTag Tag) (EntityPointer, error) {
|
|||||||
return pointer, nil
|
return pointer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MatchesEvent checks if the pointer matches an event.
|
||||||
func (ep EntityPointer) MatchesEvent(evt Event) bool {
|
func (ep EntityPointer) MatchesEvent(evt Event) bool {
|
||||||
return ep.PublicKey == evt.PubKey &&
|
return ep.PublicKey == evt.PubKey &&
|
||||||
ep.Kind == evt.Kind &&
|
ep.Kind == evt.Kind &&
|
||||||
|
19
pool.go
19
pool.go
@ -20,6 +20,7 @@ const (
|
|||||||
seenAlreadyDropTick = time.Minute
|
seenAlreadyDropTick = time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SimplePool manages connections to multiple relays, ensures they are reopened when necessary and not duplicated.
|
||||||
type SimplePool struct {
|
type SimplePool struct {
|
||||||
Relays *xsync.MapOf[string, *Relay]
|
Relays *xsync.MapOf[string, *Relay]
|
||||||
Context context.Context
|
Context context.Context
|
||||||
@ -37,11 +38,13 @@ type SimplePool struct {
|
|||||||
relayOptions []RelayOption
|
relayOptions []RelayOption
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DirectedFilter combines a Filter with a specific relay URL.
|
||||||
type DirectedFilter struct {
|
type DirectedFilter struct {
|
||||||
Filter
|
Filter
|
||||||
Relay string
|
Relay string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RelayEvent represents an event received from a specific relay.
|
||||||
type RelayEvent struct {
|
type RelayEvent struct {
|
||||||
*Event
|
*Event
|
||||||
Relay *Relay
|
Relay *Relay
|
||||||
@ -49,10 +52,12 @@ type RelayEvent struct {
|
|||||||
|
|
||||||
func (ie RelayEvent) String() string { return fmt.Sprintf("[%s] >> %s", ie.Relay.URL, ie.Event) }
|
func (ie RelayEvent) String() string { return fmt.Sprintf("[%s] >> %s", ie.Relay.URL, ie.Event) }
|
||||||
|
|
||||||
|
// PoolOption is an interface for options that can be applied to a SimplePool.
|
||||||
type PoolOption interface {
|
type PoolOption interface {
|
||||||
ApplyPoolOption(*SimplePool)
|
ApplyPoolOption(*SimplePool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSimplePool creates a new SimplePool with the given context and options.
|
||||||
func NewSimplePool(ctx context.Context, opts ...PoolOption) *SimplePool {
|
func NewSimplePool(ctx context.Context, opts ...PoolOption) *SimplePool {
|
||||||
ctx, cancel := context.WithCancelCause(ctx)
|
ctx, cancel := context.WithCancelCause(ctx)
|
||||||
|
|
||||||
@ -140,7 +145,7 @@ func (h WithDuplicateMiddleware) ApplyPoolOption(pool *SimplePool) {
|
|||||||
pool.duplicateMiddleware = h
|
pool.duplicateMiddleware = h
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithQueryMiddleware is a function that will be called with every combination of relay+pubkey+kind queried
|
// WithAuthorKindQueryMiddleware is a function that will be called with every combination of relay+pubkey+kind queried
|
||||||
// in a .SubMany*() call -- when applicable (i.e. when the query contains a pubkey and a kind).
|
// in a .SubMany*() call -- when applicable (i.e. when the query contains a pubkey and a kind).
|
||||||
type WithAuthorKindQueryMiddleware func(relay string, pubkey string, kind int)
|
type WithAuthorKindQueryMiddleware func(relay string, pubkey string, kind int)
|
||||||
|
|
||||||
@ -155,6 +160,8 @@ var (
|
|||||||
_ PoolOption = WithRelayOptions(WithRequestHeader(http.Header{}))
|
_ PoolOption = WithRelayOptions(WithRequestHeader(http.Header{}))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// EnsureRelay ensures that a relay connection exists and is active.
|
||||||
|
// If the relay is not connected, it attempts to connect.
|
||||||
func (pool *SimplePool) EnsureRelay(url string) (*Relay, error) {
|
func (pool *SimplePool) EnsureRelay(url string) (*Relay, error) {
|
||||||
nm := NormalizeURL(url)
|
nm := NormalizeURL(url)
|
||||||
defer namedLock(nm)()
|
defer namedLock(nm)()
|
||||||
@ -199,12 +206,14 @@ func (pool *SimplePool) EnsureRelay(url string) (*Relay, error) {
|
|||||||
return relay, nil
|
return relay, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublishResult represents the result of publishing an event to a relay.
|
||||||
type PublishResult struct {
|
type PublishResult struct {
|
||||||
Error error
|
Error error
|
||||||
RelayURL string
|
RelayURL string
|
||||||
Relay *Relay
|
Relay *Relay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublishMany publishes an event to multiple relays and returns a channel of results emitted as they're received.
|
||||||
func (pool *SimplePool) PublishMany(ctx context.Context, urls []string, evt Event) chan PublishResult {
|
func (pool *SimplePool) PublishMany(ctx context.Context, urls []string, evt Event) chan PublishResult {
|
||||||
ch := make(chan PublishResult, len(urls))
|
ch := make(chan PublishResult, len(urls))
|
||||||
|
|
||||||
@ -247,7 +256,7 @@ func (pool *SimplePool) FetchMany(
|
|||||||
return pool.SubManyEose(ctx, urls, Filters{filter}, opts...)
|
return pool.SubManyEose(ctx, urls, Filters{filter}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: use SubscribeMany instead.
|
// Deprecated: SubMany is deprecated: use SubscribeMany instead.
|
||||||
func (pool *SimplePool) SubMany(
|
func (pool *SimplePool) SubMany(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
urls []string,
|
urls []string,
|
||||||
@ -447,7 +456,7 @@ func (pool *SimplePool) subMany(
|
|||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: use FetchMany instead.
|
// Deprecated: SubManyEose is deprecated: use FetchMany instead.
|
||||||
func (pool *SimplePool) SubManyEose(
|
func (pool *SimplePool) SubManyEose(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
urls []string,
|
urls []string,
|
||||||
@ -562,7 +571,7 @@ func (pool *SimplePool) subManyEoseNonOverwriteCheckDuplicate(
|
|||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountMany aggregates count results from multiple relays using HyperLogLog
|
// CountMany aggregates count results from multiple relays using NIP-45 HyperLogLog
|
||||||
func (pool *SimplePool) CountMany(
|
func (pool *SimplePool) CountMany(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
urls []string,
|
urls []string,
|
||||||
@ -611,6 +620,7 @@ func (pool *SimplePool) QuerySingle(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BatchedSubManyEose performs batched subscriptions to multiple relays with different filters.
|
||||||
func (pool *SimplePool) BatchedSubManyEose(
|
func (pool *SimplePool) BatchedSubManyEose(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
dfs []DirectedFilter,
|
dfs []DirectedFilter,
|
||||||
@ -648,6 +658,7 @@ func (pool *SimplePool) BatchedSubManyEose(
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close closes the pool with the given reason.
|
||||||
func (pool *SimplePool) Close(reason string) {
|
func (pool *SimplePool) Close(reason string) {
|
||||||
pool.cancel(fmt.Errorf("pool closed with reason: '%s'", reason))
|
pool.cancel(fmt.Errorf("pool closed with reason: '%s'", reason))
|
||||||
}
|
}
|
||||||
|
38
relay.go
38
relay.go
@ -17,10 +17,9 @@ import (
|
|||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Status int
|
|
||||||
|
|
||||||
var subscriptionIDCounter atomic.Int64
|
var subscriptionIDCounter atomic.Int64
|
||||||
|
|
||||||
|
// Relay represents a connection to a Nostr relay.
|
||||||
type Relay struct {
|
type Relay struct {
|
||||||
closeMutex sync.Mutex
|
closeMutex sync.Mutex
|
||||||
|
|
||||||
@ -51,7 +50,7 @@ type writeRequest struct {
|
|||||||
answer chan error
|
answer chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRelay returns a new relay. The relay connection will be closed when the context is canceled.
|
// NewRelay returns a new relay. It takes a context that, when canceled, will close the relay connection.
|
||||||
func NewRelay(ctx context.Context, url string, opts ...RelayOption) *Relay {
|
func NewRelay(ctx context.Context, url string, opts ...RelayOption) *Relay {
|
||||||
ctx, cancel := context.WithCancelCause(ctx)
|
ctx, cancel := context.WithCancelCause(ctx)
|
||||||
r := &Relay{
|
r := &Relay{
|
||||||
@ -73,16 +72,18 @@ func NewRelay(ctx context.Context, url string, opts ...RelayOption) *Relay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RelayConnect returns a relay object connected to url.
|
// RelayConnect returns a relay object connected to url.
|
||||||
// Once successfully connected, cancelling ctx has no effect.
|
//
|
||||||
// To close the connection, call r.Close().
|
// The given subscription is only used during the connection phase. Once successfully connected, cancelling ctx has no effect.
|
||||||
|
//
|
||||||
|
// The ongoing relay connection uses a background context. To close the connection, call r.Close().
|
||||||
|
// If you need fine grained long-term connection contexts, use NewRelay() instead.
|
||||||
func RelayConnect(ctx context.Context, url string, opts ...RelayOption) (*Relay, error) {
|
func RelayConnect(ctx context.Context, url string, opts ...RelayOption) (*Relay, error) {
|
||||||
r := NewRelay(context.Background(), url, opts...)
|
r := NewRelay(context.Background(), url, opts...)
|
||||||
err := r.Connect(ctx)
|
err := r.Connect(ctx)
|
||||||
return r, err
|
return r, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// When instantiating relay connections, some options may be passed.
|
// RelayOption is the type of the argument passed when instantiating relay connections.
|
||||||
// RelayOption is the type of the argument passed for that.
|
|
||||||
type RelayOption interface {
|
type RelayOption interface {
|
||||||
ApplyRelayOption(*Relay)
|
ApplyRelayOption(*Relay)
|
||||||
}
|
}
|
||||||
@ -122,6 +123,7 @@ func (r *Relay) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Context retrieves the context that is associated with this relay connection.
|
// Context retrieves the context that is associated with this relay connection.
|
||||||
|
// It will be closed when the relay is disconnected.
|
||||||
func (r *Relay) Context() context.Context { return r.connectionContext }
|
func (r *Relay) Context() context.Context { return r.connectionContext }
|
||||||
|
|
||||||
// IsConnected returns true if the connection to this relay seems to be active.
|
// IsConnected returns true if the connection to this relay seems to be active.
|
||||||
@ -132,14 +134,13 @@ func (r *Relay) IsConnected() bool { return r.connectionContext.Err() == nil }
|
|||||||
// Once successfully connected, context expiration has no effect: call r.Close
|
// Once successfully connected, context expiration has no effect: call r.Close
|
||||||
// to close the connection.
|
// to close the connection.
|
||||||
//
|
//
|
||||||
// The underlying relay connection will use a background context. If you want to
|
// The given context here is only used during the connection phase. The long-living
|
||||||
// pass a custom context to the underlying relay connection, use NewRelay() and
|
// relay connection will be based on the context given to NewRelay().
|
||||||
// then Relay.Connect().
|
|
||||||
func (r *Relay) Connect(ctx context.Context) error {
|
func (r *Relay) Connect(ctx context.Context) error {
|
||||||
return r.ConnectWithTLS(ctx, nil)
|
return r.ConnectWithTLS(ctx, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectWithTLS tries to establish a secured websocket connection to r.URL using customized tls.Config (CA's, etc).
|
// ConnectWithTLS is like Connect(), but takes a special tls.Config if you need that.
|
||||||
func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error {
|
func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error {
|
||||||
if r.connectionContext == nil || r.Subscriptions == nil {
|
if r.connectionContext == nil || r.Subscriptions == nil {
|
||||||
return fmt.Errorf("relay must be initialized with a call to NewRelay()")
|
return fmt.Errorf("relay must be initialized with a call to NewRelay()")
|
||||||
@ -303,7 +304,7 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write queues a message to be sent to the relay.
|
// Write queues an arbitrary message to be sent to the relay.
|
||||||
func (r *Relay) Write(msg []byte) <-chan error {
|
func (r *Relay) Write(msg []byte) <-chan error {
|
||||||
ch := make(chan error)
|
ch := make(chan error)
|
||||||
select {
|
select {
|
||||||
@ -320,6 +321,9 @@ func (r *Relay) Publish(ctx context.Context, event Event) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
|
//
|
||||||
|
// You don't have to build the AUTH event yourself, this function takes a function to which the
|
||||||
|
// event that must be signed will be passed, so it's only necessary to sign that.
|
||||||
func (r *Relay) Auth(ctx context.Context, sign func(event *Event) error) error {
|
func (r *Relay) Auth(ctx context.Context, sign func(event *Event) error) error {
|
||||||
authEvent := Event{
|
authEvent := Event{
|
||||||
CreatedAt: Now(),
|
CreatedAt: Now(),
|
||||||
@ -337,7 +341,6 @@ func (r *Relay) Auth(ctx context.Context, sign func(event *Event) error) error {
|
|||||||
return r.publish(ctx, authEvent.ID, &AuthEnvelope{Event: authEvent})
|
return r.publish(ctx, authEvent.ID, &AuthEnvelope{Event: authEvent})
|
||||||
}
|
}
|
||||||
|
|
||||||
// publish can be used both for EVENT and for AUTH
|
|
||||||
func (r *Relay) publish(ctx context.Context, id string, env Envelope) error {
|
func (r *Relay) publish(ctx context.Context, id string, env Envelope) error {
|
||||||
var err error
|
var err error
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
@ -451,6 +454,9 @@ func (r *Relay) PrepareSubscription(ctx context.Context, filters Filters, opts .
|
|||||||
return sub
|
return sub
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QueryEvents subscribes to events matching the given filter and returns a channel of events.
|
||||||
|
//
|
||||||
|
// In most cases it's better to use SimplePool instead of this method.
|
||||||
func (r *Relay) QueryEvents(ctx context.Context, filter Filter) (chan *Event, error) {
|
func (r *Relay) QueryEvents(ctx context.Context, filter Filter) (chan *Event, error) {
|
||||||
sub, err := r.Subscribe(ctx, Filters{filter})
|
sub, err := r.Subscribe(ctx, Filters{filter})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -473,6 +479,10 @@ func (r *Relay) QueryEvents(ctx context.Context, filter Filter) (chan *Event, er
|
|||||||
return sub.Events, nil
|
return sub.Events, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QuerySync subscribes to events matching the given filter and returns a slice of events.
|
||||||
|
// This method blocks until all events are received or the context is canceled.
|
||||||
|
//
|
||||||
|
// In most cases it's better to use SimplePool instead of this method.
|
||||||
func (r *Relay) QuerySync(ctx context.Context, filter Filter) ([]*Event, error) {
|
func (r *Relay) QuerySync(ctx context.Context, filter Filter) ([]*Event, error) {
|
||||||
if _, ok := ctx.Deadline(); !ok {
|
if _, ok := ctx.Deadline(); !ok {
|
||||||
// if no timeout is set, force it to 7 seconds
|
// if no timeout is set, force it to 7 seconds
|
||||||
@ -494,6 +504,7 @@ func (r *Relay) QuerySync(ctx context.Context, filter Filter) ([]*Event, error)
|
|||||||
return events, nil
|
return events, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Count sends a "COUNT" command to the relay and returns the count of events matching the filters.
|
||||||
func (r *Relay) Count(
|
func (r *Relay) Count(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
filters Filters,
|
filters Filters,
|
||||||
@ -534,6 +545,7 @@ func (r *Relay) countInternal(ctx context.Context, filters Filters, opts ...Subs
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close closes the relay connection.
|
||||||
func (r *Relay) Close() error {
|
func (r *Relay) Close() error {
|
||||||
return r.close(errors.New("Close() called"))
|
return r.close(errors.New("Close() called"))
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,8 @@ import (
|
|||||||
|
|
||||||
const eventRelayPrefix = byte('r')
|
const eventRelayPrefix = byte('r')
|
||||||
|
|
||||||
|
// makeEventRelayKey creates a key for storing event relay information.
|
||||||
|
// It uses the first 8 bytes of the event ID to create a compact key.
|
||||||
func makeEventRelayKey(eventID []byte) []byte {
|
func makeEventRelayKey(eventID []byte) []byte {
|
||||||
// format: 'r' + first 8 bytes of event ID
|
// format: 'r' + first 8 bytes of event ID
|
||||||
key := make([]byte, 9)
|
key := make([]byte, 9)
|
||||||
@ -18,6 +20,8 @@ func makeEventRelayKey(eventID []byte) []byte {
|
|||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encodeRelayList serializes a list of relay URLs into a compact binary format.
|
||||||
|
// Each relay URL is prefixed with its length as a single byte.
|
||||||
func encodeRelayList(relays []string) []byte {
|
func encodeRelayList(relays []string) []byte {
|
||||||
totalSize := 0
|
totalSize := 0
|
||||||
for _, relay := range relays {
|
for _, relay := range relays {
|
||||||
@ -43,6 +47,8 @@ func encodeRelayList(relays []string) []byte {
|
|||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decodeRelayList deserializes a binary-encoded list of relay URLs.
|
||||||
|
// It expects each relay URL to be prefixed with its length as a single byte.
|
||||||
func decodeRelayList(data []byte) []string {
|
func decodeRelayList(data []byte) []string {
|
||||||
relays := make([]string, 0, 6)
|
relays := make([]string, 0, 6)
|
||||||
offset := 0
|
offset := 0
|
||||||
@ -67,6 +73,8 @@ func decodeRelayList(data []byte) []string {
|
|||||||
return relays
|
return relays
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trackEventRelay records that an event was seen on a particular relay.
|
||||||
|
// If onlyIfItExists is true, it will only update existing records and not create new ones.
|
||||||
func (sys *System) trackEventRelay(eventID string, relay string, onlyIfItExists bool) {
|
func (sys *System) trackEventRelay(eventID string, relay string, onlyIfItExists bool) {
|
||||||
// decode the event ID hex into bytes
|
// decode the event ID hex into bytes
|
||||||
idBytes, err := hex.DecodeString(eventID)
|
idBytes, err := hex.DecodeString(eventID)
|
||||||
@ -101,7 +109,8 @@ func (sys *System) trackEventRelay(eventID string, relay string, onlyIfItExists
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEventRelays returns all known relay URLs that have been seen to carry the given event.
|
// GetEventRelays returns all known relay URLs an event is known to be available on.
|
||||||
|
// It is based on information kept on KVStore.
|
||||||
func (sys *System) GetEventRelays(eventID string) ([]string, error) {
|
func (sys *System) GetEventRelays(eventID string) ([]string, error) {
|
||||||
// decode the event ID hex into bytes
|
// decode the event ID hex into bytes
|
||||||
idBytes, err := hex.DecodeString(eventID)
|
idBytes, err := hex.DecodeString(eventID)
|
||||||
|
@ -9,6 +9,8 @@ import (
|
|||||||
|
|
||||||
var json = jsoniter.ConfigFastest
|
var json = jsoniter.ConfigFastest
|
||||||
|
|
||||||
|
// appendUnique adds items to an array only if they don't already exist in the array.
|
||||||
|
// Returns the modified array.
|
||||||
func appendUnique[I comparable](arr []I, item ...I) []I {
|
func appendUnique[I comparable](arr []I, item ...I) []I {
|
||||||
for _, item := range item {
|
for _, item := range item {
|
||||||
if slices.Contains(arr, item) {
|
if slices.Contains(arr, item) {
|
||||||
@ -19,10 +21,19 @@ func appendUnique[I comparable](arr []I, item ...I) []I {
|
|||||||
return arr
|
return arr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// doThisNotMoreThanOnceAnHour checks if an operation with the given key
|
||||||
|
// has been performed in the last hour. If not, it returns true and records
|
||||||
|
// the operation to prevent it from running again within the hour.
|
||||||
func doThisNotMoreThanOnceAnHour(key string) (doItNow bool) {
|
func doThisNotMoreThanOnceAnHour(key string) (doItNow bool) {
|
||||||
|
_dtnmtoahLock.Lock()
|
||||||
|
defer _dtnmtoahLock.Unlock()
|
||||||
|
|
||||||
if _dtnmtoah == nil {
|
if _dtnmtoah == nil {
|
||||||
go func() {
|
// this runs only once for the lifetime of this library and
|
||||||
|
// starts a long-running process of checking for expired items
|
||||||
|
// and deleting them from this map every 10 minutes.
|
||||||
_dtnmtoah = make(map[string]time.Time)
|
_dtnmtoah = make(map[string]time.Time)
|
||||||
|
go func() {
|
||||||
for {
|
for {
|
||||||
time.Sleep(time.Minute * 10)
|
time.Sleep(time.Minute * 10)
|
||||||
_dtnmtoahLock.Lock()
|
_dtnmtoahLock.Lock()
|
||||||
@ -37,9 +48,11 @@ func doThisNotMoreThanOnceAnHour(key string) (doItNow bool) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
_dtnmtoahLock.Lock()
|
_, hasBeenPerformedInTheLastHour := _dtnmtoah[key]
|
||||||
defer _dtnmtoahLock.Unlock()
|
if hasBeenPerformedInTheLastHour {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
_, exists := _dtnmtoah[key]
|
_dtnmtoah[key] = time.Now()
|
||||||
return !exists
|
return true
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ import (
|
|||||||
"github.com/nbd-wtf/go-nostr/sdk/hints"
|
"github.com/nbd-wtf/go-nostr/sdk/hints"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ProfileMetadata represents user profile information from kind 0 events.
|
||||||
|
// It contains both the raw event and parsed metadata fields.
|
||||||
type ProfileMetadata struct {
|
type ProfileMetadata struct {
|
||||||
PubKey string `json:"-"` // must always be set otherwise things will break
|
PubKey string `json:"-"` // must always be set otherwise things will break
|
||||||
Event *nostr.Event `json:"-"` // may be empty if a profile metadata event wasn't found
|
Event *nostr.Event `json:"-"` // may be empty if a profile metadata event wasn't found
|
||||||
@ -29,21 +31,28 @@ type ProfileMetadata struct {
|
|||||||
nip05LastAttempt time.Time
|
nip05LastAttempt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Npub returns the NIP-19 npub encoding of the profile's public key.
|
||||||
func (p ProfileMetadata) Npub() string {
|
func (p ProfileMetadata) Npub() string {
|
||||||
v, _ := nip19.EncodePublicKey(p.PubKey)
|
v, _ := nip19.EncodePublicKey(p.PubKey)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NpubShort returns a shortened version of the NIP-19 npub encoding,
|
||||||
|
// showing only the first 7 and last 5 characters.
|
||||||
func (p ProfileMetadata) NpubShort() string {
|
func (p ProfileMetadata) NpubShort() string {
|
||||||
npub := p.Npub()
|
npub := p.Npub()
|
||||||
return npub[0:7] + "…" + npub[58:]
|
return npub[0:7] + "…" + npub[58:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nprofile returns the NIP-19 nprofile encoding of the profile,
|
||||||
|
// including relay hints from the user's outbox.
|
||||||
func (p ProfileMetadata) Nprofile(ctx context.Context, sys *System, nrelays int) string {
|
func (p ProfileMetadata) Nprofile(ctx context.Context, sys *System, nrelays int) string {
|
||||||
v, _ := nip19.EncodeProfile(p.PubKey, sys.FetchOutboxRelays(ctx, p.PubKey, 2))
|
v, _ := nip19.EncodeProfile(p.PubKey, sys.FetchOutboxRelays(ctx, p.PubKey, 2))
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShortName returns the best available name for display purposes.
|
||||||
|
// It tries Name, then DisplayName, and falls back to a shortened npub.
|
||||||
func (p ProfileMetadata) ShortName() string {
|
func (p ProfileMetadata) ShortName() string {
|
||||||
if p.Name != "" {
|
if p.Name != "" {
|
||||||
return p.Name
|
return p.Name
|
||||||
@ -54,6 +63,7 @@ func (p ProfileMetadata) ShortName() string {
|
|||||||
return p.NpubShort()
|
return p.NpubShort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NIP05Valid checks if the profile's NIP-05 identifier is valid.
|
||||||
func (p *ProfileMetadata) NIP05Valid(ctx context.Context) bool {
|
func (p *ProfileMetadata) NIP05Valid(ctx context.Context) bool {
|
||||||
if p.NIP05 == "" {
|
if p.NIP05 == "" {
|
||||||
return false
|
return false
|
||||||
@ -74,7 +84,8 @@ func (p *ProfileMetadata) NIP05Valid(ctx context.Context) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FetchProfileFromInput takes an nprofile, npub, nip05 or hex pubkey and returns a ProfileMetadata,
|
// FetchProfileFromInput takes an nprofile, npub, nip05 or hex pubkey and returns a ProfileMetadata,
|
||||||
// updating the hintsDB in the process with any eventual relay hints
|
// updating the hintsDB in the process with any eventual relay hints.
|
||||||
|
// Returns an error if the profile reference couldn't be decoded.
|
||||||
func (sys System) FetchProfileFromInput(ctx context.Context, nip19OrNip05Code string) (ProfileMetadata, error) {
|
func (sys System) FetchProfileFromInput(ctx context.Context, nip19OrNip05Code string) (ProfileMetadata, error) {
|
||||||
p := InputToProfile(ctx, nip19OrNip05Code)
|
p := InputToProfile(ctx, nip19OrNip05Code)
|
||||||
if p == nil {
|
if p == nil {
|
||||||
@ -93,6 +104,7 @@ func (sys System) FetchProfileFromInput(ctx context.Context, nip19OrNip05Code st
|
|||||||
|
|
||||||
// FetchProfileMetadata fetches metadata for a given user from the local cache, or from the local store,
|
// FetchProfileMetadata fetches metadata for a given user from the local cache, or from the local store,
|
||||||
// or, failing these, from the target user's defined outbox relays -- then caches the result.
|
// or, failing these, from the target user's defined outbox relays -- then caches the result.
|
||||||
|
// It always returns a ProfileMetadata, even if no metadata was found (in which case only the PubKey field is set).
|
||||||
func (sys *System) FetchProfileMetadata(ctx context.Context, pubkey string) (pm ProfileMetadata) {
|
func (sys *System) FetchProfileMetadata(ctx context.Context, pubkey string) (pm ProfileMetadata) {
|
||||||
if v, ok := sys.MetadataCache.Get(pubkey); ok {
|
if v, ok := sys.MetadataCache.Get(pubkey); ok {
|
||||||
return v
|
return v
|
||||||
@ -160,6 +172,8 @@ func (sys *System) tryFetchMetadataFromNetwork(ctx context.Context, pubkey strin
|
|||||||
return &pm
|
return &pm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseMetadata parses a kind 0 event into a ProfileMetadata struct.
|
||||||
|
// Returns an error if the event is not kind 0 or if the content is not valid JSON.
|
||||||
func ParseMetadata(event *nostr.Event) (meta ProfileMetadata, err error) {
|
func ParseMetadata(event *nostr.Event) (meta ProfileMetadata, err error) {
|
||||||
if event.Kind != 0 {
|
if event.Kind != 0 {
|
||||||
err = fmt.Errorf("event %s is kind %d, not 0", event.ID, event.Kind)
|
err = fmt.Errorf("event %s is kind %d, not 0", event.ID, event.Kind)
|
||||||
|
@ -11,12 +11,19 @@ import (
|
|||||||
"github.com/nbd-wtf/go-nostr/sdk/hints"
|
"github.com/nbd-wtf/go-nostr/sdk/hints"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FetchSpecificEventParameters contains options for fetching specific events.
|
||||||
type FetchSpecificEventParameters struct {
|
type FetchSpecificEventParameters struct {
|
||||||
|
// WithRelays indicates whether to include relay information in the response
|
||||||
|
// (this causes the request to take longer as it will wait for all relays to respond).
|
||||||
WithRelays bool
|
WithRelays bool
|
||||||
|
|
||||||
|
// SkipLocalStore indicates whether to skip checking the local store for the event
|
||||||
|
// and storing the result in the local store.
|
||||||
SkipLocalStore bool
|
SkipLocalStore bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchSpecificEventFromInput tries to get a specific event from a NIP-19 code using whatever means necessary.
|
// FetchSpecificEventFromInput tries to get a specific event from a NIP-19 code or event ID.
|
||||||
|
// It supports nevent, naddr, and note NIP-19 codes, as well as raw event IDs.
|
||||||
func (sys *System) FetchSpecificEventFromInput(
|
func (sys *System) FetchSpecificEventFromInput(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
input string,
|
input string,
|
||||||
@ -47,7 +54,9 @@ func (sys *System) FetchSpecificEventFromInput(
|
|||||||
return sys.FetchSpecificEvent(ctx, pointer, params)
|
return sys.FetchSpecificEvent(ctx, pointer, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchSpecificEvent tries to get a specific event from a NIP-19 code using whatever means necessary.
|
// FetchSpecificEvent tries to get a specific event using a Pointer (EventPointer or EntityPointer).
|
||||||
|
// It first checks the local store, then queries relays associated with the event or author,
|
||||||
|
// and finally falls back to general-purpose relays.
|
||||||
func (sys *System) FetchSpecificEvent(
|
func (sys *System) FetchSpecificEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
pointer nostr.Pointer,
|
pointer nostr.Pointer,
|
||||||
|
@ -16,6 +16,16 @@ import (
|
|||||||
kvstore_memory "github.com/nbd-wtf/go-nostr/sdk/kvstore/memory"
|
kvstore_memory "github.com/nbd-wtf/go-nostr/sdk/kvstore/memory"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// System represents the core functionality of the SDK, providing access to
|
||||||
|
// various caches, relays, and dataloaders for efficient Nostr operations.
|
||||||
|
//
|
||||||
|
// Usually an application should have a single global instance of this and use
|
||||||
|
// its internal Pool for all its operations.
|
||||||
|
//
|
||||||
|
// Store, KVStore and Hints are databases that should generally be persisted
|
||||||
|
// for any application that is intended to be executed more than once. By
|
||||||
|
// default they're set to in-memory stores, but ideally persisteable
|
||||||
|
// implementations should be given (some alternatives are provided in subpackages).
|
||||||
type System struct {
|
type System struct {
|
||||||
KVStore kvstore.KVStore
|
KVStore kvstore.KVStore
|
||||||
MetadataCache cache.Cache32[ProfileMetadata]
|
MetadataCache cache.Cache32[ProfileMetadata]
|
||||||
@ -47,22 +57,35 @@ type System struct {
|
|||||||
addressableLoaders []*dataloader.Loader[string, []*nostr.Event]
|
addressableLoaders []*dataloader.Loader[string, []*nostr.Event]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SystemModifier is a function that modifies a System instance.
|
||||||
|
// It's used with NewSystem to configure the system during creation.
|
||||||
type SystemModifier func(sys *System)
|
type SystemModifier func(sys *System)
|
||||||
|
|
||||||
|
// RelayStream provides a rotating list of relay URLs.
|
||||||
|
// It's used to distribute requests across multiple relays.
|
||||||
type RelayStream struct {
|
type RelayStream struct {
|
||||||
URLs []string
|
URLs []string
|
||||||
serial int
|
serial int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewRelayStream creates a new RelayStream with the provided URLs.
|
||||||
func NewRelayStream(urls ...string) *RelayStream {
|
func NewRelayStream(urls ...string) *RelayStream {
|
||||||
return &RelayStream{URLs: urls, serial: rand.Int()}
|
return &RelayStream{URLs: urls, serial: rand.Int()}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Next returns the next URL in the rotation.
|
||||||
func (rs *RelayStream) Next() string {
|
func (rs *RelayStream) Next() string {
|
||||||
rs.serial++
|
rs.serial++
|
||||||
return rs.URLs[rs.serial%len(rs.URLs)]
|
return rs.URLs[rs.serial%len(rs.URLs)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSystem creates a new System with default configuration,
|
||||||
|
// which can be customized using the provided modifiers.
|
||||||
|
//
|
||||||
|
// The list of provided With* modifiers isn't exhaustive and
|
||||||
|
// most internal fields of System can be modified after the System
|
||||||
|
// creation -- and in many cases one or another of these will have
|
||||||
|
// to be modified, so don't be afraid of doing that.
|
||||||
func NewSystem(mods ...SystemModifier) *System {
|
func NewSystem(mods ...SystemModifier) *System {
|
||||||
sys := &System{
|
sys := &System{
|
||||||
KVStore: kvstore_memory.NewStore(),
|
KVStore: kvstore_memory.NewStore(),
|
||||||
@ -123,84 +146,101 @@ func NewSystem(mods ...SystemModifier) *System {
|
|||||||
return sys
|
return sys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close releases resources held by the System.
|
||||||
func (sys *System) Close() {
|
func (sys *System) Close() {
|
||||||
if sys.KVStore != nil {
|
if sys.KVStore != nil {
|
||||||
sys.KVStore.Close()
|
sys.KVStore.Close()
|
||||||
}
|
}
|
||||||
|
if sys.Pool != nil {
|
||||||
|
sys.Pool.Close("sdk.System closed")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithHintsDB returns a SystemModifier that sets the HintsDB.
|
||||||
func WithHintsDB(hdb hints.HintsDB) SystemModifier {
|
func WithHintsDB(hdb hints.HintsDB) SystemModifier {
|
||||||
return func(sys *System) {
|
return func(sys *System) {
|
||||||
sys.Hints = hdb
|
sys.Hints = hdb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithRelayListRelays returns a SystemModifier that sets the RelayListRelays.
|
||||||
func WithRelayListRelays(list []string) SystemModifier {
|
func WithRelayListRelays(list []string) SystemModifier {
|
||||||
return func(sys *System) {
|
return func(sys *System) {
|
||||||
sys.RelayListRelays.URLs = list
|
sys.RelayListRelays.URLs = list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithMetadataRelays returns a SystemModifier that sets the MetadataRelays.
|
||||||
func WithMetadataRelays(list []string) SystemModifier {
|
func WithMetadataRelays(list []string) SystemModifier {
|
||||||
return func(sys *System) {
|
return func(sys *System) {
|
||||||
sys.MetadataRelays.URLs = list
|
sys.MetadataRelays.URLs = list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithFollowListRelays returns a SystemModifier that sets the FollowListRelays.
|
||||||
func WithFollowListRelays(list []string) SystemModifier {
|
func WithFollowListRelays(list []string) SystemModifier {
|
||||||
return func(sys *System) {
|
return func(sys *System) {
|
||||||
sys.FollowListRelays.URLs = list
|
sys.FollowListRelays.URLs = list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithFallbackRelays returns a SystemModifier that sets the FallbackRelays.
|
||||||
func WithFallbackRelays(list []string) SystemModifier {
|
func WithFallbackRelays(list []string) SystemModifier {
|
||||||
return func(sys *System) {
|
return func(sys *System) {
|
||||||
sys.FallbackRelays.URLs = list
|
sys.FallbackRelays.URLs = list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithJustIDRelays returns a SystemModifier that sets the JustIDRelays.
|
||||||
func WithJustIDRelays(list []string) SystemModifier {
|
func WithJustIDRelays(list []string) SystemModifier {
|
||||||
return func(sys *System) {
|
return func(sys *System) {
|
||||||
sys.JustIDRelays.URLs = list
|
sys.JustIDRelays.URLs = list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithUserSearchRelays returns a SystemModifier that sets the UserSearchRelays.
|
||||||
func WithUserSearchRelays(list []string) SystemModifier {
|
func WithUserSearchRelays(list []string) SystemModifier {
|
||||||
return func(sys *System) {
|
return func(sys *System) {
|
||||||
sys.UserSearchRelays.URLs = list
|
sys.UserSearchRelays.URLs = list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithNoteSearchRelays returns a SystemModifier that sets the NoteSearchRelays.
|
||||||
func WithNoteSearchRelays(list []string) SystemModifier {
|
func WithNoteSearchRelays(list []string) SystemModifier {
|
||||||
return func(sys *System) {
|
return func(sys *System) {
|
||||||
sys.NoteSearchRelays.URLs = list
|
sys.NoteSearchRelays.URLs = list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithStore returns a SystemModifier that sets the Store.
|
||||||
func WithStore(store eventstore.Store) SystemModifier {
|
func WithStore(store eventstore.Store) SystemModifier {
|
||||||
return func(sys *System) {
|
return func(sys *System) {
|
||||||
sys.Store = store
|
sys.Store = store
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithRelayListCache returns a SystemModifier that sets the RelayListCache.
|
||||||
func WithRelayListCache(cache cache.Cache32[GenericList[Relay]]) SystemModifier {
|
func WithRelayListCache(cache cache.Cache32[GenericList[Relay]]) SystemModifier {
|
||||||
return func(sys *System) {
|
return func(sys *System) {
|
||||||
sys.RelayListCache = cache
|
sys.RelayListCache = cache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithFollowListCache returns a SystemModifier that sets the FollowListCache.
|
||||||
func WithFollowListCache(cache cache.Cache32[GenericList[ProfileRef]]) SystemModifier {
|
func WithFollowListCache(cache cache.Cache32[GenericList[ProfileRef]]) SystemModifier {
|
||||||
return func(sys *System) {
|
return func(sys *System) {
|
||||||
sys.FollowListCache = cache
|
sys.FollowListCache = cache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithMetadataCache returns a SystemModifier that sets the MetadataCache.
|
||||||
func WithMetadataCache(cache cache.Cache32[ProfileMetadata]) SystemModifier {
|
func WithMetadataCache(cache cache.Cache32[ProfileMetadata]) SystemModifier {
|
||||||
return func(sys *System) {
|
return func(sys *System) {
|
||||||
sys.MetadataCache = cache
|
sys.MetadataCache = cache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithKVStore returns a SystemModifier that sets the KVStore.
|
||||||
func WithKVStore(store kvstore.KVStore) SystemModifier {
|
func WithKVStore(store kvstore.KVStore) SystemModifier {
|
||||||
return func(sys *System) {
|
return func(sys *System) {
|
||||||
if sys.KVStore != nil {
|
if sys.KVStore != nil {
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_dtnmtoah map[string]time.Time
|
_dtnmtoah map[string]time.Time = make(map[string]time.Time)
|
||||||
_dtnmtoahLock sync.Mutex
|
_dtnmtoahLock sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,6 +21,8 @@ func IsVirtualRelay(url string) bool {
|
|||||||
|
|
||||||
if strings.HasPrefix(url, "wss://feeds.nostr.band") ||
|
if strings.HasPrefix(url, "wss://feeds.nostr.band") ||
|
||||||
strings.HasPrefix(url, "wss://filter.nostr.wine") ||
|
strings.HasPrefix(url, "wss://filter.nostr.wine") ||
|
||||||
|
strings.HasPrefix(url, "ws://localhost") ||
|
||||||
|
strings.HasPrefix(url, "ws://127.0.0.1") ||
|
||||||
strings.HasPrefix(url, "wss://cache") {
|
strings.HasPrefix(url, "wss://cache") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -29,7 +31,7 @@ func IsVirtualRelay(url string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PerQueryLimitInBatch tries to make an educated guess for the batch size given the total filter limit and
|
// PerQueryLimitInBatch tries to make an educated guess for the batch size given the total filter limit and
|
||||||
// the number of abstract queries we'll be conducting at the same time
|
// the number of abstract queries we'll be conducting at the same time.
|
||||||
func PerQueryLimitInBatch(totalFilterLimit int, numberOfQueries int) int {
|
func PerQueryLimitInBatch(totalFilterLimit int, numberOfQueries int) int {
|
||||||
if numberOfQueries == 1 || totalFilterLimit*numberOfQueries < 50 {
|
if numberOfQueries == 1 || totalFilterLimit*numberOfQueries < 50 {
|
||||||
return totalFilterLimit
|
return totalFilterLimit
|
||||||
|
@ -11,9 +11,9 @@ import (
|
|||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CheckSignature checks if the signature is valid for the id
|
// CheckSignature checks if the event signature is valid for the given event.
|
||||||
// (which is a hash of the serialized event content).
|
// It won't look at the ID field, instead it will recompute the id from the entire event body.
|
||||||
// returns an error if the signature itself is invalid.
|
// If the signature is invalid bool will be false and err will be set.
|
||||||
func (evt Event) CheckSignature() (bool, error) {
|
func (evt Event) CheckSignature() (bool, error) {
|
||||||
// read and check pubkey
|
// read and check pubkey
|
||||||
pk, err := hex.DecodeString(evt.PubKey)
|
pk, err := hex.DecodeString(evt.PubKey)
|
||||||
@ -42,6 +42,8 @@ func (evt Event) CheckSignature() (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sign signs an event with a given privateKey.
|
// Sign signs an event with a given privateKey.
|
||||||
|
// It sets the event's ID, PubKey, and Sig fields.
|
||||||
|
// Returns an error if the private key is invalid or if signing fails.
|
||||||
func (evt *Event) Sign(secretKey string) error {
|
func (evt *Event) Sign(secretKey string) error {
|
||||||
s, err := hex.DecodeString(secretKey)
|
s, err := hex.DecodeString(secretKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -34,9 +34,6 @@ import (
|
|||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CheckSignature checks if the signature is valid for the id
|
|
||||||
// (which is a hash of the serialized event content).
|
|
||||||
// returns an error if the signature itself is invalid.
|
|
||||||
func (evt Event) CheckSignature() (bool, error) {
|
func (evt Event) CheckSignature() (bool, error) {
|
||||||
var pk [32]byte
|
var pk [32]byte
|
||||||
_, err := hex.Decode(pk[:], []byte(evt.PubKey))
|
_, err := hex.Decode(pk[:], []byte(evt.PubKey))
|
||||||
@ -61,7 +58,6 @@ func (evt Event) CheckSignature() (bool, error) {
|
|||||||
return res == 1, nil
|
return res == 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign signs an event with a given privateKey.
|
|
||||||
func (evt *Event) Sign(secretKey string, signOpts ...schnorr.SignOption) error {
|
func (evt *Event) Sign(secretKey string, signOpts ...schnorr.SignOption) error {
|
||||||
sk, err := hex.DecodeString(secretKey)
|
sk, err := hex.DecodeString(secretKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Subscription represents a subscription to a relay.
|
||||||
type Subscription struct {
|
type Subscription struct {
|
||||||
counter int64
|
counter int64
|
||||||
id string
|
id string
|
||||||
@ -46,13 +47,7 @@ type Subscription struct {
|
|||||||
storedwg sync.WaitGroup
|
storedwg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventMessage struct {
|
// SubscriptionOption is the type of the argument passed when instantiating relay connections.
|
||||||
Event Event
|
|
||||||
Relay string
|
|
||||||
}
|
|
||||||
|
|
||||||
// When instantiating relay connections, some options may be passed.
|
|
||||||
// SubscriptionOption is the type of the argument passed for that.
|
|
||||||
// Some examples are WithLabel.
|
// Some examples are WithLabel.
|
||||||
type SubscriptionOption interface {
|
type SubscriptionOption interface {
|
||||||
IsSubscriptionOption()
|
IsSubscriptionOption()
|
||||||
@ -85,6 +80,7 @@ func (sub *Subscription) start() {
|
|||||||
sub.mu.Unlock()
|
sub.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetID returns the subscription ID.
|
||||||
func (sub *Subscription) GetID() string { return sub.id }
|
func (sub *Subscription) GetID() string { return sub.id }
|
||||||
|
|
||||||
func (sub *Subscription) dispatchEvent(evt *Event) {
|
func (sub *Subscription) dispatchEvent(evt *Event) {
|
||||||
@ -121,6 +117,7 @@ func (sub *Subscription) dispatchEose() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleClosed handles the CLOSED message from a relay.
|
||||||
func (sub *Subscription) handleClosed(reason string) {
|
func (sub *Subscription) handleClosed(reason string) {
|
||||||
go func() {
|
go func() {
|
||||||
sub.ClosedReason <- reason
|
sub.ClosedReason <- reason
|
||||||
@ -135,6 +132,7 @@ func (sub *Subscription) Unsub() {
|
|||||||
sub.unsub(errors.New("Unsub() called"))
|
sub.unsub(errors.New("Unsub() called"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unsub is the internal implementation of Unsub.
|
||||||
func (sub *Subscription) unsub(err error) {
|
func (sub *Subscription) unsub(err error) {
|
||||||
// cancel the context (if it's not canceled already)
|
// cancel the context (if it's not canceled already)
|
||||||
sub.cancel(err)
|
sub.cancel(err)
|
||||||
|
6
utils.go
6
utils.go
@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// IsValidRelayURL checks if a URL is a valid relay URL (ws:// or wss://).
|
||||||
func IsValidRelayURL(u string) bool {
|
func IsValidRelayURL(u string) bool {
|
||||||
parsed, err := url.Parse(u)
|
parsed, err := url.Parse(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -18,6 +19,7 @@ func IsValidRelayURL(u string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValid32ByteHex checks if a string is a valid 32-byte hex string.
|
||||||
func IsValid32ByteHex(thing string) bool {
|
func IsValid32ByteHex(thing string) bool {
|
||||||
if !isLowerHex(thing) {
|
if !isLowerHex(thing) {
|
||||||
return false
|
return false
|
||||||
@ -29,6 +31,7 @@ func IsValid32ByteHex(thing string) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CompareEvent is meant to to be used with slices.Sort
|
||||||
func CompareEvent(a, b Event) int {
|
func CompareEvent(a, b Event) int {
|
||||||
if a.CreatedAt == b.CreatedAt {
|
if a.CreatedAt == b.CreatedAt {
|
||||||
return strings.Compare(a.ID, b.ID)
|
return strings.Compare(a.ID, b.ID)
|
||||||
@ -36,6 +39,7 @@ func CompareEvent(a, b Event) int {
|
|||||||
return cmp.Compare(a.CreatedAt, b.CreatedAt)
|
return cmp.Compare(a.CreatedAt, b.CreatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CompareEventReverse is meant to to be used with slices.Sort
|
||||||
func CompareEventReverse(b, a Event) int {
|
func CompareEventReverse(b, a Event) int {
|
||||||
if a.CreatedAt == b.CreatedAt {
|
if a.CreatedAt == b.CreatedAt {
|
||||||
return strings.Compare(a.ID, b.ID)
|
return strings.Compare(a.ID, b.ID)
|
||||||
@ -43,6 +47,7 @@ func CompareEventReverse(b, a Event) int {
|
|||||||
return cmp.Compare(a.CreatedAt, b.CreatedAt)
|
return cmp.Compare(a.CreatedAt, b.CreatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CompareEventPtr is meant to to be used with slices.Sort
|
||||||
func CompareEventPtr(a, b *Event) int {
|
func CompareEventPtr(a, b *Event) int {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
if b == nil {
|
if b == nil {
|
||||||
@ -60,6 +65,7 @@ func CompareEventPtr(a, b *Event) int {
|
|||||||
return cmp.Compare(a.CreatedAt, b.CreatedAt)
|
return cmp.Compare(a.CreatedAt, b.CreatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CompareEventPtrReverse is meant to to be used with slices.Sort
|
||||||
func CompareEventPtrReverse(b, a *Event) int {
|
func CompareEventPtrReverse(b, a *Event) int {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
if b == nil {
|
if b == nil {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user