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