package fitstrava
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
)
func stravaCodeFromWeb(clientID string) string {
port := 8686
return codeFromWeb(port,
fmt.Sprintf("https://www.strava.com/oauth/authorize?client_id=%s&redirect_uri=%s%d&response_type=code&approval_prompt=auto&scope=%s", clientID, `https%3A%2F%2Flocalhost%3A`, port, `activity%3Aread_all%2Cactivity%3Awrite`))
}
type stravaAuthToken struct {
TokenType string `json:"token_type"`
ExpiresAt int64 `json:"expires_at"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
AccessToken string `json:"access_token"`
Athlete Athlete `json:"athlete"`
}
type Athlete struct {
ID int64 `json:"id"`
Username string `json:"username"`
ResourceState int64 `json:"resource_state"`
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
City string `json:"city"`
State string `json:"state"`
Country string `json:"country"`
Sex string `json:"sex"`
Premium bool `json:"premium"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
BadgeTypeID int64 `json:"badge_type_id"`
ProfileMedium string `json:"profile_medium"`
Profile string `json:"profile"`
Friend interface{} `json:"friend"`
Follower interface{} `json:"follower"`
FollowerCount int64 `json:"follower_count"`
FriendCount int64 `json:"friend_count"`
MutualFriendCount int64 `json:"mutual_friend_count"`
AthleteType int64 `json:"athlete_type"`
DatePreference string `json:"date_preference"`
MeasurementPreference string `json:"measurement_preference"`
Clubs []interface{} `json:"clubs"`
FTP interface{} `json:"ftp"`
Weight float64 `json:"weight"`
Bikes []Bike `json:"bikes"`
Shoes []Bike `json:"shoes"`
}
type Bike struct {
ID string `json:"id"`
Primary bool `json:"primary"`
Name string `json:"name"`
ResourceState int64 `json:"resource_state"`
Distance int64 `json:"distance"`
}
func getStravaAuthToken(clientID, clientSecret, authCode string) (stravaAuthToken, error) {
resp, err := http.PostForm("https://www.strava.com/api/v3/oauth/token",
url.Values{"client_id": {clientID}, "client_secret": {clientSecret}, "code": {authCode}, "grant_type": {"authorization_code"}})
if err != nil {
return stravaAuthToken{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return stravaAuthToken{}, fmt.Errorf("fitbit returned non-ok status: %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return stravaAuthToken{}, err
}
var token stravaAuthToken
if err := json.Unmarshal(body, &token); err != nil {
return stravaAuthToken{}, err
}
return token, nil
}
type StravaClient struct {
clientID, clientSecret string
Token stravaAuthToken
}
func NewStravaClient(clientID, clientSecret string) (StravaClient, error) {
code := stravaCodeFromWeb(clientID)
token, err := getStravaAuthToken(clientID, clientSecret, code)
if err != nil {
panic(err)
}
return StravaClient{clientID: clientID, clientSecret: clientSecret, Token: token}, nil
}
type stravaErrorResponse struct {
Message string `json:"message"`
Errors []stravaError `json:"errors"`
}
type stravaError struct {
Resource string `json:"resource"`
Field string `json:"field"`
Code string `json:"code"`
}
func stravaHttp(strava StravaClient, method, url string, body io.Reader) (*http.Response, error) {
client := &http.Client{}
r, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
r.Header.Add("Accept", "application/json")
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", strava.Token.AccessToken))
return client.Do(r)
}
func stravaGet(strava StravaClient, url string) (*http.Response, error) {
return stravaHttp(strava, http.MethodGet, url, nil)
}
func stravaPostForm(strava StravaClient, url string, params url.Values) (*http.Response, error) {
return stravaHttp(strava, http.MethodPost, url+"?"+params.Encode(), nil)
}
type stravaAthleteActivitiesResponse []StravaActivity
type StravaActivity struct {
ResourceState int64 `json:"resource_state"`
Athlete Athlete `json:"athlete"`
Name string `json:"name"`
Distance float64 `json:"distance"`
MovingTime int64 `json:"moving_time"`
ElapsedTime int64 `json:"elapsed_time"`
TotalElevationGain float64 `json:"total_elevation_gain"`
Type string `json:"type"`
WorkoutType interface{} `json:"workout_type,omitempty"`
ID int64 `json:"id"`
ExternalID string `json:"external_id,omitempty"`
UploadID int64 `json:"upload_id,omitempty"`
StartDate string `json:"start_date"`
StartDateLocal string `json:"start_date_local"`
Timezone string `json:"timezone"`
UTCOffset float64 `json:"utc_offset"`
StartLatlng interface{} `json:"start_latlng"`
EndLatlng interface{} `json:"end_latlng"`
LocationCity interface{} `json:"location_city"`
LocationState interface{} `json:"location_state"`
LocationCountry string `json:"location_country"`
AchievementCount int64 `json:"achievement_count"`
KudosCount int64 `json:"kudos_count"`
CommentCount int64 `json:"comment_count"`
AthleteCount int64 `json:"athlete_count"`
PhotoCount int64 `json:"photo_count"`
Map Map `json:"map"`
Trainer bool `json:"trainer"`
Commute bool `json:"commute"`
Manual bool `json:"manual"`
Private bool `json:"private"`
Visibility string `json:"visibility"`
Flagged bool `json:"flagged"`
GearID interface{} `json:"gear_id"`
StartLatitude interface{} `json:"start_latitude"`
StartLongitude interface{} `json:"start_longitude"`
AverageSpeed float64 `json:"average_speed"`
MaxSpeed float64 `json:"max_speed"`
AverageCadence float64 `json:"average_cadence,omitempty"`
AverageWatts float64 `json:"average_watts,omitempty"`
WeightedAverageWatts int64 `json:"weighted_average_watts,omitempty"`
Kilojoules float64 `json:"kilojoules,omitempty"`
DeviceWatts bool `json:"device_watts,omitempty"`
HasHeartrate bool `json:"has_heartrate"`
AverageHeartrate float64 `json:"average_heartrate,omitempty"`
MaxHeartrate float64 `json:"max_heartrate,omitempty"`
MaxWatts int64 `json:"max_watts,omitempty"`
HeartrateOptOut bool `json:"heartrate_opt_out"`
DisplayHideHeartrateOption bool `json:"display_hide_heartrate_option"`
FromAcceptedTag bool `json:"from_accepted_tag"`
PRCount int64 `json:"pr_count"`
TotalPhotoCount int64 `json:"total_photo_count"`
HasKudoed bool `json:"has_kudoed"`
SufferScore int64 `json:"suffer_score,omitempty"`
Description string `json:"description"`
Calories float64 `json:"calories"`
PerceivedExertion interface{} `json:"perceived_exertion"`
PreferPerceivedExertion interface{} `json:"prefer_perceived_exertion"`
SegmentEfforts []interface{} `json:"segment_efforts"`
Photos Photos `json:"photos"`
StatsVisibility []StatsVisibility `json:"stats_visibility"`
HideFromHome bool `json:"hide_from_home"`
EmbedToken string `json:"embed_token"`
PrivateNote interface{} `json:"private_note"`
AvailableZones []interface{} `json:"available_zones"`
}
type Photos struct {
Primary interface{} `json:"primary"`
Count int64 `json:"count"`
}
type StatsVisibility struct {
Type string `json:"type"`
Visibility string `json:"visibility"`
}
type Map struct {
ID string `json:"id"`
SummaryPolyline interface{} `json:"summary_polyline"`
ResourceState int64 `json:"resource_state"`
}
func stravaHttpError(body io.ReadCloser, msg string) error {
var serr stravaErrorResponse
if err := json.NewDecoder(body).Decode(&serr); err != nil {
return fmt.Errorf("%s", msg)
}
return fmt.Errorf("%s: %s", msg, serr.Message)
}
func StravaGetActivities(strava StravaClient, after time.Time) ([]StravaActivity, error) {
activities := make([]StravaActivity, 0)
page := 1
for {
qp := url.Values{}
qp.Set("after", strconv.FormatInt(after.Unix(), 10))
qp.Set("page", strconv.Itoa(page))
qp.Set("per_page", "100")
resp, err := stravaGet(strava, "https://www.strava.com/api/v3/athlete/activities?"+qp.Encode())
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusTooManyRequests {
// X-Ratelimit-Limit:[100,1000]
// X-Ratelimit-Usage:[224,251]
fmt.Printf("%+v\n", resp.Header)
}
return nil, stravaHttpError(resp.Body, fmt.Sprintf("could not get Strava activities: %s", resp.Status))
/*
var serr stravaErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&serr); err != nil {
return nil, fmt.Errorf("could not get Strava activities: %s", resp.Status)
}
return nil, fmt.Errorf("could not get Strava activities: %s: %s", resp.Status, serr.Message)
*/
}
var stravaResp stravaAthleteActivitiesResponse
if err := json.NewDecoder(resp.Body).Decode(&stravaResp); err != nil {
return nil, err
}
if len(stravaResp) == 0 {
break
}
activities = append(activities, stravaResp...)
page = page + 1
}
return activities, nil
}
/*
name required String, in form The name of the activity.
type required String, in form Type of activity. For example - Run, Ride etc.
start_date_local required Date, in form ISO 8601 formatted date time.
elapsed_time required Integer, in form In seconds.
description String, in form Description of the activity.
distance Float, in form In meters.
*/
type StravaNewActivity struct {
Name, Type, Description string
StartDate time.Time
ElapsedSeconds int64
// Distance float64
}
func StravaCreateActivity(strava StravaClient, activity StravaNewActivity) error {
p := url.Values{}
p.Set("name", activity.Name)
p.Set("type", activity.Type)
p.Set("description", activity.Description)
p.Set("start_date_local", activity.StartDate.Format(time.RFC3339))
p.Set("elapsed_time", strconv.FormatInt(activity.ElapsedSeconds, 10))
p.Set("hide_from_home", "true")
p.Set("external_id", "42")
// p.Set("perceived_exertion", "2")
// p.Set("private_note", "this is private")
resp, err := stravaPostForm(strava, "https://www.strava.com/api/v3/activities", p)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
/*
var serr stravaErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&serr); err != nil {
return fmt.Errorf("could not create Strava activity: %s", resp.Status)
}
return fmt.Errorf("could not create Strava activity: %s: %s", resp.Status, serr.Message)
*/
return stravaHttpError(resp.Body, fmt.Sprintf("could not create Strava activity: %s", resp.Status))
}
var newactivity StravaActivity
if err := json.NewDecoder(resp.Body).Decode(&newactivity); err != nil {
return fmt.Errorf("could not parse new Strava activity: %v", err)
}
/*
p = url.Values{}
p.Set("_method", "patch")
p.Set("perceived_exertion", "2")
p.Set("private_note", "this is private")
// https://www.strava.com/activities/6786571466
resp, err = stravaPut(strava, fmt.Sprintf("https://www.strava.com/api/v3/activities/%d", newactivity.ID), p)
if err != nil {
return err
}
if resp.StatusCode != http.StatusFound {
return fmt.Errorf("could not post a patch: %s", resp.Status)
}
*/
return nil
}
func StravaGetActivityByID(strava StravaClient, id int64) (StravaActivity, error) {
resp, err := stravaGet(strava, fmt.Sprintf("https://www.strava.com/api/v3/activities/%d", id))
if err != nil {
return StravaActivity{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return StravaActivity{}, stravaHttpError(resp.Body, fmt.Sprintf("could not create Strava activity: %s", resp.Status))
}
var activity StravaActivity
if err := json.NewDecoder(resp.Body).Decode(&activity); err != nil {
return StravaActivity{}, fmt.Errorf("could not parse Strava activity: %v", err)
}
return activity, nil
}