Call ActivitySummary() and Profile() concurrently.
[kraftakt.git] / gfitsync.go
index 0007395..3bd416f 100644 (file)
@@ -6,10 +6,12 @@ import (
        "fmt"
        "io/ioutil"
        "net/http"
+       "sync"
        "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"
@@ -20,9 +22,11 @@ import (
 var delayedHandleNotifications = delay.Func("handleNotifications", handleNotifications)
 
 func init() {
-       http.HandleFunc("/setup", setupHandler)
+       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))
 }
 
@@ -40,10 +44,6 @@ func (hndl ContextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 type AuthenticatedHandler func(context.Context, http.ResponseWriter, *http.Request, *app.User) error
 
-type User struct {
-       ID string
-}
-
 func (hndl AuthenticatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        ctx := appengine.NewContext(r)
 
@@ -75,25 +75,46 @@ func indexHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u
        if err != nil && err != datastore.ErrNoSuchEntity {
                return err
        }
-       haveToken := err == nil
+       haveFitbitToken := err == nil
+
+       _, err = u.Token(ctx, "Google")
+       if err != nil && err != datastore.ErrNoSuchEntity {
+               return err
+       }
+       haveGoogleToken := err == nil
+
+       fmt.Fprintln(w, "<html><head><title>Kraftakt</title></head>")
+       fmt.Fprintln(w, "<body><h1>Kraftakt</h1>")
+
+       fmt.Fprintln(w, "<p><strong>Kraftakt</strong> copies your <em>Fitbit</em> data to <em>Google Fit</em>, seconds after you sync.</p>")
 
-       fmt.Fprintln(w, "<html><body><h1>Fitbit to Google Fit sync</h1>")
        fmt.Fprintf(w, "<p>Hello %s</p>\n", user.Current(ctx).Email)
-       fmt.Fprint(w, "<p>Fitbit: ")
-       if haveToken {
+       fmt.Fprintln(w, "<ul>")
+
+       fmt.Fprint(w, "<li>Fitbit: ")
+       if haveFitbitToken {
+               fmt.Fprint(w, `<strong style="color: DarkGreen;">Authorized</strong>`)
+       } else {
+               fmt.Fprint(w, `<strong style="color: DarkRed;">Not authorized</strong> (<a href="/fitbit/setup">Authorize</a>)`)
+       }
+       fmt.Fprintln(w, "</li>")
+
+       fmt.Fprint(w, "<li>Google Fit: ")
+       if haveGoogleToken {
                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.Fprint(w, `<strong style="color: DarkRed;">Not authorized</strong> (<a href="/google/setup">Authorize</a>)`)
        }
-       fmt.Fprintln(w, "</p>")
+       fmt.Fprintln(w, "</li>")
+
+       fmt.Fprintln(w, "</ul>")
        fmt.Fprintln(w, "</body></html>")
 
        return nil
 }
 
-func setupHandler(w http.ResponseWriter, r *http.Request) {
-       url := fitbit.AuthURL()
-       http.Redirect(w, r, url, http.StatusTemporaryRedirect)
+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 {
@@ -117,6 +138,23 @@ func fitbitGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Requ
        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.
@@ -185,21 +223,101 @@ func handleNotification(ctx context.Context, s *fitbit.Subscription) error {
        if err != nil {
                return err
        }
-       c, err := fitbit.NewClient(ctx, s.OwnerID, u)
+
+       fitbitClient, err := fitbit.NewClient(ctx, s.OwnerID, u)
        if err != nil {
                return err
        }
 
-       tm, err := time.Parse("2006-01-02", s.Date)
+       var (
+               wg      = &sync.WaitGroup{}
+               errs    appengine.MultiError
+               summary *fitbit.ActivitySummary
+               profile *fitbit.Profile
+       )
+
+       wg.Add(1)
+       go func() {
+               var err error
+               summary, err = fitbitClient.ActivitySummary(ctx, s.Date)
+               if err != nil {
+                       errs = append(errs, fmt.Errorf("fitbitClient.ActivitySummary(%q) = %v", s.Date, err))
+               }
+               wg.Done()
+       }()
+
+       wg.Add(1)
+       go func() {
+               var err error
+               profile, err = fitbitClient.Profile(ctx)
+               if err != nil {
+                       errs = append(errs, fmt.Errorf("fitbitClient.Profile(%q) = %v", s.Date, err))
+               }
+               wg.Done()
+       }()
+
+       wg.Wait()
+       if len(errs) != 0 {
+               return errs
+       }
+
+       tm, err := time.ParseInLocation("2006-01-02", s.Date, profile.Timezone)
        if err != nil {
                return err
        }
 
-       summary, err := c.ActivitySummary(tm)
+       log.Debugf(ctx, "%s (%s) took %d steps on %s",
+               profile.Name, u.Email, summary.Summary.Steps, tm)
+
+       gfitClient, err := gfit.NewClient(ctx, u)
        if err != nil {
                return err
        }
 
-       log.Debugf(ctx, "ActivitySummary(%q) = %+v", s.OwnerID, summary)
+       wg.Add(1)
+       go func() {
+               if err := gfitClient.SetSteps(ctx, summary.Summary.Steps, tm); err != nil {
+                       errs = append(errs, fmt.Errorf("gfitClient.SetSteps(%d) = %v", summary.Summary.Steps, err))
+               }
+               wg.Done()
+       }()
+
+       wg.Add(1)
+       go func() {
+               if err := gfitClient.SetCalories(ctx, summary.Summary.CaloriesOut, tm); err != nil {
+                       errs = append(errs, fmt.Errorf("gfitClient.SetCalories(%d) = %v", summary.Summary.CaloriesOut, err))
+               }
+               wg.Done()
+       }()
+
+       wg.Add(1)
+       go func() {
+               var distanceMeters float64
+               for _, d := range summary.Summary.Distances {
+                       if d.Activity != "total" {
+                               continue
+                       }
+                       distanceMeters = 1000.0 * d.Distance
+                       break
+               }
+               if err := gfitClient.SetDistance(ctx, distanceMeters, tm); err != nil {
+                       errs = append(errs, fmt.Errorf("gfitClient.SetDistance(%d) = %v", distanceMeters, err))
+               }
+               wg.Done()
+       }()
+
+       wg.Add(1)
+       go func() {
+               if err := gfitClient.SetHeartRate(ctx, summary.Summary.HeartRateZones, summary.Summary.RestingHeartRate, tm); err != nil {
+                       errs = append(errs, fmt.Errorf("gfitClient.SetHeartRate() = %v", err))
+               }
+               wg.Done()
+       }()
+
+       wg.Wait()
+
+       if len(errs) != 0 {
+               return errs
+       }
        return nil
 }