diff --git a/internal/models/types/auth.go b/internal/models/types/auth.go index bec5e65..1da3039 100644 --- a/internal/models/types/auth.go +++ b/internal/models/types/auth.go @@ -2,6 +2,11 @@ package types import "time" +// TokenRefresher is an interface for refreshing a token. +type TokenRefresher interface { + RefreshSession() error +} + // OAuthConsumer represents OAuth consumer credentials type OAuthConsumer struct { ConsumerKey string `json:"consumer_key"` @@ -25,4 +30,18 @@ type OAuth2Token struct { Scope string `json:"scope"` CreatedAt time.Time // Used for expiration tracking ExpiresAt time.Time // Computed expiration time -} \ No newline at end of file +} + +// Expired checks if token is expired +func (t *OAuth2Token) Expired() bool { + return time.Now().After(t.ExpiresAt) +} + +// RefreshIfNeeded refreshes token if expired +func (t *OAuth2Token) RefreshIfNeeded(client TokenRefresher) error { + if !t.Expired() { + return nil + } + + return client.RefreshSession() +} diff --git a/internal/models/types/types.go b/internal/models/types/types.go deleted file mode 100644 index 560de7e..0000000 --- a/internal/models/types/types.go +++ /dev/null @@ -1,469 +0,0 @@ -package types - -import ( - "fmt" - "strconv" - "strings" - "time" -) - -// TokenRefresher is an interface for refreshing a token. -type TokenRefresher interface { - RefreshSession() error -} - -// OAuthConsumer represents OAuth consumer credentials -type OAuthConsumer struct { - ConsumerKey string `json:"consumer_key"` - ConsumerSecret string `json:"consumer_secret"` -} - -// OAuth1Token represents OAuth1 token response -type OAuth1Token struct { - OAuthToken string `json:"oauth_token"` - OAuthTokenSecret string `json:"oauth_token_secret"` - MFAToken string `json:"mfa_token,omitempty"` - Domain string `json:"domain"` -} - -// OAuth2Token represents OAuth2 token response -type OAuth2Token struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn int `json:"expires_in"` - RefreshToken string `json:"refresh_token"` - Scope string `json:"scope"` - ExpiresAt time.Time `json:"expires_at"` -} - -// SessionData represents the data stored in a session file -type SessionData struct { - Domain string `json:"domain"` - Username string `json:"username"` - AuthToken string `json:"auth_token"` - OAuth2Token *OAuth2Token `json:"oauth2_token"` -} - -// Expired checks if token is expired -func (t *OAuth2Token) Expired() bool { - return time.Now().After(t.ExpiresAt) -} - -// RefreshIfNeeded refreshes token if expired -func (t *OAuth2Token) RefreshIfNeeded(client TokenRefresher) error { - if !t.Expired() { - return nil - } - - return client.RefreshSession() -} - -var ( - // Default location for conversions (set to UTC by default) - defaultLocation *time.Location -) - -func init() { - var err error - defaultLocation, err = time.LoadLocation("UTC") - if err != nil { - panic(err) - } -} - -// ParseTimestamp converts a millisecond timestamp to time.Time in default location -func ParseTimestamp(ts int) time.Time { - return time.Unix(0, int64(ts)*int64(time.Millisecond)).In(defaultLocation) -} - -// parseAggregationKey is a helper function to parse aggregation key back to a time.Time object -func ParseAggregationKey(key, aggregate string) time.Time { - switch aggregate { - case "day": - t, _ := time.Parse("2006-01-02", key) - return t - case "week": - year, _ := strconv.Atoi(key[:4]) - week, _ := strconv.Atoi(key[6:]) - t := time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC) - // Find the first Monday of the year - for t.Weekday() != time.Monday { - t = t.AddDate(0, 0, 1) - } - // Add weeks - return t.AddDate(0, 0, (week-1)*7) - case "month": - t, _ := time.Parse("2006-01", key) - return t - case "year": - t, _ := time.Parse("2006", key) - return t - } - return time.Time{} -} - -// GarminTime represents Garmin's timestamp format with custom JSON parsing -type GarminTime struct { - time.Time -} - -// UnmarshalJSON implements the json.Unmarshaler interface. -// It parses Garmin's specific timestamp format. -func (gt *GarminTime) UnmarshalJSON(b []byte) (err error) { - s := strings.Trim(string(b), `"`) - if s == "null" { - return nil - } - - // Try parsing with milliseconds (e.g., "2018-09-01T00:13:25.000") - // Garmin sometimes returns .0 for milliseconds, which Go's time.Parse handles as .000 - // The 'Z' in the layout indicates a UTC time without a specific offset, which is often how these are interpreted. - // If the input string does not contain 'Z', it will be parsed as local time. - // For consistency, we'll assume UTC if no timezone is specified. - layouts := []string{ - "2006-01-02T15:04:05.0", // Example: 2018-09-01T00:13:25.0 - "2006-01-02T15:04:05", // Example: 2018-09-01T00:13:25 - "2006-01-02 15:04:05", // Example: 2025-09-18 15:14:40 - "2006-01-02", // Example: 2018-09-01 - } - - for _, layout := range layouts { - if t, err := time.Parse(layout, s); err == nil { - gt.Time = t - return nil - } - } - - return fmt.Errorf("cannot parse %q into a GarminTime", s) -} - - -// ActivityType represents the type of activity -type ActivityType struct { - TypeID int `json:"typeId"` - TypeKey string `json:"typeKey"` - ParentTypeID *int `json:"parentTypeId,omitempty"` -} - -// EventType represents the event type of an activity -type EventType struct { - TypeID int `json:"typeId"` - TypeKey string `json:"typeKey"` -} - -// Activity represents a Garmin Connect activity -type Activity struct { - ActivityID int64 `json:"activityId"` - ActivityName string `json:"activityName"` - Description string `json:"description"` - StartTimeLocal GarminTime `json:"startTimeLocal"` - StartTimeGMT GarminTime `json:"startTimeGMT"` - ActivityType ActivityType `json:"activityType"` - EventType EventType `json:"eventType"` - Distance float64 `json:"distance"` - Duration float64 `json:"duration"` - ElapsedDuration float64 `json:"elapsedDuration"` - MovingDuration float64 `json:"movingDuration"` - ElevationGain float64 `json:"elevationGain"` - ElevationLoss float64 `json:"elevationLoss"` - AverageSpeed float64 `json:"averageSpeed"` - MaxSpeed float64 `json:"maxSpeed"` - Calories float64 `json:"calories"` - AverageHR float64 `json:"averageHR"` - MaxHR float64 `json:"maxHR"` -} - -// UserProfile represents a Garmin user profile -type UserProfile struct { - UserName string `json:"userName"` - DisplayName string `json:"displayName"` - LevelUpdateDate GarminTime `json:"levelUpdateDate"` - // Add other fields as needed from API response -} - -// VO2MaxData represents VO2 max data -type VO2MaxData struct { - Date time.Time `json:"calendarDate"` - VO2MaxRunning *float64 `json:"vo2MaxRunning"` - VO2MaxCycling *float64 `json:"vo2MaxCycling"` - UserProfilePK int `json:"userProfilePk"` -} - -// Add these new structs -type VO2MaxEntry struct { - Value float64 `json:"value"` - ActivityType string `json:"activityType"` // "running" or "cycling" - Date time.Time `json:"date"` - Source string `json:"source"` // "user_settings", "activity", etc. -} - -type VO2Max struct { - Value float64 `json:"vo2Max"` - FitnessLevel string `json:"fitnessLevel"` - UpdatedDate time.Time `json:"date"` -} - -// VO2MaxProfile represents the current VO2 max profile from user settings -type VO2MaxProfile struct { - UserProfilePK int `json:"userProfilePk"` - LastUpdated time.Time `json:"lastUpdated"` - Running *VO2MaxEntry `json:"running,omitempty"` - Cycling *VO2MaxEntry `json:"cycling,omitempty"` -} - -// SleepLevel represents different sleep stages -type SleepLevel struct { - StartGMT time.Time `json:"startGmt"` - EndGMT time.Time `json:"endGmt"` - ActivityLevel float64 `json:"activityLevel"` - SleepLevel string `json:"sleepLevel"` // "deep", "light", "rem", "awake" -} - -// SleepMovement represents movement during sleep -type SleepMovement struct { - StartGMT time.Time `json:"startGmt"` - EndGMT time.Time `json:"endGmt"` - ActivityLevel float64 `json:"activityLevel"` -} - -// SleepScore represents detailed sleep scoring -type SleepScore struct { - Overall int `json:"overall"` - Composition SleepScoreBreakdown `json:"composition"` - Revitalization SleepScoreBreakdown `json:"revitalization"` - Duration SleepScoreBreakdown `json:"duration"` - DeepPercentage float64 `json:"deepPercentage"` - LightPercentage float64 `json:"lightPercentage"` - RemPercentage float64 `json:"remPercentage"` - RestfulnessValue float64 `json:"restfulnessValue"` -} - -type SleepScoreBreakdown struct { - QualifierKey string `json:"qualifierKey"` - OptimalStart float64 `json:"optimalStart"` - OptimalEnd float64 `json:"optimalEnd"` - Value float64 `json:"value"` - IdealStartSecs *int `json:"idealStartInSeconds"` - IdealEndSecs *int `json:"idealEndInSeconds"` -} - -// DetailedSleepData represents comprehensive sleep data -type DetailedSleepData struct { - UserProfilePK int `json:"userProfilePk"` - CalendarDate time.Time `json:"calendarDate"` - SleepStartTimestampGMT time.Time `json:"sleepStartTimestampGmt"` - SleepEndTimestampGMT time.Time `json:"sleepEndTimestampGmt"` - SleepStartTimestampLocal time.Time `json:"sleepStartTimestampLocal"` - SleepEndTimestampLocal time.Time `json:"sleepEndTimestampLocal"` - UnmeasurableSleepSeconds int `json:"unmeasurableSleepSeconds"` - DeepSleepSeconds int `json:"deepSleepSeconds"` - LightSleepSeconds int `json:"lightSleepSeconds"` - RemSleepSeconds int `json:"remSleepSeconds"` - AwakeSleepSeconds int `json:"awakeSleepSeconds"` - DeviceRemCapable bool `json:"deviceRemCapable"` - SleepLevels []SleepLevel `json:"sleepLevels"` - SleepMovement []SleepMovement `json:"sleepMovement"` - SleepScores *SleepScore `json:"sleepScores"` - AverageSpO2Value *float64 `json:"averageSpO2Value"` - LowestSpO2Value *int `json:"lowestSpO2Value"` - HighestSpO2Value *int `json:"highestSpO2Value"` - AverageRespirationValue *float64 `json:"averageRespirationValue"` - LowestRespirationValue *float64 `json:"lowestRespirationValue"` - HighestRespirationValue *float64 `json:"highestRespirationValue"` - AvgSleepStress *float64 `json:"avgSleepStress"` -} - -// HRVBaseline represents HRV baseline data -type HRVBaseline struct { - LowUpper int `json:"lowUpper"` - BalancedLow int `json:"balancedLow"` - BalancedUpper int `json:"balancedUpper"` - MarkerValue float64 `json:"markerValue"` -} - -// DailyHRVData represents comprehensive daily HRV data -type DailyHRVData struct { - UserProfilePK int `json:"userProfilePk"` - CalendarDate time.Time `json:"calendarDate"` - WeeklyAvg *float64 `json:"weeklyAvg"` - LastNightAvg *float64 `json:"lastNightAvg"` - LastNight5MinHigh *float64 `json:"lastNight5MinHigh"` - Baseline HRVBaseline `json:"baseline"` - Status string `json:"status"` - FeedbackPhrase string `json:"feedbackPhrase"` - CreateTimeStamp time.Time `json:"createTimeStamp"` - HRVReadings []HRVReading `json:"hrvReadings"` - StartTimestampGMT time.Time `json:"startTimestampGmt"` - EndTimestampGMT time.Time `json:"endTimestampGmt"` - StartTimestampLocal time.Time `json:"startTimestampLocal"` - EndTimestampLocal time.Time `json:"endTimestampLocal"` - SleepStartTimestampGMT time.Time `json:"sleepStartTimestampGmt"` - SleepEndTimestampGMT time.Time `json:"sleepEndTimestampGmt"` -} - -// BodyBatteryEvent represents events that impact Body Battery -type BodyBatteryEvent struct { - EventType string `json:"eventType"` // "sleep", "activity", "stress" - EventStartTimeGMT time.Time `json:"eventStartTimeGmt"` - TimezoneOffset int `json:"timezoneOffset"` - DurationInMilliseconds int `json:"durationInMilliseconds"` - BodyBatteryImpact int `json:"bodyBatteryImpact"` - FeedbackType string `json:"feedbackType"` - ShortFeedback string `json:"shortFeedback"` -} - -// DetailedBodyBatteryData represents comprehensive Body Battery data -type DetailedBodyBatteryData struct { - UserProfilePK int `json:"userProfilePk"` - CalendarDate time.Time `json:"calendarDate"` - StartTimestampGMT time.Time `json:"startTimestampGmt"` - EndTimestampGMT time.Time `json:"endTimestampGmt"` - StartTimestampLocal time.Time `json:"startTimestampLocal"` - EndTimestampLocal time.Time `json:"endTimestampLocal"` - MaxStressLevel int `json:"maxStressLevel"` - AvgStressLevel int `json:"avgStressLevel"` - BodyBatteryValuesArray [][]interface{} `json:"bodyBatteryValuesArray"` - StressValuesArray [][]int `json:"stressValuesArray"` - Events []BodyBatteryEvent `json:"bodyBatteryEvents"` -} - -// TrainingStatus represents current training status -type TrainingStatus struct { - CalendarDate time.Time `json:"calendarDate"` - TrainingStatusKey string `json:"trainingStatusKey"` // "DETRAINING", "RECOVERY", "MAINTAINING", "PRODUCTIVE", "PEAKING", "OVERREACHING", "UNPRODUCTIVE", "NONE" - TrainingStatusTypeKey string `json:"trainingStatusTypeKey"` - TrainingStatusValue int `json:"trainingStatusValue"` - LoadRatio float64 `json:"loadRatio"` -} - -// TrainingLoad represents training load data -type TrainingLoad struct { - CalendarDate time.Time `json:"calendarDate"` - AcuteTrainingLoad float64 `json:"acuteTrainingLoad"` - ChronicTrainingLoad float64 `json:"chronicTrainingLoad"` - TrainingLoadRatio float64 `json:"trainingLoadRatio"` - TrainingEffectAerobic float64 `json:"trainingEffectAerobic"` - TrainingEffectAnaerobic float64 `json:"trainingEffectAnaerobic"` -} - -// FitnessAge represents fitness age calculation -type FitnessAge struct { - FitnessAge int `json:"fitnessAge"` - ChronologicalAge int `json:"chronologicalAge"` - VO2MaxRunning float64 `json:"vo2MaxRunning"` - LastUpdated time.Time `json:"lastUpdated"` -} - -// HeartRateZones represents heart rate zone data -type HeartRateZones struct { - RestingHR int `json:"resting_hr"` - MaxHR int `json:"max_hr"` - LactateThreshold int `json:"lactate_threshold"` - Zones []HRZone `json:"zones"` - UpdatedAt time.Time `json:"updated_at"` -} - -// HRZone represents a single heart rate zone -type HRZone struct { - Zone int `json:"zone"` - MinBPM int `json:"min_bpm"` - MaxBPM int `json:"max_bpm"` - Name string `json:"name"` -} - -// WellnessData represents additional wellness metrics -type WellnessData struct { - Date time.Time `json:"calendarDate"` - RestingHR *int `json:"resting_hr"` - Weight *float64 `json:"weight"` - BodyFat *float64 `json:"body_fat"` - BMI *float64 `json:"bmi"` - BodyWater *float64 `json:"body_water"` - BoneMass *float64 `json:"bone_mass"` - MuscleMass *float64 `json:"muscle_mass"` - // Add more fields as needed -} - -// SleepData represents sleep summary data -type SleepData struct { - Date time.Time `json:"calendarDate"` - SleepScore int `json:"sleepScore"` - TotalSleepSeconds int `json:"totalSleepSeconds"` - DeepSleepSeconds int `json:"deepSleepSeconds"` - LightSleepSeconds int `json:"lightSleepSeconds"` - RemSleepSeconds int `json:"remSleepSeconds"` - AwakeSleepSeconds int `json:"awakeSleepSeconds"` - // Add more fields as needed -} - -// HrvData represents Heart Rate Variability data -type HrvData struct { - Date time.Time `json:"calendarDate"` - HrvValue float64 `json:"hrvValue"` - // Add more fields as needed -} - -// HRVStatus represents HRV status and baseline -type HRVStatus struct { - Status string `json:"status"` // "BALANCED", "UNBALANCED", "POOR" - FeedbackPhrase string `json:"feedbackPhrase"` - BaselineLowUpper int `json:"baselineLowUpper"` - BalancedLow int `json:"balancedLow"` - BalancedUpper int `json:"balancedUpper"` - MarkerValue float64 `json:"markerValue"` -} - -// HRVReading represents an individual HRV reading -type HRVReading struct { - Timestamp int `json:"timestamp"` - StressLevel int `json:"stressLevel"` - HeartRate int `json:"heartRate"` - RRInterval int `json:"rrInterval"` - Status string `json:"status"` - SignalQuality float64 `json:"signalQuality"` -} - -// TimestampAsTime converts the reading timestamp to time.Time using timeutils -func (r *HRVReading) TimestampAsTime() time.Time { - return ParseTimestamp(r.Timestamp) -} - -// RRSeconds converts the RR interval to seconds -func (r *HRVReading) RRSeconds() float64 { - return float64(r.RRInterval) / 1000.0 -} - -// StressData represents stress level data -type StressData struct { - Date time.Time `json:"calendarDate"` - StressLevel int `json:"stressLevel"` - RestStressLevel int `json:"restStressLevel"` - // Add more fields as needed -} - -// BodyBatteryData represents Body Battery data -type BodyBatteryData struct { - Date time.Time `json:"calendarDate"` - BatteryLevel int `json:"batteryLevel"` - Charge int `json:"charge"` - Drain int `json:"drain"` - // Add more fields as needed -} - -// StepsData represents steps statistics -type StepsData struct { - Date time.Time `json:"calendarDate"` - Steps int `json:"steps"` -} - -// DistanceData represents distance statistics -type DistanceData struct { - Date time.Time `json:"calendarDate"` - Distance float64 `json:"distance"` // in meters -} - -// CaloriesData represents calories statistics -type CaloriesData struct { - Date time.Time `json:"calendarDate"` - Calories int `json:"activeCalories"` -}