subscription.go raw

   1  package bridge
   2  
   3  import (
   4  	"encoding/json"
   5  	"fmt"
   6  	"os"
   7  	"path/filepath"
   8  	"sync"
   9  	"time"
  10  )
  11  
  12  // Subscription represents a user's bridge subscription.
  13  type Subscription struct {
  14  	// PubkeyHex is the subscriber's 32-byte pubkey in hex.
  15  	PubkeyHex string `json:"pubkey"`
  16  	// ExpiresAt is the subscription expiration time.
  17  	ExpiresAt time.Time `json:"expires_at"`
  18  	// CreatedAt is when the subscription was created.
  19  	CreatedAt time.Time `json:"created_at"`
  20  	// InvoiceHash is the payment hash of the last paid invoice.
  21  	InvoiceHash string `json:"invoice_hash,omitempty"`
  22  }
  23  
  24  // IsActive returns true if the subscription has not expired.
  25  func (s *Subscription) IsActive() bool {
  26  	return time.Now().Before(s.ExpiresAt)
  27  }
  28  
  29  // SubscriptionStore persists and queries subscriptions.
  30  type SubscriptionStore interface {
  31  	Save(sub *Subscription) error
  32  	Get(pubkeyHex string) (*Subscription, error)
  33  	List() ([]*Subscription, error)
  34  	Delete(pubkeyHex string) error
  35  }
  36  
  37  // FileSubscriptionStore persists subscriptions as a JSON file.
  38  type FileSubscriptionStore struct {
  39  	path string
  40  	mu   sync.RWMutex
  41  	subs map[string]*Subscription
  42  }
  43  
  44  // NewFileSubscriptionStore creates a subscription store backed by a JSON file.
  45  func NewFileSubscriptionStore(dataDir string) (*FileSubscriptionStore, error) {
  46  	path := filepath.Join(dataDir, "subscriptions.json")
  47  	store := &FileSubscriptionStore{
  48  		path: path,
  49  		subs: make(map[string]*Subscription),
  50  	}
  51  
  52  	// Load existing file if present
  53  	data, err := os.ReadFile(path)
  54  	if err == nil {
  55  		var subs []*Subscription
  56  		if err := json.Unmarshal(data, &subs); err == nil {
  57  			for _, s := range subs {
  58  				store.subs[s.PubkeyHex] = s
  59  			}
  60  		}
  61  	}
  62  
  63  	return store, nil
  64  }
  65  
  66  func (s *FileSubscriptionStore) Save(sub *Subscription) error {
  67  	s.mu.Lock()
  68  	defer s.mu.Unlock()
  69  
  70  	s.subs[sub.PubkeyHex] = sub
  71  	return s.flush()
  72  }
  73  
  74  func (s *FileSubscriptionStore) Get(pubkeyHex string) (*Subscription, error) {
  75  	s.mu.RLock()
  76  	defer s.mu.RUnlock()
  77  
  78  	sub, ok := s.subs[pubkeyHex]
  79  	if !ok {
  80  		return nil, fmt.Errorf("subscription not found for %s", pubkeyHex)
  81  	}
  82  	return sub, nil
  83  }
  84  
  85  func (s *FileSubscriptionStore) List() ([]*Subscription, error) {
  86  	s.mu.RLock()
  87  	defer s.mu.RUnlock()
  88  
  89  	var subs []*Subscription
  90  	for _, sub := range s.subs {
  91  		subs = append(subs, sub)
  92  	}
  93  	return subs, nil
  94  }
  95  
  96  func (s *FileSubscriptionStore) Delete(pubkeyHex string) error {
  97  	s.mu.Lock()
  98  	defer s.mu.Unlock()
  99  
 100  	delete(s.subs, pubkeyHex)
 101  	return s.flush()
 102  }
 103  
 104  func (s *FileSubscriptionStore) flush() error {
 105  	var subs []*Subscription
 106  	for _, sub := range s.subs {
 107  		subs = append(subs, sub)
 108  	}
 109  
 110  	data, err := json.MarshalIndent(subs, "", "  ")
 111  	if err != nil {
 112  		return fmt.Errorf("marshal subscriptions: %w", err)
 113  	}
 114  
 115  	dir := filepath.Dir(s.path)
 116  	if err := os.MkdirAll(dir, 0700); err != nil {
 117  		return fmt.Errorf("create subscription dir: %w", err)
 118  	}
 119  
 120  	return os.WriteFile(s.path, data, 0600)
 121  }
 122  
 123  // MemorySubscriptionStore is an in-memory subscription store for testing.
 124  type MemorySubscriptionStore struct {
 125  	mu   sync.RWMutex
 126  	subs map[string]*Subscription
 127  }
 128  
 129  // NewMemorySubscriptionStore creates a new in-memory subscription store.
 130  func NewMemorySubscriptionStore() *MemorySubscriptionStore {
 131  	return &MemorySubscriptionStore{
 132  		subs: make(map[string]*Subscription),
 133  	}
 134  }
 135  
 136  func (s *MemorySubscriptionStore) Save(sub *Subscription) error {
 137  	s.mu.Lock()
 138  	defer s.mu.Unlock()
 139  	s.subs[sub.PubkeyHex] = sub
 140  	return nil
 141  }
 142  
 143  func (s *MemorySubscriptionStore) Get(pubkeyHex string) (*Subscription, error) {
 144  	s.mu.RLock()
 145  	defer s.mu.RUnlock()
 146  	sub, ok := s.subs[pubkeyHex]
 147  	if !ok {
 148  		return nil, fmt.Errorf("subscription not found for %s", pubkeyHex)
 149  	}
 150  	return sub, nil
 151  }
 152  
 153  func (s *MemorySubscriptionStore) List() ([]*Subscription, error) {
 154  	s.mu.RLock()
 155  	defer s.mu.RUnlock()
 156  	var subs []*Subscription
 157  	for _, sub := range s.subs {
 158  		subs = append(subs, sub)
 159  	}
 160  	return subs, nil
 161  }
 162  
 163  func (s *MemorySubscriptionStore) Delete(pubkeyHex string) error {
 164  	s.mu.Lock()
 165  	defer s.mu.Unlock()
 166  	delete(s.subs, pubkeyHex)
 167  	return nil
 168  }
 169