1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
package strava
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/martinlehoux/kagamigo/kcore"
)
const (
AuthURL = "https://www.strava.com/oauth/authorize"
TokenURL = "https://www.strava.com/oauth/token"
APIURL = "https://www.strava.com/api/v3"
)
var (
tokenEndpoint = TokenURL
apiEndpoint = APIURL
)
type Token struct {
AccessToken string
RefreshToken string
ExpiresAt time.Time
}
func AuthorizeURLWithState(clientID, redirectURI, state string) string {
q := url.Values{}
q.Set("client_id", clientID)
q.Set("response_type", "code")
q.Set("redirect_uri", redirectURI)
q.Set("approval_prompt", "auto")
q.Set("scope", "activity:read_all")
if state != "" {
q.Set("state", state)
}
return AuthURL + "?" + q.Encode()
}
type tokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt int64 `json:"expires_at"`
}
func ExchangeCode(clientID, clientSecret, code, redirectURI string) (Token, error) {
form := url.Values{}
form.Set("client_id", clientID)
form.Set("client_secret", clientSecret)
form.Set("code", code)
form.Set("grant_type", "authorization_code")
form.Set("redirect_uri", redirectURI)
return requestToken(form)
}
func Refresh(clientID, clientSecret, refreshToken string) (Token, error) {
form := url.Values{}
form.Set("client_id", clientID)
form.Set("client_secret", clientSecret)
form.Set("grant_type", "refresh_token")
form.Set("refresh_token", refreshToken)
return requestToken(form)
}
func requestToken(form url.Values) (Token, error) {
resp, err := http.PostForm(tokenEndpoint, form)
if err != nil {
return Token{}, kcore.Wrap(err, "failed to request Strava token")
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return Token{}, kcore.Wrap(err, "failed to read Strava token response")
}
if resp.StatusCode != http.StatusOK {
return Token{}, fmt.Errorf("strava token request failed (HTTP %d): %s", resp.StatusCode, body)
}
var parsed tokenResponse
if err := json.Unmarshal(body, &parsed); err != nil {
return Token{}, kcore.Wrap(err, "failed to decode Strava token response")
}
if parsed.AccessToken == "" {
return Token{}, fmt.Errorf("strava token response missing access_token: %s", body)
}
return Token{
AccessToken: parsed.AccessToken,
RefreshToken: parsed.RefreshToken,
ExpiresAt: time.Unix(parsed.ExpiresAt, 0),
}, nil
}
type Client struct {
ClientID string
ClientSecret string
HTTP *http.Client
tokens Token
}
func NewClient(clientID, clientSecret string, tokens Token) *Client {
return &Client{ClientID: clientID, ClientSecret: clientSecret, HTTP: http.DefaultClient, tokens: tokens}
}
func (c *Client) Tokens() Token {
return c.tokens
}
func (c *Client) RefreshIfNeeded() (bool, error) {
if time.Until(c.tokens.ExpiresAt) > 5*time.Minute {
return false, nil
}
refreshed, err := Refresh(c.ClientID, c.ClientSecret, c.tokens.RefreshToken)
if err != nil {
return false, kcore.Wrap(err, "failed to refresh Strava access token")
}
c.tokens = refreshed
return true, nil
}
func (c *Client) GetJSON(path string, out any) error {
if _, err := c.RefreshIfNeeded(); err != nil {
return err
}
req, err := http.NewRequest(http.MethodGet, apiEndpoint+path, nil)
if err != nil {
return kcore.Wrap(err, "failed to build Strava request")
}
req.Header.Set("Authorization", "Bearer "+c.tokens.AccessToken)
resp, err := c.HTTP.Do(req)
if err != nil {
return kcore.Wrap(err, "failed to call Strava API")
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return kcore.Wrap(err, "failed to read Strava API response")
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("strava API %s failed (HTTP %d): %s", path, resp.StatusCode, body)
}
if err := json.Unmarshal(body, out); err != nil {
return kcore.Wrap(err, "failed to decode Strava API response")
}
return nil
}
|