package account import ( "context" "errors" "fmt" "strings" "time" "unicode/utf8" "github.com/go-jet/jet/v2/postgres" "github.com/go-jet/jet/v2/qrm" "github.com/google/uuid" "scrabble/backend/internal/postgres/jet/backend/model" "scrabble/backend/internal/postgres/jet/backend/table" ) // maxDisplayName caps a display name's length in runes. const maxDisplayName = 64 // ErrInvalidProfile is returned when a profile update carries an unacceptable // field (an unknown language, an invalid timezone, or an over-long display name). var ErrInvalidProfile = errors.New("account: invalid profile") // ProfileUpdate is the full set of player-editable profile fields. UpdateProfile // overwrites every field, so callers send the complete desired profile. AwayStart // and AwayEnd carry only the hour and minute of the daily away window, in the // account's TimeZone. type ProfileUpdate struct { DisplayName string PreferredLanguage string // "en" or "ru" TimeZone string // an IANA location name AwayStart time.Time AwayEnd time.Time BlockChat bool BlockFriendRequests bool } // UpdateProfile validates and overwrites the editable fields of the account, then // returns the stored row. It reports ErrInvalidProfile for a bad language, // timezone or display name and ErrNotFound when no account matches id. func (s *Store) UpdateProfile(ctx context.Context, id uuid.UUID, p ProfileUpdate) (Account, error) { lang := strings.TrimSpace(p.PreferredLanguage) if lang != "en" && lang != "ru" { return Account{}, fmt.Errorf("%w: preferred_language %q", ErrInvalidProfile, p.PreferredLanguage) } tz := strings.TrimSpace(p.TimeZone) if _, err := time.LoadLocation(tz); err != nil { return Account{}, fmt.Errorf("%w: time_zone %q: %v", ErrInvalidProfile, p.TimeZone, err) } name := strings.TrimSpace(p.DisplayName) if utf8.RuneCountInString(name) > maxDisplayName { return Account{}, fmt.Errorf("%w: display name exceeds %d characters", ErrInvalidProfile, maxDisplayName) } stmt := table.Accounts.UPDATE( table.Accounts.DisplayName, table.Accounts.PreferredLanguage, table.Accounts.TimeZone, table.Accounts.AwayStart, table.Accounts.AwayEnd, table.Accounts.BlockChat, table.Accounts.BlockFriendRequests, table.Accounts.UpdatedAt, ).SET( postgres.String(name), postgres.String(lang), postgres.String(tz), postgres.TimeT(p.AwayStart), postgres.TimeT(p.AwayEnd), postgres.Bool(p.BlockChat), postgres.Bool(p.BlockFriendRequests), postgres.TimestampzT(time.Now().UTC()), ).WHERE(table.Accounts.AccountID.EQ(postgres.UUID(id))). RETURNING(table.Accounts.AllColumns) var row model.Accounts if err := stmt.QueryContext(ctx, s.db, &row); err != nil { if errors.Is(err, qrm.ErrNoRows) { return Account{}, ErrNotFound } return Account{}, fmt.Errorf("account: update profile %s: %w", id, err) } return modelToAccount(row), nil }