mirror of
https://github.com/sstent/go-garth.git
synced 2025-12-06 08:01:42 +00:00
reworked api interfaces
This commit is contained in:
98
README.md
98
README.md
@@ -25,35 +25,101 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = client.Login("your@email.com", "password")
|
err = client.Login("your@email.com", "password")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get yesterday's body battery data (detailed)
|
// List recent activities with filtering
|
||||||
yesterday := time.Now().AddDate(0, 0, -1)
|
opts := garmin.ActivityOptions{
|
||||||
bb, err := client.GetBodyBatteryData(yesterday)
|
Limit: 10,
|
||||||
|
Offset: 0,
|
||||||
|
ActivityType: "running", // optional filter
|
||||||
|
DateFrom: time.Now().AddDate(0, 0, -30), // last 30 days
|
||||||
|
DateTo: time.Now(),
|
||||||
|
}
|
||||||
|
activities, err := client.ListActivities(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if bb != nil {
|
|
||||||
fmt.Printf("Body Battery: %d\n", bb.BodyBatteryValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get weekly steps
|
for _, activity := range activities {
|
||||||
steps := garmin.NewDailySteps()
|
fmt.Printf("%s: %s (%.2f km)\n",
|
||||||
stepData, err := steps.List(time.Now(), 7, client)
|
activity.StartTimeLocal.Format("2006-01-02"),
|
||||||
|
activity.ActivityName,
|
||||||
|
activity.Distance/1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get detailed activity information
|
||||||
|
if len(activities) > 0 {
|
||||||
|
activityDetail, err := client.GetActivity(activities[0].ActivityID)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Activity details: %+v\n", activityDetail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for activities
|
||||||
|
searchResults, err := client.SearchActivities("morning run")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
fmt.Printf("Found %d activities matching search\n", len(searchResults))
|
||||||
for _, s := range stepData {
|
|
||||||
fmt.Printf("%s: %d steps\n",
|
// Get fitness age
|
||||||
s.(garmin.DailySteps).CalendarDate.Format("2006-01-02"),
|
fitnessAge, err := client.GetFitnessAge()
|
||||||
*s.(garmin.DailySteps).TotalSteps)
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
|
fmt.Printf("Fitness Age: %d (Chronological: %d)\n",
|
||||||
|
fitnessAge.FitnessAge, fitnessAge.ChronologicalAge)
|
||||||
|
|
||||||
|
// Get health data ranges
|
||||||
|
start := time.Now().AddDate(0, 0, -7)
|
||||||
|
end := time.Now()
|
||||||
|
|
||||||
|
sleepData, err := client.GetSleepData(start, end)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Sleep records: %d\n", len(sleepData))
|
||||||
|
|
||||||
|
hrvData, err := client.GetHrvData(start, end)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("HRV records: %d\n", len(hrvData))
|
||||||
|
|
||||||
|
stressData, err := client.GetStressData(start, end)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Stress records: %d\n", len(stressData))
|
||||||
|
|
||||||
|
bodyBatteryData, err := client.GetBodyBatteryData(start, end)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Body Battery records: %d\n", len(bodyBatteryData))
|
||||||
|
|
||||||
|
stepsData, err := client.GetStepsData(start, end)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Steps records: %d\n", len(stepsData))
|
||||||
|
|
||||||
|
distanceData, err := client.GetDistanceData(start, end)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Distance records: %d\n", len(distanceData))
|
||||||
|
|
||||||
|
caloriesData, err := client.GetCaloriesData(start, end)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Calories records: %d\n", len(caloriesData))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package garmin
|
package garmin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -8,8 +9,9 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
internalClient "github.com/sstent/go-garth/pkg/garth/client"
|
|
||||||
"github.com/sstent/go-garth/internal/errors"
|
"github.com/sstent/go-garth/internal/errors"
|
||||||
|
internalClient "github.com/sstent/go-garth/pkg/garth/client"
|
||||||
|
garth "github.com/sstent/go-garth/pkg/garth/types"
|
||||||
shared "github.com/sstent/go-garth/shared/interfaces"
|
shared "github.com/sstent/go-garth/shared/interfaces"
|
||||||
models "github.com/sstent/go-garth/shared/models"
|
models "github.com/sstent/go-garth/shared/models"
|
||||||
)
|
)
|
||||||
@@ -81,9 +83,7 @@ func (c *Client) RefreshSession() error {
|
|||||||
|
|
||||||
// ListActivities retrieves recent activities
|
// ListActivities retrieves recent activities
|
||||||
func (c *Client) ListActivities(opts ActivityOptions) ([]Activity, error) {
|
func (c *Client) ListActivities(opts ActivityOptions) ([]Activity, error) {
|
||||||
// TODO: Map ActivityOptions to internalClient.Client.GetActivities parameters
|
internalActivities, err := c.Client.GetActivitiesWithOptions(opts.Limit, opts.Offset, opts.ActivityType, opts.DateFrom, opts.DateTo)
|
||||||
// For now, just call the internal client's GetActivities with a dummy limit
|
|
||||||
internalActivities, err := c.Client.GetActivities(opts.Limit)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -104,8 +104,33 @@ func (c *Client) ListActivities(opts ActivityOptions) ([]Activity, error) {
|
|||||||
|
|
||||||
// GetActivity retrieves details for a specific activity ID
|
// GetActivity retrieves details for a specific activity ID
|
||||||
func (c *Client) GetActivity(activityID int) (*ActivityDetail, error) {
|
func (c *Client) GetActivity(activityID int) (*ActivityDetail, error) {
|
||||||
// TODO: Implement internalClient.Client.GetActivity
|
path := fmt.Sprintf("/activity-service/activity/%d", activityID)
|
||||||
return nil, fmt.Errorf("not implemented")
|
|
||||||
|
data, err := c.Client.ConnectAPI(path, "GET", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get activity details: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil, fmt.Errorf("activity not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var activity garth.Activity
|
||||||
|
if err := json.Unmarshal(data, &activity); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse activity response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ActivityDetail{
|
||||||
|
Activity: Activity{
|
||||||
|
ActivityID: activity.ActivityID,
|
||||||
|
ActivityName: activity.ActivityName,
|
||||||
|
ActivityType: ActivityType(activity.ActivityType),
|
||||||
|
StartTimeLocal: activity.StartTimeLocal,
|
||||||
|
Distance: activity.Distance,
|
||||||
|
Duration: activity.Duration,
|
||||||
|
},
|
||||||
|
Description: activity.Description,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadActivity downloads activity data
|
// DownloadActivity downloads activity data
|
||||||
@@ -162,8 +187,37 @@ func (c *Client) DownloadActivity(activityID int, opts DownloadOptions) error {
|
|||||||
|
|
||||||
// SearchActivities searches for activities by a query string
|
// SearchActivities searches for activities by a query string
|
||||||
func (c *Client) SearchActivities(query string) ([]Activity, error) {
|
func (c *Client) SearchActivities(query string) ([]Activity, error) {
|
||||||
// TODO: Implement internalClient.Client.SearchActivities
|
params := url.Values{}
|
||||||
return nil, fmt.Errorf("not implemented")
|
params.Add("search", query)
|
||||||
|
params.Add("limit", "20") // Default limit
|
||||||
|
|
||||||
|
data, err := c.Client.ConnectAPI("/activitylist-service/activities/search/activities", "GET", params, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to search activities: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []Activity{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var garthActivities []garth.Activity
|
||||||
|
if err := json.Unmarshal(data, &garthActivities); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse search response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var activities []Activity
|
||||||
|
for _, act := range garthActivities {
|
||||||
|
activities = append(activities, Activity{
|
||||||
|
ActivityID: act.ActivityID,
|
||||||
|
ActivityName: act.ActivityName,
|
||||||
|
ActivityType: ActivityType(act.ActivityType),
|
||||||
|
StartTimeLocal: act.StartTimeLocal,
|
||||||
|
Distance: act.Distance,
|
||||||
|
Duration: act.Duration,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return activities, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSleepData retrieves sleep data for a specified date range
|
// GetSleepData retrieves sleep data for a specified date range
|
||||||
@@ -223,8 +277,27 @@ func (c *Client) GetTrainingLoad(date time.Time) (*TrainingLoad, error) {
|
|||||||
|
|
||||||
// GetFitnessAge retrieves fitness age calculation
|
// GetFitnessAge retrieves fitness age calculation
|
||||||
func (c *Client) GetFitnessAge() (*FitnessAge, error) {
|
func (c *Client) GetFitnessAge() (*FitnessAge, error) {
|
||||||
// TODO: Implement GetFitnessAge in internalClient.Client
|
data, err := c.Client.ConnectAPI("/fitness-service/fitness/fitnessAge", "GET", nil, nil)
|
||||||
return nil, fmt.Errorf("GetFitnessAge not implemented in internalClient.Client")
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get fitness age: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var fitnessAge garth.FitnessAge
|
||||||
|
if err := json.Unmarshal(data, &fitnessAge); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse fitness age response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fitnessAge.LastUpdated = time.Now()
|
||||||
|
return &FitnessAge{
|
||||||
|
FitnessAge: fitnessAge.FitnessAge,
|
||||||
|
ChronologicalAge: fitnessAge.ChronologicalAge,
|
||||||
|
VO2MaxRunning: fitnessAge.VO2MaxRunning,
|
||||||
|
LastUpdated: fitnessAge.LastUpdated,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuth1Token returns the OAuth1 token
|
// OAuth1Token returns the OAuth1 token
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/sstent/go-garth/internal/auth/sso"
|
"github.com/sstent/go-garth/internal/auth/sso"
|
||||||
"github.com/sstent/go-garth/internal/errors"
|
"github.com/sstent/go-garth/internal/errors"
|
||||||
|
"github.com/sstent/go-garth/internal/utils"
|
||||||
garth "github.com/sstent/go-garth/pkg/garth/types"
|
garth "github.com/sstent/go-garth/pkg/garth/types"
|
||||||
shared "github.com/sstent/go-garth/shared/interfaces"
|
shared "github.com/sstent/go-garth/shared/interfaces"
|
||||||
models "github.com/sstent/go-garth/shared/models"
|
models "github.com/sstent/go-garth/shared/models"
|
||||||
@@ -503,45 +504,233 @@ func (c *Client) GetActivities(limit int) ([]garth.Activity, error) {
|
|||||||
return activities, nil
|
return activities, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetSleepData(startDate, endDate time.Time) ([]garth.SleepData, error) {
|
// GetActivitiesWithOptions retrieves activities with filtering options
|
||||||
// TODO: Implement GetSleepData
|
func (c *Client) GetActivitiesWithOptions(limit, offset int, activityType string, dateFrom, dateTo time.Time) ([]garth.Activity, error) {
|
||||||
return nil, fmt.Errorf("GetSleepData not implemented")
|
if limit <= 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := "https"
|
||||||
|
if strings.HasPrefix(c.Domain, "127.0.0.1") {
|
||||||
|
scheme = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("limit", fmt.Sprintf("%d", limit))
|
||||||
|
params.Add("start", fmt.Sprintf("%d", offset))
|
||||||
|
if activityType != "" {
|
||||||
|
params.Add("activityType", activityType)
|
||||||
|
}
|
||||||
|
if !dateFrom.IsZero() {
|
||||||
|
params.Add("startDate", dateFrom.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
if !dateTo.IsZero() {
|
||||||
|
params.Add("endDate", dateTo.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
|
||||||
|
activitiesURL := fmt.Sprintf("%s://connectapi.%s/activitylist-service/activities/search/activities?%s", scheme, c.Domain, params.Encode())
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", activitiesURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &errors.APIError{
|
||||||
|
GarthHTTPError: errors.GarthHTTPError{
|
||||||
|
GarthError: errors.GarthError{
|
||||||
|
Message: "Failed to create activities request",
|
||||||
|
Cause: err,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", c.AuthToken)
|
||||||
|
req.Header.Set("User-Agent", "com.garmin.android.apps.connectmobile")
|
||||||
|
|
||||||
|
resp, err := c.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &errors.APIError{
|
||||||
|
GarthHTTPError: errors.GarthHTTPError{
|
||||||
|
GarthError: errors.GarthError{
|
||||||
|
Message: "Failed to get activities",
|
||||||
|
Cause: err,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, &errors.APIError{
|
||||||
|
GarthHTTPError: errors.GarthHTTPError{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Response: string(body),
|
||||||
|
GarthError: errors.GarthError{
|
||||||
|
Message: "Activities request failed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var activities []garth.Activity
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&activities); err != nil {
|
||||||
|
return nil, &errors.IOError{
|
||||||
|
GarthError: errors.GarthError{
|
||||||
|
Message: "Failed to parse activities",
|
||||||
|
Cause: err,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return activities, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHrvData retrieves HRV data for a specified number of days
|
func (c *Client) GetSleepData(startDate, endDate time.Time) ([]garth.SleepData, error) {
|
||||||
func (c *Client) GetHrvData(days int) ([]garth.HrvData, error) {
|
path := fmt.Sprintf("/usersummary-service/stats/sleep/daily/%s/%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||||
// TODO: Implement GetHrvData
|
|
||||||
return nil, fmt.Errorf("GetHrvData not implemented")
|
data, err := c.ConnectAPI(path, "GET", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get sleep data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []garth.SleepData{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []garth.SleepData
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse sleep response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHrvData retrieves HRV data for a specified date range
|
||||||
|
func (c *Client) GetHrvData(startDate, endDate time.Time) ([]garth.HrvData, error) {
|
||||||
|
path := fmt.Sprintf("/usersummary-service/stats/hrv/daily/%s/%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||||
|
|
||||||
|
data, err := c.ConnectAPI(path, "GET", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get HRV data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []garth.HrvData{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []garth.HrvData
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse HRV response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStressData retrieves stress data
|
// GetStressData retrieves stress data
|
||||||
func (c *Client) GetStressData(startDate, endDate time.Time) ([]garth.StressData, error) {
|
func (c *Client) GetStressData(startDate, endDate time.Time) ([]garth.StressData, error) {
|
||||||
// TODO: Implement GetStressData
|
path := fmt.Sprintf("/usersummary-service/stats/stress/daily/%s/%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||||
return nil, fmt.Errorf("GetStressData not implemented")
|
|
||||||
|
data, err := c.ConnectAPI(path, "GET", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get stress data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []garth.StressData{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []garth.StressData
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse stress response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBodyBatteryData retrieves Body Battery data
|
// GetBodyBatteryData retrieves Body Battery data
|
||||||
func (c *Client) GetBodyBatteryData(startDate, endDate time.Time) ([]garth.BodyBatteryData, error) {
|
func (c *Client) GetBodyBatteryData(startDate, endDate time.Time) ([]garth.BodyBatteryData, error) {
|
||||||
// TODO: Implement GetBodyBatteryData
|
path := fmt.Sprintf("/usersummary-service/stats/bodybattery/daily/%s/%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||||
return nil, fmt.Errorf("GetBodyBatteryData not implemented")
|
|
||||||
|
data, err := c.ConnectAPI(path, "GET", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get Body Battery data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []garth.BodyBatteryData{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []garth.BodyBatteryData
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse Body Battery response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStepsData retrieves steps data for a specified date range
|
// GetStepsData retrieves steps data for a specified date range
|
||||||
func (c *Client) GetStepsData(startDate, endDate time.Time) ([]garth.StepsData, error) {
|
func (c *Client) GetStepsData(startDate, endDate time.Time) ([]garth.StepsData, error) {
|
||||||
// TODO: Implement GetStepsData
|
path := fmt.Sprintf("/usersummary-service/stats/steps/daily/%s/%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||||
return nil, fmt.Errorf("GetStepsData not implemented")
|
|
||||||
|
data, err := c.ConnectAPI(path, "GET", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get steps data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []garth.StepsData{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []garth.StepsData
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse steps response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDistanceData retrieves distance data for a specified date range
|
// GetDistanceData retrieves distance data for a specified date range
|
||||||
func (c *Client) GetDistanceData(startDate, endDate time.Time) ([]garth.DistanceData, error) {
|
func (c *Client) GetDistanceData(startDate, endDate time.Time) ([]garth.DistanceData, error) {
|
||||||
// TODO: Implement GetDistanceData
|
path := fmt.Sprintf("/usersummary-service/stats/distance/daily/%s/%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||||
return nil, fmt.Errorf("GetDistanceData not implemented")
|
|
||||||
|
data, err := c.ConnectAPI(path, "GET", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get distance data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []garth.DistanceData{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []garth.DistanceData
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse distance response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCaloriesData retrieves calories data for a specified date range
|
// GetCaloriesData retrieves calories data for a specified date range
|
||||||
func (c *Client) GetCaloriesData(startDate, endDate time.Time) ([]garth.CaloriesData, error) {
|
func (c *Client) GetCaloriesData(startDate, endDate time.Time) ([]garth.CaloriesData, error) {
|
||||||
// TODO: Implement GetCaloriesData
|
path := fmt.Sprintf("/usersummary-service/stats/calories/daily/%s/%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||||
return nil, fmt.Errorf("GetCaloriesData not implemented")
|
|
||||||
|
data, err := c.ConnectAPI(path, "GET", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get calories data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []garth.CaloriesData{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []garth.CaloriesData
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse calories response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVO2MaxData retrieves VO2 max data using the modern approach via user settings
|
// GetVO2MaxData retrieves VO2 max data using the modern approach via user settings
|
||||||
@@ -959,6 +1148,56 @@ func (c *Client) LoadSession(filename string) error {
|
|||||||
|
|
||||||
// RefreshSession refreshes the authentication tokens
|
// RefreshSession refreshes the authentication tokens
|
||||||
func (c *Client) RefreshSession() error {
|
func (c *Client) RefreshSession() error {
|
||||||
// TODO: Implement token refresh logic
|
if c.OAuth2Token == nil || c.OAuth2Token.RefreshToken == "" {
|
||||||
return fmt.Errorf("RefreshSession not implemented")
|
return fmt.Errorf("no refresh token available")
|
||||||
|
}
|
||||||
|
|
||||||
|
consumer, err := utils.LoadOAuthConsumer()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load OAuth consumer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := "https"
|
||||||
|
if strings.HasPrefix(c.Domain, "127.0.0.1") {
|
||||||
|
scheme = "http"
|
||||||
|
}
|
||||||
|
tokenURL := fmt.Sprintf("%s://connectapi.%s/oauth-service/oauth/token", scheme, c.Domain)
|
||||||
|
|
||||||
|
data := url.Values{}
|
||||||
|
data.Set("grant_type", "refresh_token")
|
||||||
|
data.Set("refresh_token", c.OAuth2Token.RefreshToken)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", tokenURL, strings.NewReader(data.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create refresh request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.SetBasicAuth(consumer.ConsumerKey, consumer.ConsumerSecret)
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("User-Agent", "garth-go-client/1.0")
|
||||||
|
|
||||||
|
resp, err := c.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("refresh request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("refresh failed with status %d: %s", resp.StatusCode, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
var newToken garth.OAuth2Token
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&newToken); err != nil {
|
||||||
|
return fmt.Errorf("failed to decode refresh response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update token with new values while preserving existing fields
|
||||||
|
c.OAuth2Token.AccessToken = newToken.AccessToken
|
||||||
|
c.OAuth2Token.RefreshToken = newToken.RefreshToken
|
||||||
|
c.OAuth2Token.ExpiresIn = newToken.ExpiresIn
|
||||||
|
c.OAuth2Token.ExpiresAt = time.Now().Add(time.Duration(newToken.ExpiresIn) * time.Second)
|
||||||
|
c.AuthToken = fmt.Sprintf("%s %s", newToken.TokenType, newToken.AccessToken)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user