15 "github.com/octo/gfitsync/app"
17 oauth2fitbit "golang.org/x/oauth2/fitbit"
18 "google.golang.org/appengine/log"
21 var oauth2Config = &oauth2.Config{
22 ClientID: "@FITBIT_CLIENT_ID@",
23 ClientSecret: "@FITBIT_CLIENT_SECRET@",
24 Endpoint: oauth2fitbit.Endpoint,
25 RedirectURL: "https://fitbit-gfit-sync.appspot.com/fitbit/grant",
26 Scopes: []string{"activity"},
29 const csrfToken = "@CSRFTOKEN@"
31 func AuthURL() string {
32 return oauth2Config.AuthCodeURL(csrfToken, oauth2.AccessTypeOffline)
35 func ParseToken(ctx context.Context, r *http.Request, u *app.User) error {
36 if state := r.FormValue("state"); state != csrfToken {
37 return fmt.Errorf("invalid state parameter: %q", state)
40 tok, err := oauth2Config.Exchange(ctx, r.FormValue("code"))
45 return u.SetToken(ctx, "Fitbit", tok)
48 func CheckSignature(ctx context.Context, payload []byte, rawSig string) bool {
49 base64Sig, err := url.QueryUnescape(rawSig)
51 log.Errorf(ctx, "QueryUnescape(%q) = %v", rawSig, err)
54 signatureGot, err := base64.StdEncoding.DecodeString(base64Sig)
56 log.Errorf(ctx, "base64.StdEncoding.DecodeString(%q) = %v", base64Sig, err)
60 mac := hmac.New(sha1.New, []byte(oauth2Config.ClientSecret+"&"))
62 signatureWant := mac.Sum(nil)
64 return hmac.Equal(signatureGot, signatureWant)
67 type Activity struct {
68 ActivityID int `json:"activityId"`
69 ActivityParentID int `json:"activityParentId"`
70 Calories int `json:"calories"`
71 Description string `json:"description"`
72 Distance float64 `json:"distance"`
73 Duration int `json:"duration"`
74 HasStartTime bool `json:"hasStartTime"`
75 IsFavorite bool `json:"isFavorite"`
76 LogID int `json:"logId"`
77 Name string `json:"name"`
78 StartTime string `json:"startTime"`
79 Steps int `json:"steps"`
82 type Distance struct {
83 Activity string `json:"activity"`
84 Distance float64 `json:"distance"`
87 type ActivitySummary struct {
88 Activities []Activity `json:"activities"`
90 CaloriesOut int `json:"caloriesOut"`
91 Distance float64 `json:"distance"`
92 Floors int `json:"floors"`
93 Steps int `json:"steps"`
96 ActivityCalories int `json:"activityCalories"`
97 CaloriesBMR int `json:"caloriesBMR"`
98 CaloriesOut int `json:"caloriesOut"`
99 MarginalCalories int `json:"marginalCalories"`
100 Distances []Distance `json:"distances"`
101 Elevation float64 `json:"elevation"`
102 Floors int `json:"floors"`
103 Steps int `json:"steps"`
104 SedentaryMinutes int `json:"sedentaryMinutes"`
105 LightlyActiveMinutes int `json:"lightlyActiveMinutes"`
106 FairlyActiveMinutes int `json:"fairlyActiveMinutes"`
107 VeryActiveMinutes int `json:"veryActiveMinutes"`
111 type Subscription struct {
112 CollectionType string `json:"collectionType"`
113 Date string `json:"date"`
114 OwnerID string `json:"ownerId"`
115 OwnerType string `json:"ownerType"`
116 SubscriptionID string `json:"subscriptionId"`
125 func NewClient(ctx context.Context, fitbitUserID string, u *app.User) (*Client, error) {
126 tok, err := u.Token(ctx, "Fitbit")
131 if fitbitUserID == "" {
136 fitbitUserID: fitbitUserID,
138 client: oauth2Config.Client(ctx, tok),
142 func (c *Client) ActivitySummary(t time.Time) (*ActivitySummary, error) {
143 url := fmt.Sprintf("https://api.fitbit.com/1/user/%s/activity/date/%s.json",
144 c.fitbitUserID, t.Format("2006-01-02"))
146 res, err := c.client.Get(url)
150 defer res.Body.Close()
152 var summary ActivitySummary
153 if err := json.NewDecoder(res.Body).Decode(&summary); err != nil {
160 func (c *Client) Subscribe(ctx context.Context, collection string) error {
161 subscriberID, err := c.appUser.ID(ctx)
166 url := fmt.Sprintf("https://api.fitbit.com/1/user/%s/%s/apiSubscriptions/%s.json",
167 c.fitbitUserID, collection, subscriberID)
168 res, err := c.client.Post(url, "", nil)
172 defer res.Body.Close()
174 if res.StatusCode >= 400 {
175 data, _ := ioutil.ReadAll(res.Body)
176 log.Errorf(ctx, "creating subscription failed: status %d %q", res.StatusCode, data)
177 return fmt.Errorf("creating subscription failed")