You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
291 lines
6.6 KiB
291 lines
6.6 KiB
package database |
|
|
|
import ( |
|
"encoding/hex" |
|
"errors" |
|
"fmt" |
|
"strings" |
|
"time" |
|
|
|
"encoding/json" |
|
|
|
"github.com/dgraph-io/badger/v4" |
|
) |
|
|
|
type Subscription struct { |
|
TrialEnd time.Time `json:"trial_end"` |
|
PaidUntil time.Time `json:"paid_until"` |
|
BlossomLevel string `json:"blossom_level,omitempty"` // Service level name (e.g., "basic", "premium") |
|
BlossomStorage int64 `json:"blossom_storage,omitempty"` // Storage quota in MB |
|
} |
|
|
|
func (d *D) GetSubscription(pubkey []byte) (*Subscription, error) { |
|
key := fmt.Sprintf("sub:%s", hex.EncodeToString(pubkey)) |
|
var sub *Subscription |
|
|
|
err := d.DB.View( |
|
func(txn *badger.Txn) error { |
|
item, err := txn.Get([]byte(key)) |
|
if errors.Is(err, badger.ErrKeyNotFound) { |
|
return nil |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
return item.Value( |
|
func(val []byte) error { |
|
sub = &Subscription{} |
|
return json.Unmarshal(val, sub) |
|
}, |
|
) |
|
}, |
|
) |
|
return sub, err |
|
} |
|
|
|
func (d *D) IsSubscriptionActive(pubkey []byte) (bool, error) { |
|
key := fmt.Sprintf("sub:%s", hex.EncodeToString(pubkey)) |
|
now := time.Now() |
|
active := false |
|
|
|
err := d.DB.Update( |
|
func(txn *badger.Txn) error { |
|
item, err := txn.Get([]byte(key)) |
|
if errors.Is(err, badger.ErrKeyNotFound) { |
|
sub := &Subscription{TrialEnd: now.AddDate(0, 0, 30)} |
|
data, err := json.Marshal(sub) |
|
if err != nil { |
|
return err |
|
} |
|
active = true |
|
return txn.Set([]byte(key), data) |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
|
|
var sub Subscription |
|
err = item.Value( |
|
func(val []byte) error { |
|
return json.Unmarshal(val, &sub) |
|
}, |
|
) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
active = now.Before(sub.TrialEnd) || (!sub.PaidUntil.IsZero() && now.Before(sub.PaidUntil)) |
|
return nil |
|
}, |
|
) |
|
return active, err |
|
} |
|
|
|
func (d *D) ExtendSubscription(pubkey []byte, days int) error { |
|
if days <= 0 { |
|
return fmt.Errorf("invalid days: %d", days) |
|
} |
|
|
|
key := fmt.Sprintf("sub:%s", hex.EncodeToString(pubkey)) |
|
now := time.Now() |
|
|
|
return d.DB.Update( |
|
func(txn *badger.Txn) error { |
|
var sub Subscription |
|
item, err := txn.Get([]byte(key)) |
|
if errors.Is(err, badger.ErrKeyNotFound) { |
|
sub.PaidUntil = now.AddDate(0, 0, days) |
|
} else if err != nil { |
|
return err |
|
} else { |
|
err = item.Value( |
|
func(val []byte) error { |
|
return json.Unmarshal(val, &sub) |
|
}, |
|
) |
|
if err != nil { |
|
return err |
|
} |
|
extendFrom := now |
|
if !sub.PaidUntil.IsZero() && sub.PaidUntil.After(now) { |
|
extendFrom = sub.PaidUntil |
|
} |
|
sub.PaidUntil = extendFrom.AddDate(0, 0, days) |
|
} |
|
|
|
data, err := json.Marshal(&sub) |
|
if err != nil { |
|
return err |
|
} |
|
return txn.Set([]byte(key), data) |
|
}, |
|
) |
|
} |
|
|
|
type Payment struct { |
|
Amount int64 `json:"amount"` |
|
Timestamp time.Time `json:"timestamp"` |
|
Invoice string `json:"invoice"` |
|
Preimage string `json:"preimage"` |
|
} |
|
|
|
func (d *D) RecordPayment( |
|
pubkey []byte, amount int64, invoice, preimage string, |
|
) error { |
|
now := time.Now() |
|
key := fmt.Sprintf("payment:%d:%s", now.Unix(), hex.EncodeToString(pubkey)) |
|
|
|
payment := Payment{ |
|
Amount: amount, |
|
Timestamp: now, |
|
Invoice: invoice, |
|
Preimage: preimage, |
|
} |
|
|
|
data, err := json.Marshal(&payment) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
return d.DB.Update( |
|
func(txn *badger.Txn) error { |
|
return txn.Set([]byte(key), data) |
|
}, |
|
) |
|
} |
|
|
|
func (d *D) GetPaymentHistory(pubkey []byte) ([]Payment, error) { |
|
prefix := fmt.Sprintf("payment:") |
|
suffix := fmt.Sprintf(":%s", hex.EncodeToString(pubkey)) |
|
var payments []Payment |
|
|
|
err := d.DB.View( |
|
func(txn *badger.Txn) error { |
|
it := txn.NewIterator(badger.DefaultIteratorOptions) |
|
defer it.Close() |
|
|
|
for it.Seek([]byte(prefix)); it.ValidForPrefix([]byte(prefix)); it.Next() { |
|
key := string(it.Item().Key()) |
|
if !strings.HasSuffix(key, suffix) { |
|
continue |
|
} |
|
|
|
err := it.Item().Value( |
|
func(val []byte) error { |
|
var payment Payment |
|
err := json.Unmarshal(val, &payment) |
|
if err != nil { |
|
return err |
|
} |
|
payments = append(payments, payment) |
|
return nil |
|
}, |
|
) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
}, |
|
) |
|
|
|
return payments, err |
|
} |
|
|
|
// ExtendBlossomSubscription extends or creates a blossom subscription with service level |
|
func (d *D) ExtendBlossomSubscription( |
|
pubkey []byte, level string, storageMB int64, days int, |
|
) error { |
|
if days <= 0 { |
|
return fmt.Errorf("invalid days: %d", days) |
|
} |
|
|
|
key := fmt.Sprintf("sub:%s", hex.EncodeToString(pubkey)) |
|
now := time.Now() |
|
|
|
return d.DB.Update( |
|
func(txn *badger.Txn) error { |
|
var sub Subscription |
|
item, err := txn.Get([]byte(key)) |
|
if errors.Is(err, badger.ErrKeyNotFound) { |
|
sub.PaidUntil = now.AddDate(0, 0, days) |
|
} else if err != nil { |
|
return err |
|
} else { |
|
err = item.Value( |
|
func(val []byte) error { |
|
return json.Unmarshal(val, &sub) |
|
}, |
|
) |
|
if err != nil { |
|
return err |
|
} |
|
extendFrom := now |
|
if !sub.PaidUntil.IsZero() && sub.PaidUntil.After(now) { |
|
extendFrom = sub.PaidUntil |
|
} |
|
sub.PaidUntil = extendFrom.AddDate(0, 0, days) |
|
} |
|
|
|
// Set blossom service level and storage |
|
sub.BlossomLevel = level |
|
// Add storage quota (accumulate if subscription already exists) |
|
if sub.BlossomStorage > 0 && sub.PaidUntil.After(now) { |
|
// Add to existing quota |
|
sub.BlossomStorage += storageMB |
|
} else { |
|
// Set new quota |
|
sub.BlossomStorage = storageMB |
|
} |
|
|
|
data, err := json.Marshal(&sub) |
|
if err != nil { |
|
return err |
|
} |
|
return txn.Set([]byte(key), data) |
|
}, |
|
) |
|
} |
|
|
|
// GetBlossomStorageQuota returns the current blossom storage quota in MB for a pubkey |
|
func (d *D) GetBlossomStorageQuota(pubkey []byte) (quotaMB int64, err error) { |
|
sub, err := d.GetSubscription(pubkey) |
|
if err != nil { |
|
return 0, err |
|
} |
|
if sub == nil { |
|
return 0, nil |
|
} |
|
// Only return quota if subscription is active |
|
if sub.PaidUntil.IsZero() || time.Now().After(sub.PaidUntil) { |
|
return 0, nil |
|
} |
|
return sub.BlossomStorage, nil |
|
} |
|
|
|
// IsFirstTimeUser checks if a user is logging in for the first time and marks them as seen |
|
func (d *D) IsFirstTimeUser(pubkey []byte) (bool, error) { |
|
key := fmt.Sprintf("firstlogin:%s", hex.EncodeToString(pubkey)) |
|
|
|
isFirstTime := false |
|
err := d.DB.Update( |
|
func(txn *badger.Txn) error { |
|
_, err := txn.Get([]byte(key)) |
|
if errors.Is(err, badger.ErrKeyNotFound) { |
|
// First time - record the login |
|
isFirstTime = true |
|
now := time.Now() |
|
data, err := json.Marshal(map[string]interface{}{ |
|
"first_login": now, |
|
}) |
|
if err != nil { |
|
return err |
|
} |
|
return txn.Set([]byte(key), data) |
|
} |
|
return err // Return any other error as-is |
|
}, |
|
) |
|
|
|
return isFirstTime, err |
|
}
|
|
|