X-Git-Url: https://git.octo.it/?a=blobdiff_plain;f=fitbit%2Ffitbit.go;h=9f661d6637fc6170f675d0020d23cfcff2af9e09;hb=ad911fb5b8a17f238e6158b4f9479c5f8c428bff;hp=6c7a61de7d012f38c5866dca64831d0af3229ec9;hpb=2a96ce53ec33a7fbd6d5054671ed2dfcc0ff379e;p=kraftakt.git diff --git a/fitbit/fitbit.go b/fitbit/fitbit.go index 6c7a61d..9f661d6 100644 --- a/fitbit/fitbit.go +++ b/fitbit/fitbit.go @@ -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 +}