- Introduced the `initializeBlossomServer` function to set up the Blossom blob storage server with dynamic base URL handling and ACL configuration. - Implemented the `blossomHandler` method to manage incoming requests to the Blossom API, ensuring proper URL handling and context management. - Enhanced the `PaymentProcessor` to support Blossom service levels, allowing for subscription extensions based on payment metadata. - Added methods for parsing and validating Blossom service levels, including storage quota management and subscription extension logic. - Updated the configuration to include Blossom service level settings, facilitating dynamic service level management. - Integrated storage quota checks in the blob upload process to prevent exceeding allocated limits. - Refactored existing code to improve organization and maintainability, including the removal of unused blob directory configurations. - Added tests to ensure the robustness of new functionalities and maintain existing behavior across blob operations.
292 lines
6.6 KiB
Go
292 lines
6.6 KiB
Go
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
|
|
}
|