Package fitbit: Implement the Profile() method.
[kraftakt.git] / fitbit / fitbit.go
index 6c7a61d..9f661d6 100644 (file)
@@ -2,13 +2,20 @@ package fitbit
 
 import (
        "context"
+       "crypto/hmac"
+       "crypto/sha1"
+       "encoding/base64"
        "encoding/json"
        "fmt"
+       "io/ioutil"
        "net/http"
+       "net/url"
        "time"
 
+       "github.com/octo/gfitsync/app"
        "golang.org/x/oauth2"
        oauth2fitbit "golang.org/x/oauth2/fitbit"
+       "google.golang.org/appengine/log"
 )
 
 var oauth2Config = &oauth2.Config{
@@ -16,7 +23,45 @@ var oauth2Config = &oauth2.Config{
        ClientSecret: "@FITBIT_CLIENT_SECRET@",
        Endpoint:     oauth2fitbit.Endpoint,
        RedirectURL:  "https://fitbit-gfit-sync.appspot.com/fitbit/grant",
-       Scopes:       []string{"activity"},
+       Scopes:       []string{"activity", "heartrate", "profile"},
+}
+
+const csrfToken = "@CSRFTOKEN@"
+
+func AuthURL() string {
+       return oauth2Config.AuthCodeURL(csrfToken, oauth2.AccessTypeOffline)
+}
+
+func ParseToken(ctx context.Context, r *http.Request, u *app.User) error {
+       if state := r.FormValue("state"); state != csrfToken {
+               return fmt.Errorf("invalid state parameter: %q", state)
+       }
+
+       tok, err := oauth2Config.Exchange(ctx, r.FormValue("code"))
+       if err != nil {
+               return err
+       }
+
+       return u.SetToken(ctx, "Fitbit", tok)
+}
+
+func CheckSignature(ctx context.Context, payload []byte, rawSig string) bool {
+       base64Sig, err := url.QueryUnescape(rawSig)
+       if err != nil {
+               log.Errorf(ctx, "QueryUnescape(%q) = %v", rawSig, err)
+               return false
+       }
+       signatureGot, err := base64.StdEncoding.DecodeString(base64Sig)
+       if err != nil {
+               log.Errorf(ctx, "base64.StdEncoding.DecodeString(%q) = %v", base64Sig, err)
+               return false
+       }
+
+       mac := hmac.New(sha1.New, []byte(oauth2Config.ClientSecret+"&"))
+       mac.Write(payload)
+       signatureWant := mac.Sum(nil)
+
+       return hmac.Equal(signatureGot, signatureWant)
 }
 
 type Activity struct {
@@ -63,21 +108,40 @@ type ActivitySummary struct {
        } `json:"summary"`
 }
 
+type Subscription struct {
+       CollectionType string `json:"collectionType"`
+       Date           string `json:"date"`
+       OwnerID        string `json:"ownerId"`
+       OwnerType      string `json:"ownerType"`
+       SubscriptionID string `json:"subscriptionId"`
+}
+
 type Client struct {
-       userID string
-       client *http.Client
+       fitbitUserID string
+       appUser      *app.User
+       client       *http.Client
 }
 
-func NewClient(ctx context.Context, userID string, tok *oauth2.Token) *Client {
-       return &Client{
-               userID: userID,
-               client: oauth2Config.Client(ctx, tok),
+func NewClient(ctx context.Context, fitbitUserID string, u *app.User) (*Client, error) {
+       if fitbitUserID == "" {
+               fitbitUserID = "-"
        }
+
+       c, err := u.OAuthClient(ctx, "Fitbit", oauth2Config)
+       if err != nil {
+               return nil, err
+       }
+
+       return &Client{
+               fitbitUserID: fitbitUserID,
+               appUser:      u,
+               client:       c,
+       }, nil
 }
 
 func (c *Client) ActivitySummary(t time.Time) (*ActivitySummary, error) {
-       url := fmt.Sprintf("https://api.fitbit.com/1/user/%s/activity/date/%s.json",
-               c.userID, t.Format("2006-01-02"))
+       url := fmt.Sprintf("https://api.fitbit.com/1/user/%s/activities/date/%s.json",
+               c.fitbitUserID, t.Format("2006-01-02"))
 
        res, err := c.client.Get(url)
        if err != nil {
@@ -92,3 +156,66 @@ func (c *Client) ActivitySummary(t time.Time) (*ActivitySummary, error) {
 
        return &summary, nil
 }
+
+func (c *Client) Subscribe(ctx context.Context, collection string) error {
+       subscriberID, err := c.appUser.ID(ctx)
+       if err != nil {
+               return err
+       }
+
+       url := fmt.Sprintf("https://api.fitbit.com/1/user/%s/%s/apiSubscriptions/%s.json",
+               c.fitbitUserID, collection, subscriberID)
+       res, err := c.client.Post(url, "", nil)
+       if err != nil {
+               return err
+       }
+       defer res.Body.Close()
+
+       if res.StatusCode >= 400 {
+               data, _ := ioutil.ReadAll(res.Body)
+               log.Errorf(ctx, "creating subscription failed: status %d %q", res.StatusCode, data)
+               return fmt.Errorf("creating subscription failed")
+       }
+
+       return nil
+}
+
+type Profile struct {
+       Name     string
+       Timezone *time.Location
+}
+
+func (c *Client) Profile(ctx context.Context) (*Profile, error) {
+       res, err := c.client.Get("https://api.fitbit.com/1/user/-/profile.json")
+       if err != nil {
+               return nil, err
+       }
+       defer res.Body.Close()
+
+       if res.StatusCode >= 400 {
+               data, _ := ioutil.ReadAll(res.Body)
+               log.Errorf(ctx, "reading profile failed: %s", data)
+               return nil, fmt.Errorf("HTTP %d error", res.StatusCode)
+       }
+
+       var data struct {
+               User struct {
+                       FullName            string
+                       OffsetFromUTCMillis int
+                       Timezone            string
+               }
+       }
+       if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
+               return nil, err
+       }
+
+       loc, err := time.LoadLocation(data.User.Timezone)
+       if err != nil {
+               loc = time.FixedZone("Fitbit preference", data.User.OffsetFromUTCMillis/1000)
+       }
+
+       return &Profile{
+               Name:     data.User.FullName,
+               Timezone: loc,
+       }, nil
+}