subscriptions.go raw
1 //go:build !(js && wasm)
2
3 package database
4
5 import (
6 "encoding/hex"
7 "errors"
8 "fmt"
9 "strings"
10 "time"
11
12 "encoding/json"
13
14 "github.com/dgraph-io/badger/v4"
15 )
16
17 func (d *D) GetSubscription(pubkey []byte) (*Subscription, error) {
18 key := fmt.Sprintf("sub:%s", hex.EncodeToString(pubkey))
19 var sub *Subscription
20
21 err := d.DB.View(
22 func(txn *badger.Txn) error {
23 item, err := txn.Get([]byte(key))
24 if errors.Is(err, badger.ErrKeyNotFound) {
25 return nil
26 }
27 if err != nil {
28 return err
29 }
30 return item.Value(
31 func(val []byte) error {
32 sub = &Subscription{}
33 return json.Unmarshal(val, sub)
34 },
35 )
36 },
37 )
38 return sub, err
39 }
40
41 func (d *D) IsSubscriptionActive(pubkey []byte) (bool, error) {
42 key := fmt.Sprintf("sub:%s", hex.EncodeToString(pubkey))
43 now := time.Now()
44 active := false
45
46 err := d.DB.Update(
47 func(txn *badger.Txn) error {
48 item, err := txn.Get([]byte(key))
49 if errors.Is(err, badger.ErrKeyNotFound) {
50 sub := &Subscription{TrialEnd: now.AddDate(0, 0, 30)}
51 data, err := json.Marshal(sub)
52 if err != nil {
53 return err
54 }
55 active = true
56 return txn.Set([]byte(key), data)
57 }
58 if err != nil {
59 return err
60 }
61
62 var sub Subscription
63 err = item.Value(
64 func(val []byte) error {
65 return json.Unmarshal(val, &sub)
66 },
67 )
68 if err != nil {
69 return err
70 }
71
72 active = now.Before(sub.TrialEnd) || (!sub.PaidUntil.IsZero() && now.Before(sub.PaidUntil))
73 return nil
74 },
75 )
76 return active, err
77 }
78
79 func (d *D) ExtendSubscription(pubkey []byte, days int) error {
80 if days <= 0 {
81 return fmt.Errorf("invalid days: %d", days)
82 }
83
84 key := fmt.Sprintf("sub:%s", hex.EncodeToString(pubkey))
85 now := time.Now()
86
87 return d.DB.Update(
88 func(txn *badger.Txn) error {
89 var sub Subscription
90 item, err := txn.Get([]byte(key))
91 if errors.Is(err, badger.ErrKeyNotFound) {
92 sub.PaidUntil = now.AddDate(0, 0, days)
93 } else if err != nil {
94 return err
95 } else {
96 err = item.Value(
97 func(val []byte) error {
98 return json.Unmarshal(val, &sub)
99 },
100 )
101 if err != nil {
102 return err
103 }
104 extendFrom := now
105 if !sub.PaidUntil.IsZero() && sub.PaidUntil.After(now) {
106 extendFrom = sub.PaidUntil
107 }
108 sub.PaidUntil = extendFrom.AddDate(0, 0, days)
109 }
110
111 data, err := json.Marshal(&sub)
112 if err != nil {
113 return err
114 }
115 return txn.Set([]byte(key), data)
116 },
117 )
118 }
119
120 func (d *D) RecordPayment(
121 pubkey []byte, amount int64, invoice, preimage string,
122 ) error {
123 now := time.Now()
124 key := fmt.Sprintf("payment:%d:%s", now.Unix(), hex.EncodeToString(pubkey))
125
126 payment := Payment{
127 Amount: amount,
128 Timestamp: now,
129 Invoice: invoice,
130 Preimage: preimage,
131 }
132
133 data, err := json.Marshal(&payment)
134 if err != nil {
135 return err
136 }
137
138 return d.DB.Update(
139 func(txn *badger.Txn) error {
140 return txn.Set([]byte(key), data)
141 },
142 )
143 }
144
145 func (d *D) GetPaymentHistory(pubkey []byte) ([]Payment, error) {
146 prefix := fmt.Sprintf("payment:")
147 suffix := fmt.Sprintf(":%s", hex.EncodeToString(pubkey))
148 var payments []Payment
149
150 err := d.DB.View(
151 func(txn *badger.Txn) error {
152 it := txn.NewIterator(badger.DefaultIteratorOptions)
153 defer it.Close()
154
155 for it.Seek([]byte(prefix)); it.ValidForPrefix([]byte(prefix)); it.Next() {
156 key := string(it.Item().Key())
157 if !strings.HasSuffix(key, suffix) {
158 continue
159 }
160
161 err := it.Item().Value(
162 func(val []byte) error {
163 var payment Payment
164 err := json.Unmarshal(val, &payment)
165 if err != nil {
166 return err
167 }
168 payments = append(payments, payment)
169 return nil
170 },
171 )
172 if err != nil {
173 return err
174 }
175 }
176 return nil
177 },
178 )
179
180 return payments, err
181 }
182
183 // ExtendBlossomSubscription extends or creates a blossom subscription with service level
184 func (d *D) ExtendBlossomSubscription(
185 pubkey []byte, level string, storageMB int64, days int,
186 ) error {
187 if days <= 0 {
188 return fmt.Errorf("invalid days: %d", days)
189 }
190
191 key := fmt.Sprintf("sub:%s", hex.EncodeToString(pubkey))
192 now := time.Now()
193
194 return d.DB.Update(
195 func(txn *badger.Txn) error {
196 var sub Subscription
197 item, err := txn.Get([]byte(key))
198 if errors.Is(err, badger.ErrKeyNotFound) {
199 sub.PaidUntil = now.AddDate(0, 0, days)
200 } else if err != nil {
201 return err
202 } else {
203 err = item.Value(
204 func(val []byte) error {
205 return json.Unmarshal(val, &sub)
206 },
207 )
208 if err != nil {
209 return err
210 }
211 extendFrom := now
212 if !sub.PaidUntil.IsZero() && sub.PaidUntil.After(now) {
213 extendFrom = sub.PaidUntil
214 }
215 sub.PaidUntil = extendFrom.AddDate(0, 0, days)
216 }
217
218 // Set blossom service level and storage
219 sub.BlossomLevel = level
220 // Add storage quota (accumulate if subscription already exists)
221 if sub.BlossomStorage > 0 && sub.PaidUntil.After(now) {
222 // Add to existing quota
223 sub.BlossomStorage += storageMB
224 } else {
225 // Set new quota
226 sub.BlossomStorage = storageMB
227 }
228
229 data, err := json.Marshal(&sub)
230 if err != nil {
231 return err
232 }
233 return txn.Set([]byte(key), data)
234 },
235 )
236 }
237
238 // GetBlossomStorageQuota returns the current blossom storage quota in MB for a pubkey
239 func (d *D) GetBlossomStorageQuota(pubkey []byte) (quotaMB int64, err error) {
240 sub, err := d.GetSubscription(pubkey)
241 if err != nil {
242 return 0, err
243 }
244 if sub == nil {
245 return 0, nil
246 }
247 // Only return quota if subscription is active
248 if sub.PaidUntil.IsZero() || time.Now().After(sub.PaidUntil) {
249 return 0, nil
250 }
251 return sub.BlossomStorage, nil
252 }
253
254 // IsFirstTimeUser checks if a user is logging in for the first time and marks them as seen
255 func (d *D) IsFirstTimeUser(pubkey []byte) (bool, error) {
256 key := fmt.Sprintf("firstlogin:%s", hex.EncodeToString(pubkey))
257
258 isFirstTime := false
259 err := d.DB.Update(
260 func(txn *badger.Txn) error {
261 _, err := txn.Get([]byte(key))
262 if errors.Is(err, badger.ErrKeyNotFound) {
263 // First time - record the login
264 isFirstTime = true
265 now := time.Now()
266 data, err := json.Marshal(map[string]interface{}{
267 "first_login": now,
268 })
269 if err != nil {
270 return err
271 }
272 return txn.Set([]byte(key), data)
273 }
274 return err // Return any other error as-is
275 },
276 )
277
278 return isFirstTime, err
279 }
280