mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-06-01 10:39:19 +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"
|
||||
)
|
||||
|
||||
// Connection represents a websocket connection to a Nostr relay.
|
||||
type Connection struct {
|
||||
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) {
|
||||
c, _, err := ws.Dial(ctx, url, getConnectionOptions(requestHeader, tlsConfig))
|
||||
if err != nil {
|
||||
@ -29,6 +31,7 @@ func NewConnection(ctx context.Context, url string, requestHeader http.Header, t
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WriteMessage writes arbitrary bytes to the websocket connection.
|
||||
func (c *Connection) WriteMessage(ctx context.Context, data []byte) error {
|
||||
if err := c.conn.Write(ctx, ws.MessageText, data); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// ReadMessage reads arbitrary bytes from the websocket connection into the provided buffer.
|
||||
func (c *Connection) ReadMessage(ctx context.Context, buf io.Writer) error {
|
||||
_, reader, err := c.conn.Reader(ctx)
|
||||
if err != nil {
|
||||
@ -48,10 +52,12 @@ func (c *Connection) ReadMessage(ctx context.Context, buf io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the websocket connection.
|
||||
func (c *Connection) Close() error {
|
||||
return c.conn.Close(ws.StatusNormalClosure, "")
|
||||
}
|
||||
|
||||
// Ping sends a ping message to the websocket connection.
|
||||
func (c *Connection) Ping(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Millisecond*800, errors.New("ping took too long"))
|
||||
defer cancel()
|
||||
|
13
envelopes.go
13
envelopes.go
@ -27,6 +27,7 @@ var (
|
||||
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) {
|
||||
parsed, err := simdjson.Parse(message, reuse)
|
||||
if err != nil {
|
||||
@ -75,6 +76,7 @@ func ParseMessageSIMD(message []byte, reuse *simdjson.ParsedJson) (Envelope, err
|
||||
return v, err
|
||||
}
|
||||
|
||||
// ParseMessage parses a message into an Envelope.
|
||||
func ParseMessage(message []byte) Envelope {
|
||||
firstComma := bytes.Index(message, []byte{','})
|
||||
if firstComma == -1 {
|
||||
@ -115,6 +117,7 @@ func ParseMessage(message []byte) Envelope {
|
||||
return v
|
||||
}
|
||||
|
||||
// Envelope is the interface for all nostr message envelopes.
|
||||
type Envelope interface {
|
||||
Label() string
|
||||
UnmarshalJSON([]byte) error
|
||||
@ -122,6 +125,7 @@ type Envelope interface {
|
||||
String() string
|
||||
}
|
||||
|
||||
// EnvelopeSIMD extends Envelope with SIMD unmarshaling capability.
|
||||
type EnvelopeSIMD interface {
|
||||
Envelope
|
||||
UnmarshalSIMD(simdjson.Iter) error
|
||||
@ -138,6 +142,7 @@ var (
|
||||
_ Envelope = (*AuthEnvelope)(nil)
|
||||
)
|
||||
|
||||
// EventEnvelope represents an EVENT message.
|
||||
type EventEnvelope struct {
|
||||
SubscriptionID *string
|
||||
Event
|
||||
@ -191,6 +196,7 @@ func (v EventEnvelope) MarshalJSON() ([]byte, error) {
|
||||
return w.BuildBytes()
|
||||
}
|
||||
|
||||
// ReqEnvelope represents a REQ message.
|
||||
type ReqEnvelope struct {
|
||||
SubscriptionID string
|
||||
Filters
|
||||
@ -268,6 +274,7 @@ func (v ReqEnvelope) MarshalJSON() ([]byte, error) {
|
||||
return w.BuildBytes()
|
||||
}
|
||||
|
||||
// CountEnvelope represents a COUNT message.
|
||||
type CountEnvelope struct {
|
||||
SubscriptionID string
|
||||
Filters
|
||||
@ -392,6 +399,7 @@ func (v CountEnvelope) MarshalJSON() ([]byte, error) {
|
||||
return w.BuildBytes()
|
||||
}
|
||||
|
||||
// NoticeEnvelope represents a NOTICE message.
|
||||
type NoticeEnvelope string
|
||||
|
||||
func (_ NoticeEnvelope) Label() string { return "NOTICE" }
|
||||
@ -426,6 +434,7 @@ func (v NoticeEnvelope) MarshalJSON() ([]byte, error) {
|
||||
return w.BuildBytes()
|
||||
}
|
||||
|
||||
// EOSEEnvelope represents an EOSE (End of Stored Events) message.
|
||||
type EOSEEnvelope string
|
||||
|
||||
func (_ EOSEEnvelope) Label() string { return "EOSE" }
|
||||
@ -460,6 +469,7 @@ func (v EOSEEnvelope) MarshalJSON() ([]byte, error) {
|
||||
return w.BuildBytes()
|
||||
}
|
||||
|
||||
// CloseEnvelope represents a CLOSE message.
|
||||
type CloseEnvelope string
|
||||
|
||||
func (_ CloseEnvelope) Label() string { return "CLOSE" }
|
||||
@ -496,6 +506,7 @@ func (v CloseEnvelope) MarshalJSON() ([]byte, error) {
|
||||
return w.BuildBytes()
|
||||
}
|
||||
|
||||
// ClosedEnvelope represents a CLOSED message.
|
||||
type ClosedEnvelope struct {
|
||||
SubscriptionID string
|
||||
Reason string
|
||||
@ -539,6 +550,7 @@ func (v ClosedEnvelope) MarshalJSON() ([]byte, error) {
|
||||
return w.BuildBytes()
|
||||
}
|
||||
|
||||
// OKEnvelope represents an OK message.
|
||||
type OKEnvelope struct {
|
||||
EventID string
|
||||
OK bool
|
||||
@ -597,6 +609,7 @@ func (v OKEnvelope) MarshalJSON() ([]byte, error) {
|
||||
return w.BuildBytes()
|
||||
}
|
||||
|
||||
// AuthEnvelope represents an AUTH message.
|
||||
type AuthEnvelope struct {
|
||||
Challenge *string
|
||||
Event Event
|
||||
|
9
event.go
9
event.go
@ -8,6 +8,7 @@ import (
|
||||
"github.com/mailru/easyjson"
|
||||
)
|
||||
|
||||
// Event represents a Nostr event.
|
||||
type Event struct {
|
||||
ID string
|
||||
PubKey string
|
||||
@ -18,19 +19,18 @@ type Event struct {
|
||||
Sig string
|
||||
}
|
||||
|
||||
// Event Stringer interface, just returns the raw JSON as a string.
|
||||
func (evt Event) String() string {
|
||||
j, _ := easyjson.Marshal(evt)
|
||||
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 {
|
||||
h := sha256.Sum256(evt.Serialize())
|
||||
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 {
|
||||
ser := evt.Serialize()
|
||||
h := sha256.Sum256(ser)
|
||||
@ -52,8 +52,7 @@ func (evt *Event) CheckID() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Serialize outputs a byte array that can be hashed/signed to identify/authenticate.
|
||||
// JSON encoding as defined in RFC4627.
|
||||
// Serialize outputs a byte array that can be hashed to produce the canonical event "id".
|
||||
func (evt *Event) Serialize() []byte {
|
||||
// the serialization process is just putting everything into a JSON array
|
||||
// so the order is kept. See NIP-01
|
||||
|
28
keyer.go
28
keyer.go
@ -1,20 +1,38 @@
|
||||
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 {
|
||||
// Signer provides event signing capabilities
|
||||
Signer
|
||||
|
||||
// Cipher provides encryption and decryption capabilities (NIP-44)
|
||||
Cipher
|
||||
}
|
||||
|
||||
// A Signer provides basic public key signing methods.
|
||||
// Signer is an interface for signing events.
|
||||
type Signer interface {
|
||||
GetPublicKey(context.Context) (string, error)
|
||||
SignEvent(context.Context, *Event) error
|
||||
// SignEvent signs the provided event, setting its ID, PubKey, and Sig fields.
|
||||
// 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 {
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
@ -9,15 +9,20 @@ import (
|
||||
"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 {
|
||||
bunker *nip46.BunkerClient
|
||||
}
|
||||
|
||||
// NewBunkerSignerFromBunkerClient creates a new BunkerSigner from an existing BunkerClient.
|
||||
func NewBunkerSignerFromBunkerClient(bc *nip46.BunkerClient) BunkerSigner {
|
||||
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) {
|
||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*30, errors.New("get_public_key took too long"))
|
||||
defer cancel()
|
||||
@ -28,16 +33,20 @@ func (bs BunkerSigner) GetPublicKey(ctx context.Context) (string, error) {
|
||||
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 {
|
||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*30, errors.New("sign_event took too long"))
|
||||
defer cancel()
|
||||
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) {
|
||||
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) {
|
||||
return bs.bunker.NIP44Encrypt(ctx, sender, base64ciphertext)
|
||||
}
|
||||
|
@ -9,13 +9,18 @@ import (
|
||||
"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 {
|
||||
ncryptsec string
|
||||
pk 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) {
|
||||
if es.pk != "" {
|
||||
return es.pk, nil
|
||||
@ -33,6 +38,8 @@ func (es *EncryptedKeySigner) GetPublicKey(ctx context.Context) (string, error)
|
||||
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 {
|
||||
password := es.callback(ctx)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
password := es.callback(ctx)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
password := es.callback(ctx)
|
||||
sk, err := nip49.Decrypt(es.ncryptsec, password)
|
||||
|
26
keyer/lib.go
26
keyer/lib.go
@ -22,19 +22,37 @@ var (
|
||||
_ nostr.Keyer = (*ManualSigner)(nil)
|
||||
)
|
||||
|
||||
// SignerOptions contains configuration options for creating a new signer.
|
||||
type SignerOptions struct {
|
||||
// BunkerClientSecretKey is the secret key used for the bunker client
|
||||
BunkerClientSecretKey string
|
||||
BunkerSignTimeout time.Duration
|
||||
BunkerAuthHandler func(string)
|
||||
|
||||
// if a PasswordHandler is provided the key will be stored encrypted and this function will be called
|
||||
// BunkerSignTimeout is the timeout duration for bunker signing operations
|
||||
BunkerSignTimeout time.Duration
|
||||
|
||||
// BunkerAuthHandler is called when authentication is needed for bunker operations
|
||||
BunkerAuthHandler func(string)
|
||||
|
||||
// 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.
|
||||
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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if opts == nil {
|
||||
opts = &SignerOptions{}
|
||||
|
@ -6,28 +6,40 @@ import (
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
// ManualSigner is a signer that doesn't really do anything, it just calls the functions given to it.
|
||||
// It can be used when an app for some reason wants to ask the user to manually provide a signed event
|
||||
// by copy-and-paste, for example.
|
||||
// ManualSigner is a signer that delegates all operations to user-provided functions.
|
||||
// It can be used when an app wants to ask the user or some custom server to manually provide a
|
||||
// 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 {
|
||||
// ManualGetPublicKey is called when the public key is needed
|
||||
ManualGetPublicKey func(context.Context) (string, error)
|
||||
ManualSignEvent func(context.Context, *nostr.Event) error
|
||||
ManualEncrypt func(ctx context.Context, plaintext string, recipientPublicKey string) (base64ciphertext string, err error)
|
||||
ManualDecrypt func(ctx context.Context, base64ciphertext string, senderPublicKey string) (plaintext string, err error)
|
||||
|
||||
// ManualSignEvent is called when an event needs to be signed
|
||||
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)
|
||||
|
||||
// ManualDecrypt is called when a message needs to be decrypted
|
||||
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 {
|
||||
return ms.ManualSignEvent(ctx, evt)
|
||||
}
|
||||
|
||||
// GetPublicKey delegates public key retrieval to the ManualGetPublicKey function.
|
||||
func (ms ManualSigner) GetPublicKey(ctx context.Context) (string, error) {
|
||||
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) {
|
||||
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) {
|
||||
return ms.ManualDecrypt(ctx, base64ciphertext, sender)
|
||||
}
|
||||
|
@ -8,7 +8,8 @@ import (
|
||||
"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 {
|
||||
sk string
|
||||
pk string
|
||||
@ -16,6 +17,8 @@ type KeySigner struct {
|
||||
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) {
|
||||
pk, err := nostr.GetPublicKey(sec)
|
||||
if err != nil {
|
||||
@ -24,9 +27,15 @@ func NewPlainKeySigner(sec string) (KeySigner, error) {
|
||||
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) }
|
||||
|
||||
// GetPublicKey returns the public key associated with this signer.
|
||||
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) {
|
||||
ck, ok := ks.conversationKeys.Load(recipient)
|
||||
if !ok {
|
||||
@ -40,6 +49,8 @@ func (ks KeySigner) Encrypt(ctx context.Context, plaintext string, recipient str
|
||||
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) {
|
||||
ck, ok := ks.conversationKeys.Load(sender)
|
||||
if !ok {
|
||||
|
18
pointers.go
18
pointers.go
@ -6,9 +6,18 @@ import (
|
||||
"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 {
|
||||
// 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
|
||||
|
||||
// AsTag converts the pointer with all the information available to a tag that can be included in events.
|
||||
AsTag() Tag
|
||||
|
||||
// AsFilter converts the pointer to a Filter that can be used to query for it on relays.
|
||||
AsFilter() Filter
|
||||
MatchesEvent(Event) bool
|
||||
}
|
||||
@ -19,11 +28,13 @@ var (
|
||||
_ Pointer = (*EntityPointer)(nil)
|
||||
)
|
||||
|
||||
// ProfilePointer represents a pointer to a Nostr profile.
|
||||
type ProfilePointer struct {
|
||||
PublicKey string `json:"pubkey"`
|
||||
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) {
|
||||
pk := (refTag)[1]
|
||||
if !IsValidPublicKey(pk) {
|
||||
@ -41,6 +52,7 @@ func ProfilePointerFromTag(refTag Tag) (ProfilePointer, error) {
|
||||
return pointer, nil
|
||||
}
|
||||
|
||||
// MatchesEvent checks if the pointer matches an event.
|
||||
func (ep ProfilePointer) MatchesEvent(_ Event) bool { return false }
|
||||
func (ep ProfilePointer) AsTagReference() string { return 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}
|
||||
}
|
||||
|
||||
// EventPointer represents a pointer to a nostr event.
|
||||
type EventPointer struct {
|
||||
ID string `json:"id"`
|
||||
Relays []string `json:"relays,omitempty"`
|
||||
@ -59,6 +72,7 @@ type EventPointer struct {
|
||||
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) {
|
||||
id := (refTag)[1]
|
||||
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) AsFilter() Filter { return Filter{IDs: []string{ep.ID}} }
|
||||
|
||||
// AsTag converts the pointer to a Tag.
|
||||
func (ep EventPointer) AsTag() Tag {
|
||||
if len(ep.Relays) > 0 {
|
||||
if ep.Author != "" {
|
||||
@ -94,6 +109,7 @@ func (ep EventPointer) AsTag() Tag {
|
||||
return Tag{"e", ep.ID}
|
||||
}
|
||||
|
||||
// EntityPointer represents a pointer to a nostr entity (addressable event).
|
||||
type EntityPointer struct {
|
||||
PublicKey string `json:"pubkey"`
|
||||
Kind int `json:"kind,omitempty"`
|
||||
@ -101,6 +117,7 @@ type EntityPointer struct {
|
||||
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) {
|
||||
spl := strings.SplitN(refTag[1], ":", 3)
|
||||
if len(spl) != 3 {
|
||||
@ -129,6 +146,7 @@ func EntityPointerFromTag(refTag Tag) (EntityPointer, error) {
|
||||
return pointer, nil
|
||||
}
|
||||
|
||||
// MatchesEvent checks if the pointer matches an event.
|
||||
func (ep EntityPointer) MatchesEvent(evt Event) bool {
|
||||
return ep.PublicKey == evt.PubKey &&
|
||||
ep.Kind == evt.Kind &&
|
||||
|
19
pool.go
19
pool.go
@ -20,6 +20,7 @@ const (
|
||||
seenAlreadyDropTick = time.Minute
|
||||
)
|
||||
|
||||
// SimplePool manages connections to multiple relays, ensures they are reopened when necessary and not duplicated.
|
||||
type SimplePool struct {
|
||||
Relays *xsync.MapOf[string, *Relay]
|
||||
Context context.Context
|
||||
@ -37,11 +38,13 @@ type SimplePool struct {
|
||||
relayOptions []RelayOption
|
||||
}
|
||||
|
||||
// DirectedFilter combines a Filter with a specific relay URL.
|
||||
type DirectedFilter struct {
|
||||
Filter
|
||||
Relay string
|
||||
}
|
||||
|
||||
// RelayEvent represents an event received from a specific relay.
|
||||
type RelayEvent struct {
|
||||
*Event
|
||||
Relay *Relay
|
||||
@ -49,10 +52,12 @@ type RelayEvent struct {
|
||||
|
||||
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 {
|
||||
ApplyPoolOption(*SimplePool)
|
||||
}
|
||||
|
||||
// NewSimplePool creates a new SimplePool with the given context and options.
|
||||
func NewSimplePool(ctx context.Context, opts ...PoolOption) *SimplePool {
|
||||
ctx, cancel := context.WithCancelCause(ctx)
|
||||
|
||||
@ -140,7 +145,7 @@ func (h WithDuplicateMiddleware) ApplyPoolOption(pool *SimplePool) {
|
||||
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).
|
||||
type WithAuthorKindQueryMiddleware func(relay string, pubkey string, kind int)
|
||||
|
||||
@ -155,6 +160,8 @@ var (
|
||||
_ 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) {
|
||||
nm := NormalizeURL(url)
|
||||
defer namedLock(nm)()
|
||||
@ -199,12 +206,14 @@ func (pool *SimplePool) EnsureRelay(url string) (*Relay, error) {
|
||||
return relay, nil
|
||||
}
|
||||
|
||||
// PublishResult represents the result of publishing an event to a relay.
|
||||
type PublishResult struct {
|
||||
Error error
|
||||
RelayURL string
|
||||
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 {
|
||||
ch := make(chan PublishResult, len(urls))
|
||||
|
||||
@ -247,7 +256,7 @@ func (pool *SimplePool) FetchMany(
|
||||
return pool.SubManyEose(ctx, urls, Filters{filter}, opts...)
|
||||
}
|
||||
|
||||
// Deprecated: use SubscribeMany instead.
|
||||
// Deprecated: SubMany is deprecated: use SubscribeMany instead.
|
||||
func (pool *SimplePool) SubMany(
|
||||
ctx context.Context,
|
||||
urls []string,
|
||||
@ -447,7 +456,7 @@ func (pool *SimplePool) subMany(
|
||||
return events
|
||||
}
|
||||
|
||||
// Deprecated: use FetchMany instead.
|
||||
// Deprecated: SubManyEose is deprecated: use FetchMany instead.
|
||||
func (pool *SimplePool) SubManyEose(
|
||||
ctx context.Context,
|
||||
urls []string,
|
||||
@ -562,7 +571,7 @@ func (pool *SimplePool) subManyEoseNonOverwriteCheckDuplicate(
|
||||
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(
|
||||
ctx context.Context,
|
||||
urls []string,
|
||||
@ -611,6 +620,7 @@ func (pool *SimplePool) QuerySingle(
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchedSubManyEose performs batched subscriptions to multiple relays with different filters.
|
||||
func (pool *SimplePool) BatchedSubManyEose(
|
||||
ctx context.Context,
|
||||
dfs []DirectedFilter,
|
||||
@ -648,6 +658,7 @@ func (pool *SimplePool) BatchedSubManyEose(
|
||||
return res
|
||||
}
|
||||
|
||||
// Close closes the pool with the given reason.
|
||||
func (pool *SimplePool) Close(reason string) {
|
||||
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"
|
||||
)
|
||||
|
||||
type Status int
|
||||
|
||||
var subscriptionIDCounter atomic.Int64
|
||||
|
||||
// Relay represents a connection to a Nostr relay.
|
||||
type Relay struct {
|
||||
closeMutex sync.Mutex
|
||||
|
||||
@ -51,7 +50,7 @@ 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. It takes a context that, when canceled, will close the relay connection.
|
||||
func NewRelay(ctx context.Context, url string, opts ...RelayOption) *Relay {
|
||||
ctx, cancel := context.WithCancelCause(ctx)
|
||||
r := &Relay{
|
||||
@ -73,16 +72,18 @@ func NewRelay(ctx context.Context, url string, opts ...RelayOption) *Relay {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
r := NewRelay(context.Background(), url, opts...)
|
||||
err := r.Connect(ctx)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// When instantiating relay connections, some options may be passed.
|
||||
// RelayOption is the type of the argument passed for that.
|
||||
// RelayOption is the type of the argument passed when instantiating relay connections.
|
||||
type RelayOption interface {
|
||||
ApplyRelayOption(*Relay)
|
||||
}
|
||||
@ -122,6 +123,7 @@ func (r *Relay) String() string {
|
||||
}
|
||||
|
||||
// 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 }
|
||||
|
||||
// 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
|
||||
// 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 Relay.Connect().
|
||||
// The given context here is only used during the connection phase. The long-living
|
||||
// relay connection will be based on the context given to NewRelay().
|
||||
func (r *Relay) Connect(ctx context.Context) error {
|
||||
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 {
|
||||
if r.connectionContext == nil || r.Subscriptions == nil {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
ch := make(chan error)
|
||||
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.
|
||||
//
|
||||
// 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 {
|
||||
authEvent := Event{
|
||||
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})
|
||||
}
|
||||
|
||||
// publish can be used both for EVENT and for AUTH
|
||||
func (r *Relay) publish(ctx context.Context, id string, env Envelope) error {
|
||||
var err error
|
||||
var cancel context.CancelFunc
|
||||
@ -451,6 +454,9 @@ func (r *Relay) PrepareSubscription(ctx context.Context, filters Filters, opts .
|
||||
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) {
|
||||
sub, err := r.Subscribe(ctx, Filters{filter})
|
||||
if err != nil {
|
||||
@ -473,6 +479,10 @@ func (r *Relay) QueryEvents(ctx context.Context, filter Filter) (chan *Event, er
|
||||
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) {
|
||||
if _, ok := ctx.Deadline(); !ok {
|
||||
// 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
|
||||
}
|
||||
|
||||
// Count sends a "COUNT" command to the relay and returns the count of events matching the filters.
|
||||
func (r *Relay) Count(
|
||||
ctx context.Context,
|
||||
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 {
|
||||
return r.close(errors.New("Close() called"))
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ import (
|
||||
|
||||
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 {
|
||||
// format: 'r' + first 8 bytes of event ID
|
||||
key := make([]byte, 9)
|
||||
@ -18,6 +20,8 @@ func makeEventRelayKey(eventID []byte) []byte {
|
||||
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 {
|
||||
totalSize := 0
|
||||
for _, relay := range relays {
|
||||
@ -43,6 +47,8 @@ func encodeRelayList(relays []string) []byte {
|
||||
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 {
|
||||
relays := make([]string, 0, 6)
|
||||
offset := 0
|
||||
@ -67,6 +73,8 @@ func decodeRelayList(data []byte) []string {
|
||||
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) {
|
||||
// decode the event ID hex into bytes
|
||||
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) {
|
||||
// decode the event ID hex into bytes
|
||||
idBytes, err := hex.DecodeString(eventID)
|
||||
|
@ -9,6 +9,8 @@ import (
|
||||
|
||||
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 {
|
||||
for _, item := range item {
|
||||
if slices.Contains(arr, item) {
|
||||
@ -19,10 +21,19 @@ func appendUnique[I comparable](arr []I, item ...I) []I {
|
||||
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) {
|
||||
_dtnmtoahLock.Lock()
|
||||
defer _dtnmtoahLock.Unlock()
|
||||
|
||||
if _dtnmtoah == nil {
|
||||
// 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)
|
||||
go func() {
|
||||
_dtnmtoah = make(map[string]time.Time)
|
||||
for {
|
||||
time.Sleep(time.Minute * 10)
|
||||
_dtnmtoahLock.Lock()
|
||||
@ -37,9 +48,11 @@ func doThisNotMoreThanOnceAnHour(key string) (doItNow bool) {
|
||||
}()
|
||||
}
|
||||
|
||||
_dtnmtoahLock.Lock()
|
||||
defer _dtnmtoahLock.Unlock()
|
||||
_, hasBeenPerformedInTheLastHour := _dtnmtoah[key]
|
||||
if hasBeenPerformedInTheLastHour {
|
||||
return false
|
||||
}
|
||||
|
||||
_, exists := _dtnmtoah[key]
|
||||
return !exists
|
||||
_dtnmtoah[key] = time.Now()
|
||||
return true
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ import (
|
||||
"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 {
|
||||
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
|
||||
@ -29,21 +31,28 @@ type ProfileMetadata struct {
|
||||
nip05LastAttempt time.Time
|
||||
}
|
||||
|
||||
// Npub returns the NIP-19 npub encoding of the profile's public key.
|
||||
func (p ProfileMetadata) Npub() string {
|
||||
v, _ := nip19.EncodePublicKey(p.PubKey)
|
||||
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 {
|
||||
npub := p.Npub()
|
||||
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 {
|
||||
v, _ := nip19.EncodeProfile(p.PubKey, sys.FetchOutboxRelays(ctx, p.PubKey, 2))
|
||||
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 {
|
||||
if p.Name != "" {
|
||||
return p.Name
|
||||
@ -54,6 +63,7 @@ func (p ProfileMetadata) ShortName() string {
|
||||
return p.NpubShort()
|
||||
}
|
||||
|
||||
// NIP05Valid checks if the profile's NIP-05 identifier is valid.
|
||||
func (p *ProfileMetadata) NIP05Valid(ctx context.Context) bool {
|
||||
if p.NIP05 == "" {
|
||||
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,
|
||||
// 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) {
|
||||
p := InputToProfile(ctx, nip19OrNip05Code)
|
||||
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,
|
||||
// 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) {
|
||||
if v, ok := sys.MetadataCache.Get(pubkey); ok {
|
||||
return v
|
||||
@ -160,6 +172,8 @@ func (sys *System) tryFetchMetadataFromNetwork(ctx context.Context, pubkey strin
|
||||
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) {
|
||||
if event.Kind != 0 {
|
||||
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"
|
||||
)
|
||||
|
||||
// FetchSpecificEventParameters contains options for fetching specific events.
|
||||
type FetchSpecificEventParameters struct {
|
||||
WithRelays bool
|
||||
// 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
|
||||
|
||||
// SkipLocalStore indicates whether to skip checking the local store for the event
|
||||
// and storing the result in the local store.
|
||||
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(
|
||||
ctx context.Context,
|
||||
input string,
|
||||
@ -47,7 +54,9 @@ func (sys *System) FetchSpecificEventFromInput(
|
||||
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(
|
||||
ctx context.Context,
|
||||
pointer nostr.Pointer,
|
||||
|
@ -16,6 +16,16 @@ import (
|
||||
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 {
|
||||
KVStore kvstore.KVStore
|
||||
MetadataCache cache.Cache32[ProfileMetadata]
|
||||
@ -47,22 +57,35 @@ type System struct {
|
||||
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)
|
||||
|
||||
// RelayStream provides a rotating list of relay URLs.
|
||||
// It's used to distribute requests across multiple relays.
|
||||
type RelayStream struct {
|
||||
URLs []string
|
||||
serial int
|
||||
}
|
||||
|
||||
// NewRelayStream creates a new RelayStream with the provided URLs.
|
||||
func NewRelayStream(urls ...string) *RelayStream {
|
||||
return &RelayStream{URLs: urls, serial: rand.Int()}
|
||||
}
|
||||
|
||||
// Next returns the next URL in the rotation.
|
||||
func (rs *RelayStream) Next() string {
|
||||
rs.serial++
|
||||
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 {
|
||||
sys := &System{
|
||||
KVStore: kvstore_memory.NewStore(),
|
||||
@ -123,84 +146,101 @@ func NewSystem(mods ...SystemModifier) *System {
|
||||
return sys
|
||||
}
|
||||
|
||||
// Close releases resources held by the System.
|
||||
func (sys *System) Close() {
|
||||
if sys.KVStore != nil {
|
||||
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 {
|
||||
return func(sys *System) {
|
||||
sys.Hints = hdb
|
||||
}
|
||||
}
|
||||
|
||||
// WithRelayListRelays returns a SystemModifier that sets the RelayListRelays.
|
||||
func WithRelayListRelays(list []string) SystemModifier {
|
||||
return func(sys *System) {
|
||||
sys.RelayListRelays.URLs = list
|
||||
}
|
||||
}
|
||||
|
||||
// WithMetadataRelays returns a SystemModifier that sets the MetadataRelays.
|
||||
func WithMetadataRelays(list []string) SystemModifier {
|
||||
return func(sys *System) {
|
||||
sys.MetadataRelays.URLs = list
|
||||
}
|
||||
}
|
||||
|
||||
// WithFollowListRelays returns a SystemModifier that sets the FollowListRelays.
|
||||
func WithFollowListRelays(list []string) SystemModifier {
|
||||
return func(sys *System) {
|
||||
sys.FollowListRelays.URLs = list
|
||||
}
|
||||
}
|
||||
|
||||
// WithFallbackRelays returns a SystemModifier that sets the FallbackRelays.
|
||||
func WithFallbackRelays(list []string) SystemModifier {
|
||||
return func(sys *System) {
|
||||
sys.FallbackRelays.URLs = list
|
||||
}
|
||||
}
|
||||
|
||||
// WithJustIDRelays returns a SystemModifier that sets the JustIDRelays.
|
||||
func WithJustIDRelays(list []string) SystemModifier {
|
||||
return func(sys *System) {
|
||||
sys.JustIDRelays.URLs = list
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserSearchRelays returns a SystemModifier that sets the UserSearchRelays.
|
||||
func WithUserSearchRelays(list []string) SystemModifier {
|
||||
return func(sys *System) {
|
||||
sys.UserSearchRelays.URLs = list
|
||||
}
|
||||
}
|
||||
|
||||
// WithNoteSearchRelays returns a SystemModifier that sets the NoteSearchRelays.
|
||||
func WithNoteSearchRelays(list []string) SystemModifier {
|
||||
return func(sys *System) {
|
||||
sys.NoteSearchRelays.URLs = list
|
||||
}
|
||||
}
|
||||
|
||||
// WithStore returns a SystemModifier that sets the Store.
|
||||
func WithStore(store eventstore.Store) SystemModifier {
|
||||
return func(sys *System) {
|
||||
sys.Store = store
|
||||
}
|
||||
}
|
||||
|
||||
// WithRelayListCache returns a SystemModifier that sets the RelayListCache.
|
||||
func WithRelayListCache(cache cache.Cache32[GenericList[Relay]]) SystemModifier {
|
||||
return func(sys *System) {
|
||||
sys.RelayListCache = cache
|
||||
}
|
||||
}
|
||||
|
||||
// WithFollowListCache returns a SystemModifier that sets the FollowListCache.
|
||||
func WithFollowListCache(cache cache.Cache32[GenericList[ProfileRef]]) SystemModifier {
|
||||
return func(sys *System) {
|
||||
sys.FollowListCache = cache
|
||||
}
|
||||
}
|
||||
|
||||
// WithMetadataCache returns a SystemModifier that sets the MetadataCache.
|
||||
func WithMetadataCache(cache cache.Cache32[ProfileMetadata]) SystemModifier {
|
||||
return func(sys *System) {
|
||||
sys.MetadataCache = cache
|
||||
}
|
||||
}
|
||||
|
||||
// WithKVStore returns a SystemModifier that sets the KVStore.
|
||||
func WithKVStore(store kvstore.KVStore) SystemModifier {
|
||||
return func(sys *System) {
|
||||
if sys.KVStore != nil {
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
_dtnmtoah map[string]time.Time
|
||||
_dtnmtoah map[string]time.Time = make(map[string]time.Time)
|
||||
_dtnmtoahLock sync.Mutex
|
||||
)
|
||||
|
||||
@ -21,6 +21,8 @@ func IsVirtualRelay(url string) bool {
|
||||
|
||||
if strings.HasPrefix(url, "wss://feeds.nostr.band") ||
|
||||
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") {
|
||||
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
|
||||
// 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 {
|
||||
if numberOfQueries == 1 || totalFilterLimit*numberOfQueries < 50 {
|
||||
return totalFilterLimit
|
||||
|
@ -11,9 +11,9 @@ import (
|
||||
"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.
|
||||
// CheckSignature checks if the event signature is valid for the given event.
|
||||
// It won't look at the ID field, instead it will recompute the id from the entire event body.
|
||||
// If the signature is invalid bool will be false and err will be set.
|
||||
func (evt Event) CheckSignature() (bool, error) {
|
||||
// read and check 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.
|
||||
// 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 {
|
||||
s, err := hex.DecodeString(secretKey)
|
||||
if err != nil {
|
||||
|
@ -34,9 +34,6 @@ import (
|
||||
"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) {
|
||||
var pk [32]byte
|
||||
_, err := hex.Decode(pk[:], []byte(evt.PubKey))
|
||||
@ -61,7 +58,6 @@ func (evt Event) CheckSignature() (bool, error) {
|
||||
return res == 1, nil
|
||||
}
|
||||
|
||||
// Sign signs an event with a given privateKey.
|
||||
func (evt *Event) Sign(secretKey string, signOpts ...schnorr.SignOption) error {
|
||||
sk, err := hex.DecodeString(secretKey)
|
||||
if err != nil {
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Subscription represents a subscription to a relay.
|
||||
type Subscription struct {
|
||||
counter int64
|
||||
id string
|
||||
@ -46,13 +47,7 @@ type Subscription struct {
|
||||
storedwg sync.WaitGroup
|
||||
}
|
||||
|
||||
type EventMessage struct {
|
||||
Event Event
|
||||
Relay string
|
||||
}
|
||||
|
||||
// When instantiating relay connections, some options may be passed.
|
||||
// SubscriptionOption is the type of the argument passed for that.
|
||||
// SubscriptionOption is the type of the argument passed when instantiating relay connections.
|
||||
// Some examples are WithLabel.
|
||||
type SubscriptionOption interface {
|
||||
IsSubscriptionOption()
|
||||
@ -85,6 +80,7 @@ func (sub *Subscription) start() {
|
||||
sub.mu.Unlock()
|
||||
}
|
||||
|
||||
// GetID returns the subscription ID.
|
||||
func (sub *Subscription) GetID() string { return sub.id }
|
||||
|
||||
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) {
|
||||
go func() {
|
||||
sub.ClosedReason <- reason
|
||||
@ -135,6 +132,7 @@ func (sub *Subscription) Unsub() {
|
||||
sub.unsub(errors.New("Unsub() called"))
|
||||
}
|
||||
|
||||
// unsub is the internal implementation of Unsub.
|
||||
func (sub *Subscription) unsub(err error) {
|
||||
// cancel the context (if it's not canceled already)
|
||||
sub.cancel(err)
|
||||
|
6
utils.go
6
utils.go
@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsValidRelayURL checks if a URL is a valid relay URL (ws:// or wss://).
|
||||
func IsValidRelayURL(u string) bool {
|
||||
parsed, err := url.Parse(u)
|
||||
if err != nil {
|
||||
@ -18,6 +19,7 @@ func IsValidRelayURL(u string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsValid32ByteHex checks if a string is a valid 32-byte hex string.
|
||||
func IsValid32ByteHex(thing string) bool {
|
||||
if !isLowerHex(thing) {
|
||||
return false
|
||||
@ -29,6 +31,7 @@ func IsValid32ByteHex(thing string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// CompareEvent is meant to to be used with slices.Sort
|
||||
func CompareEvent(a, b Event) int {
|
||||
if a.CreatedAt == b.CreatedAt {
|
||||
return strings.Compare(a.ID, b.ID)
|
||||
@ -36,6 +39,7 @@ func CompareEvent(a, b Event) int {
|
||||
return cmp.Compare(a.CreatedAt, b.CreatedAt)
|
||||
}
|
||||
|
||||
// CompareEventReverse is meant to to be used with slices.Sort
|
||||
func CompareEventReverse(b, a Event) int {
|
||||
if a.CreatedAt == b.CreatedAt {
|
||||
return strings.Compare(a.ID, b.ID)
|
||||
@ -43,6 +47,7 @@ func CompareEventReverse(b, a Event) int {
|
||||
return cmp.Compare(a.CreatedAt, b.CreatedAt)
|
||||
}
|
||||
|
||||
// CompareEventPtr is meant to to be used with slices.Sort
|
||||
func CompareEventPtr(a, b *Event) int {
|
||||
if a == nil {
|
||||
if b == nil {
|
||||
@ -60,6 +65,7 @@ func CompareEventPtr(a, b *Event) int {
|
||||
return cmp.Compare(a.CreatedAt, b.CreatedAt)
|
||||
}
|
||||
|
||||
// CompareEventPtrReverse is meant to to be used with slices.Sort
|
||||
func CompareEventPtrReverse(b, a *Event) int {
|
||||
if a == nil {
|
||||
if b == nil {
|
||||
|
Loading…
x
Reference in New Issue
Block a user