Files
go-garth/garth/client/client.go
2025-09-07 17:24:05 -07:00

182 lines
4.6 KiB
Go

package client
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"os"
"time"
"garmin-connect/garth/sso"
"garmin-connect/garth/types"
)
// Client represents the Garmin Connect API client
type Client struct {
Domain string
HTTPClient *http.Client
Username string
AuthToken string
OAuth1Token *types.OAuth1Token
OAuth2Token *types.OAuth2Token
}
// NewClient creates a new Garmin Connect client
func NewClient(domain string) (*Client, error) {
if domain == "" {
domain = "garmin.com"
}
jar, err := cookiejar.New(nil)
if err != nil {
return nil, fmt.Errorf("failed to create cookie jar: %w", err)
}
return &Client{
Domain: domain,
HTTPClient: &http.Client{
Jar: jar,
Timeout: 30 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return fmt.Errorf("too many redirects")
}
return nil
},
},
}, nil
}
// Login authenticates to Garmin Connect using SSO
func (c *Client) Login(email, password string) error {
ssoClient := sso.NewClient(c.Domain)
oauth2Token, mfaContext, err := ssoClient.Login(email, password)
if err != nil {
return fmt.Errorf("SSO login failed: %w", err)
}
// Handle MFA required
if mfaContext != nil {
return fmt.Errorf("MFA required - not implemented yet")
}
c.OAuth2Token = oauth2Token
c.AuthToken = fmt.Sprintf("%s %s", oauth2Token.TokenType, oauth2Token.AccessToken)
// Get user profile to set username
profile, err := c.GetUserProfile()
if err != nil {
return fmt.Errorf("failed to get user profile after login: %w", err)
}
c.Username = profile.UserName
return nil
}
// GetUserProfile retrieves the current user's full profile
func (c *Client) GetUserProfile() (*UserProfile, error) {
profileURL := fmt.Sprintf("https://connectapi.%s/userprofile-service/socialProfile", c.Domain)
req, err := http.NewRequest("GET", profileURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create profile request: %w", 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, fmt.Errorf("failed to get user profile: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("profile request failed with status %d: %s", resp.StatusCode, string(body))
}
var profile UserProfile
if err := json.NewDecoder(resp.Body).Decode(&profile); err != nil {
return nil, fmt.Errorf("failed to parse profile: %w", err)
}
return &profile, nil
}
// GetActivities retrieves recent activities
func (c *Client) GetActivities(limit int) ([]types.Activity, error) {
if limit <= 0 {
limit = 10
}
activitiesURL := fmt.Sprintf("https://connectapi.%s/activitylist-service/activities/search/activities?limit=%d&start=0", c.Domain, limit)
req, err := http.NewRequest("GET", activitiesURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create activities request: %w", 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, fmt.Errorf("failed to get activities: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("activities request failed with status %d: %s", resp.StatusCode, string(body))
}
var activities []types.Activity
if err := json.NewDecoder(resp.Body).Decode(&activities); err != nil {
return nil, fmt.Errorf("failed to parse activities: %w", err)
}
return activities, nil
}
// SaveSession saves the current session to a file
func (c *Client) SaveSession(filename string) error {
session := types.SessionData{
Domain: c.Domain,
Username: c.Username,
AuthToken: c.AuthToken,
}
data, err := json.MarshalIndent(session, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal session: %w", err)
}
if err := os.WriteFile(filename, data, 0600); err != nil {
return fmt.Errorf("failed to write session file: %w", err)
}
return nil
}
// LoadSession loads a session from a file
func (c *Client) LoadSession(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read session file: %w", err)
}
var session types.SessionData
if err := json.Unmarshal(data, &session); err != nil {
return fmt.Errorf("failed to unmarshal session: %w", err)
}
c.Domain = session.Domain
c.Username = session.Username
c.AuthToken = session.AuthToken
return nil
}