package database import ( "encoding/hex" "errors" "fmt" "strings" "time" "github.com/dgraph-io/badger/v4" "next.orly.dev/pkg/encoders/json" ) type Subscription struct { TrialEnd time.Time `json:"trial_end"` PaidUntil time.Time `json:"paid_until"` } 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 } // 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 }