package gfitsync import ( "context" "encoding/json" "fmt" "io/ioutil" "net/http" "time" "github.com/octo/gfitsync/app" "github.com/octo/gfitsync/fitbit" "github.com/octo/gfitsync/gfit" "google.golang.org/appengine" "google.golang.org/appengine/datastore" "google.golang.org/appengine/delay" "google.golang.org/appengine/log" "google.golang.org/appengine/user" ) var delayedHandleNotifications = delay.Func("handleNotifications", handleNotifications) func init() { http.HandleFunc("/fitbit/setup", fitbitSetupHandler) http.Handle("/fitbit/grant", AuthenticatedHandler(fitbitGrantHandler)) http.Handle("/fitbit/notify", ContextHandler(fitbitNotifyHandler)) http.HandleFunc("/google/setup", googleSetupHandler) http.Handle("/google/grant", AuthenticatedHandler(googleGrantHandler)) http.Handle("/", AuthenticatedHandler(indexHandler)) } // ContextHandler implements http.Handler type ContextHandler func(context.Context, http.ResponseWriter, *http.Request) error func (hndl ContextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := appengine.NewContext(r) if err := hndl(ctx, w, r); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } type AuthenticatedHandler func(context.Context, http.ResponseWriter, *http.Request, *app.User) error func (hndl AuthenticatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := appengine.NewContext(r) gaeUser := user.Current(ctx) if gaeUser == nil { url, err := user.LoginURL(ctx, r.URL.String()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, url, http.StatusTemporaryRedirect) return } u, err := app.NewUser(ctx, gaeUser.Email) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if err := hndl(ctx, w, r, u); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } func indexHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error { _, err := u.Token(ctx, "Fitbit") if err != nil && err != datastore.ErrNoSuchEntity { return err } haveFitbitToken := err == nil _, err = u.Token(ctx, "Google") if err != nil && err != datastore.ErrNoSuchEntity { return err } haveGoogleToken := err == nil fmt.Fprintln(w, "

Fitbit to Google Fit sync

") fmt.Fprintf(w, "

Hello %s

\n", user.Current(ctx).Email) fmt.Fprintln(w, "") fmt.Fprintln(w, "") return nil } func fitbitSetupHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, fitbit.AuthURL(), http.StatusTemporaryRedirect) } func fitbitGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error { if err := fitbit.ParseToken(ctx, r, u); err != nil { return err } c, err := fitbit.NewClient(ctx, "-", u) if err != nil { return err } if err := c.Subscribe(ctx, "activities"); err != nil { return fmt.Errorf("c.Subscribe() = %v", err) } redirectURL := r.URL redirectURL.Path = "/" redirectURL.RawQuery = "" redirectURL.Fragment = "" http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect) return nil } func googleSetupHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, gfit.AuthURL(), http.StatusTemporaryRedirect) } func googleGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error { if err := gfit.ParseToken(ctx, r, u); err != nil { return err } redirectURL := r.URL redirectURL.Path = "/" redirectURL.RawQuery = "" redirectURL.Fragment = "" http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect) return nil } // fitbitNotifyHandler is called by Fitbit whenever there are updates to a // subscription. It verifies the payload, splits it into individual // notifications and adds it to the taskqueue service. func fitbitNotifyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error { defer r.Body.Close() fitbitTimeout := 3 * time.Second ctx, cancel := context.WithTimeout(ctx, fitbitTimeout) defer cancel() // this is used when setting up a new subscriber in the UI. Once set // up, this code path should not be triggered. if verify := r.FormValue("verify"); verify != "" { if verify == "@FITBIT_SUBSCRIBER_CODE@" { w.WriteHeader(http.StatusNoContent) } else { w.WriteHeader(http.StatusNotFound) } return nil } data, err := ioutil.ReadAll(r.Body) if err != nil { return err } // Fitbit recommendation: "If signature verification fails, you should // respond with a 404" if !fitbit.CheckSignature(ctx, data, r.Header.Get("X-Fitbit-Signature")) { w.WriteHeader(http.StatusNotFound) return nil } if err := delayedHandleNotifications.Call(ctx, data); err != nil { return err } w.WriteHeader(http.StatusCreated) return nil } // 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 subscriptions []fitbit.Subscription if err := json.Unmarshal(payload, &subscriptions); err != nil { return err } for _, s := range subscriptions { if s.CollectionType != "activities" { continue } if err := handleNotification(ctx, &s); err != nil { log.Errorf(ctx, "handleNotification() = %v", err) continue } } return nil } func handleNotification(ctx context.Context, s *fitbit.Subscription) error { u, err := app.UserByID(ctx, s.SubscriptionID) if err != nil { return err } tm, err := time.Parse("2006-01-02", s.Date) if err != nil { return err } fitbitClient, err := fitbit.NewClient(ctx, s.OwnerID, u) if err != nil { return err } summary, err := fitbitClient.ActivitySummary(tm) if err != nil { return err } log.Debugf(ctx, "ActivitySummary for %s = %+v", u.Email, summary) gfitClient, err := gfit.NewClient(ctx, u) if err != nil { return err } if err := gfitClient.SetSteps(ctx, summary.Summary.Steps, tm); err != nil { return fmt.Errorf("gfitClient.SetSteps(%d) = %v", summary.Summary.Steps, err) } return nil }