package nostr import ( "context" "fmt" "sync" "time" "github.com/nbd-wtf/go-nostr" ) // Client handles connections to Nostr relays with failover support type Client struct { primaryRelay string fallbackRelay string mu sync.RWMutex lastError error } // NewClient creates a new Nostr client with primary and fallback relays func NewClient(primaryRelay, fallbackRelay string) *Client { return &Client{ primaryRelay: primaryRelay, fallbackRelay: fallbackRelay, } } // Connect connects to the relays (no-op for now, connections happen on query) func (c *Client) Connect(ctx context.Context) error { // Connections are established lazily when querying return nil } // ConnectToRelay connects to a single relay (exported for use by services) func (c *Client) ConnectToRelay(ctx context.Context, url string) (*nostr.Relay, error) { relay, err := nostr.RelayConnect(ctx, url) if err != nil { return nil, err } return relay, nil } // connectToRelay connects to a single relay (deprecated, use ConnectToRelay) func (c *Client) connectToRelay(ctx context.Context, url string) (*nostr.Relay, error) { return c.ConnectToRelay(ctx, url) } // FetchEvent fetches a single event by filter, trying primary relay first, then fallback func (c *Client) FetchEvent(ctx context.Context, filter nostr.Filter) (*nostr.Event, error) { // Try primary relay first relay, err := c.connectToRelay(ctx, c.primaryRelay) if err == nil { events, err := relay.QuerySync(ctx, filter) relay.Close() if err == nil && len(events) > 0 { return events[0], nil } } // Try fallback relay relay, err = c.connectToRelay(ctx, c.fallbackRelay) if err != nil { return nil, fmt.Errorf("failed to connect to both relays: %w", err) } defer relay.Close() events, err := relay.QuerySync(ctx, filter) if err != nil { return nil, fmt.Errorf("failed to fetch event: %w", err) } if len(events) == 0 { return nil, fmt.Errorf("event not found") } return events[0], nil } // FetchEvents fetches multiple events by filter, trying primary relay first, then fallback func (c *Client) FetchEvents(ctx context.Context, filter nostr.Filter) ([]*nostr.Event, error) { // Try primary relay first relay, err := c.connectToRelay(ctx, c.primaryRelay) if err == nil { events, err := relay.QuerySync(ctx, filter) relay.Close() if err == nil { return events, nil } } // Try fallback relay relay, err = c.connectToRelay(ctx, c.fallbackRelay) if err != nil { return nil, fmt.Errorf("failed to connect to both relays: %w", err) } defer relay.Close() events, err := relay.QuerySync(ctx, filter) if err != nil { return nil, fmt.Errorf("failed to fetch events: %w", err) } return events, nil } // FetchEventByID fetches an event by its ID func (c *Client) FetchEventByID(ctx context.Context, eventID string) (*nostr.Event, error) { filter := nostr.Filter{ IDs: []string{eventID}, } return c.FetchEvent(ctx, filter) } // FetchEventsByKind fetches events of a specific kind func (c *Client) FetchEventsByKind(ctx context.Context, kind int, limit int) ([]*nostr.Event, error) { filter := nostr.Filter{ Kinds: []int{kind}, Limit: limit, } return c.FetchEvents(ctx, filter) } // Close closes all relay connections (no-op for lazy connections) func (c *Client) Close() { // Connections are closed after each query, so nothing to do here } // GetLastError returns the last error encountered func (c *Client) GetLastError() error { c.mu.RLock() defer c.mu.RUnlock() return c.lastError } // IsConnected checks if at least one relay is connected (always true for lazy connections) func (c *Client) IsConnected() bool { return true // We connect on-demand } // HealthCheck performs a health check on the relays func (c *Client) HealthCheck(ctx context.Context, timeout time.Duration) error { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() // Try to fetch a recent event to test connectivity filter := nostr.Filter{ Kinds: []int{1}, // kind 1 (notes) for testing Limit: 1, } _, err := c.FetchEvents(ctx, filter) return err }