docstrings for many functions.

This commit is contained in:
fiatjaf 2025-03-04 11:08:31 -03:00
parent a82780e82e
commit 5bfaed2740
22 changed files with 293 additions and 66 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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