Files
Nip-05-api/internal/user/model.go
2026-04-29 02:35:00 +00:00

75 lines
1.9 KiB
Go

package user
import (
"errors"
"regexp"
"strings"
"time"
)
type SubscriptionType string
const (
SubYearly SubscriptionType = "yearly"
SubLifetime SubscriptionType = "lifetime"
)
func (s SubscriptionType) Valid() bool {
return s == SubYearly || s == SubLifetime
}
type User struct {
ID int64
Pubkey string
Username string
SubscriptionType SubscriptionType
ExpiresAt *time.Time
IsActive bool
ManualUsername bool
LastSyncedAt *time.Time
ExpiringReminderSentAt *time.Time
DeactivatedAt *time.Time
CreatedAt time.Time
}
func (u *User) IsLifetime() bool { return u.SubscriptionType == SubLifetime }
func (u *User) InGrace() bool { return !u.IsActive && u.DeactivatedAt != nil }
var (
ErrInvalidUsername = errors.New("invalid username")
ErrUserNotFound = errors.New("user not found")
ErrUsernameTaken = errors.New("username taken")
)
// Username rules: 1-30 chars, [a-z0-9_-], lowercase, must start with alnum.
var usernameRE = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,29}$`)
func ValidateUsername(name string, reserved []string) error {
name = strings.ToLower(strings.TrimSpace(name))
if !usernameRE.MatchString(name) {
return ErrInvalidUsername
}
for _, r := range reserved {
if strings.EqualFold(name, strings.TrimSpace(r)) {
return ErrInvalidUsername
}
}
return nil
}
func NormalizeUsername(name string) string {
return strings.ToLower(strings.TrimSpace(name))
}
// ProvisionalUsername returns a deterministic placeholder handle for a pubkey
// when the caller did not supply one. The format `u_<first 16 hex>` keeps the
// result inside the 30-char username rule and matches usernameRE.
func ProvisionalUsername(pubkey string) string {
hex := strings.ToLower(strings.TrimSpace(pubkey))
if len(hex) > 16 {
hex = hex[:16]
}
return "u_" + hex
}