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