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_` 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 }