"net/url"
"time"
+ "github.com/google/uuid"
+ "github.com/octo/gfitsync/fitbit"
legacy_context "golang.org/x/net/context"
"golang.org/x/oauth2"
- "golang.org/x/oauth2/fitbit"
+ oauth2fitbit "golang.org/x/oauth2/fitbit"
"google.golang.org/appengine"
"google.golang.org/appengine/datastore"
"google.golang.org/appengine/delay"
var oauthConfig = &oauth2.Config{
ClientID: "@FITBIT_CLIENT_ID@",
ClientSecret: "@FITBIT_CLIENT_SECRET@",
- Endpoint: fitbit.Endpoint,
+ Endpoint: oauth2fitbit.Endpoint,
RedirectURL: "https://fitbit-gfit-sync.appspot.com/fitbit/grant", // TODO(octo): make full URL
Scopes: []string{"activity"},
}
-type storedToken struct {
- Email string
- oauth2.Token
-}
-
func init() {
http.HandleFunc("/setup", setupHandler)
http.Handle("/fitbit/grant", AuthenticatedHandler(fitbitGrantHandler))
}
}
-type AuthenticatedHandler func(context.Context, http.ResponseWriter, *http.Request, *user.User) error
+type AuthenticatedHandler func(context.Context, http.ResponseWriter, *http.Request, *datastore.Key) error
+
+type User struct {
+ ID string
+}
func (hndl AuthenticatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)
}
err := datastore.RunInTransaction(ctx, func(ctx legacy_context.Context) error {
- key := RootKey(ctx, u)
- if err := datastore.Get(ctx, key, &struct{}{}); err != datastore.ErrNoSuchEntity {
+ key := datastore.NewKey(ctx, "User", u.Email, 0, nil)
+
+ if err := datastore.Get(ctx, key, &User{}); err != datastore.ErrNoSuchEntity {
return err // may be nil
}
- _, err := datastore.Put(ctx, key, &struct{}{})
+
+ _, err := datastore.Put(ctx, key, &User{
+ ID: uuid.New().String(),
+ })
return err
}, nil)
if err != nil {
return
}
- if err := hndl(ctx, w, r, u); err != nil {
+ rootKey := datastore.NewKey(ctx, "User", u.Email, 0, nil)
+ if err := hndl(ctx, w, r, rootKey); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
-func RootKey(ctx context.Context, u *user.User) *datastore.Key {
- return datastore.NewKey(ctx, "User", u.Email, 0, nil)
-}
-
-func indexHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *user.User) error {
+func indexHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, rootKey *datastore.Key) error {
var (
tok oauth2.Token
haveToken bool
)
- key := datastore.NewKey(ctx, "Token", "Fitbit", 0, RootKey(ctx, u))
+ key := datastore.NewKey(ctx, "Token", "Fitbit", 0, rootKey)
err := datastore.Get(ctx, key, &tok)
if err != nil && err != datastore.ErrNoSuchEntity {
return err
haveToken = true
}
- fmt.Fprintf(w, "u = %v, tok = %v, haveToken = %v\n", u, tok, haveToken)
+ u := user.Current(ctx)
+
+ // fmt.Fprintf(w, "u = %v, tok = %v, haveToken = %v\n", u, tok, haveToken)
+ fmt.Fprintln(w, "<html><body><h1>Fitbit to Google Fit sync</h1>")
+
+ fmt.Fprintf(w, "<p>Hello %s</p>\n", u.Email)
+ fmt.Fprint(w, "<p>Fitbit: ")
+ if haveToken {
+ fmt.Fprint(w, `<strong style="color: DarkGreen;">Authorized</strong>`)
+ } else {
+ fmt.Fprint(w, `<strong style="color: DarkRed;">Not authorized</strong> (<a href="/setup">Authorize</a>)`)
+ }
+ fmt.Fprintln(w, "</p>")
+ fmt.Fprintln(w, "</body></html>")
// TODO(octo): print summary to user
return nil
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
-func fitbitGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *user.User) error {
+func fitbitGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, rootKey *datastore.Key) error {
if state := r.FormValue("state"); state != csrfToken {
return fmt.Errorf("invalid state parameter: %q", state)
}
return err
}
- key := datastore.NewKey(ctx, "Token", "Fitbit", 0, RootKey(ctx, u))
+ key := datastore.NewKey(ctx, "Token", "Fitbit", 0, rootKey)
if _, err := datastore.Put(ctx, key, tok); err != nil {
return err
}
-
c := oauthConfig.Client(ctx, tok)
+ var u User
+ if err := datastore.Get(ctx, rootKey, &u); err != nil {
+ return err
+ }
+
// create a subscription
- url := fmt.Sprintf("https://api.fitbit.com/1/user/-/activities/apiSubscriptions/%s.json",
- RootKey(ctx, u).Encode())
+ url := fmt.Sprintf("https://api.fitbit.com/1/user/-/activities/apiSubscriptions/%s.json", u.ID)
res, err := c.Post(url, "", nil)
if err != nil {
return err
}
defer res.Body.Close()
+ if res.StatusCode == http.StatusConflict {
+ var n fitbitSubscription
+ if err := json.NewDecoder(res.Body).Decode(&n); err != nil {
+ return err
+ }
+ log.Warningf(ctx, "conflict with existing subscription %v", n)
+ }
+
if res.StatusCode >= 400 {
- data, _ := ioutil.ReadAll(r.Body)
+ 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 fitbitNotification struct {
+type fitbitSubscription struct {
CollectionType string `json:"collectionType"`
Date string `json:"date"`
OwnerID string `json:"ownerId"`
SubscriptionID string `json:"subscriptionId"`
}
-func (n *fitbitNotification) URLValues() url.Values {
+func (s *fitbitSubscription) URLValues() url.Values {
return url.Values{
- "CollectionType": []string{n.CollectionType},
- "Date": []string{n.Date},
- "OwnerID": []string{n.OwnerID},
- "OwnerType": []string{n.OwnerType},
- "SubscriptionID": []string{n.SubscriptionID},
+ "CollectionType": []string{s.CollectionType},
+ "Date": []string{s.Date},
+ "OwnerID": []string{s.OwnerID},
+ "OwnerType": []string{s.OwnerType},
+ "SubscriptionID": []string{s.SubscriptionID},
}
}
-func (n *fitbitNotification) URL() string {
+func (s *fitbitSubscription) URL() string {
+ // daily summary: GET https://api.fitbit.com/1/user/[user-id]/activities/date/[date].json
return fmt.Sprintf("https://api.fitbit.com/1/user/%s/%s/date/%s.json",
- n.OwnerID, n.CollectionType, n.Date)
+ s.OwnerID, s.CollectionType, s.Date)
}
func checkSignature(ctx context.Context, payload []byte, rawSig string) bool {
// handleNotifications parses fitbit notifications and requests the individual
// activities from Fitbit. It is executed asynchronously via the delay package.
func handleNotifications(ctx context.Context, payload []byte) error {
- var notifications []fitbitNotification
- if err := json.Unmarshal(payload, notifications); err != nil {
+ var subscriptions []fitbitSubscription
+ if err := json.Unmarshal(payload, &subscriptions); err != nil {
return err
}
- for _, n := range notifications {
- if n.CollectionType != "activities" {
+ for _, s := range subscriptions {
+ if s.CollectionType != "activities" {
continue
}
- if err := handleNotification(ctx, &n); err != nil {
+ if err := handleNotification(ctx, &s); err != nil {
log.Errorf(ctx, "handleNotification() = %v", err)
continue
}
return nil
}
-func handleNotification(ctx context.Context, n *fitbitNotification) error {
- rootKey, err := datastore.DecodeKey(n.SubscriptionID)
+func handleNotification(ctx context.Context, s *fitbitSubscription) error {
+ q := datastore.NewQuery("User").Filter("ID=", s.SubscriptionID).KeysOnly()
+ keys, err := q.GetAll(ctx, nil)
if err != nil {
- return err
+ return fmt.Errorf("datastore.Query.GetAll(): %v", err)
}
+ if len(keys) != 1 {
+ return fmt.Errorf("len(keys) = %d, want 1", len(keys))
+ }
+
+ rootKey := keys[0]
key := datastore.NewKey(ctx, "Token", "Fitbit", 0, rootKey)
var tok oauth2.Token
return err
}
- c := oauthConfig.Client(ctx, &tok)
- res, err := c.Get(n.URL())
+ c := fitbit.NewClient(ctx, s.OwnerID, &tok)
+
+ t, err := time.Parse("2006-01-02", s.Date)
+ if err != nil {
+ return err
+ }
+
+ summary, err := c.ActivitySummary(t)
if err != nil {
return err
}
- log.Infof(ctx, "GET %s = %v", n.URL(), res)
+ log.Debugf(ctx, "ActivitySummary() = %v", summary)
return nil
}