Rename the "gfitsync" package to "kraftakt".
authorFlorian Forster <ff@octo.it>
Sun, 28 Jan 2018 20:03:25 +0000 (21:03 +0100)
committerFlorian Forster <ff@octo.it>
Sun, 28 Jan 2018 20:03:25 +0000 (21:03 +0100)
fitbit/fitbit.go
gfit/gfit.go
gfitsync.go [deleted file]
kraftakt.go [new file with mode: 0644]

index bde96ff..90457d2 100644 (file)
@@ -11,7 +11,7 @@ import (
        "net/http"
        "time"
 
-       "github.com/octo/gfitsync/app"
+       "github.com/octo/kraftakt/app"
        "golang.org/x/oauth2"
        oauth2fitbit "golang.org/x/oauth2/fitbit"
        "google.golang.org/appengine/log"
index b6e9bc0..69308a7 100644 (file)
@@ -7,8 +7,8 @@ import (
        "strings"
        "time"
 
-       "github.com/octo/gfitsync/app"
-       "github.com/octo/gfitsync/fitbit"
+       "github.com/octo/kraftakt/app"
+       "github.com/octo/kraftakt/fitbit"
        "golang.org/x/oauth2"
        oauth2google "golang.org/x/oauth2/google"
        fitness "google.golang.org/api/fitness/v1"
diff --git a/gfitsync.go b/gfitsync.go
deleted file mode 100644 (file)
index 9eadde9..0000000
+++ /dev/null
@@ -1,358 +0,0 @@
-package gfitsync
-
-import (
-       "context"
-       "encoding/json"
-       "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"
-       "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, "<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.Fprintf(w, "<p>Hello %s</p>\n", user.Current(ctx).Email)
-       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="/google/setup">Authorize</a>)`)
-       }
-       fmt.Fprintln(w, "</li>")
-
-       fmt.Fprintln(w, "</ul>")
-       fmt.Fprintln(w, "</body></html>")
-
-       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
-       }
-
-       for _, collection := range []string{"activities", "sleep"} {
-               if err := c.Subscribe(ctx, collection); err != nil {
-                       return fmt.Errorf("c.Subscribe(%q) = %v", collection, err)
-               }
-               log.Infof(ctx, "Successfully subscribed to %q", collection)
-       }
-
-       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" {
-                       log.Warningf(ctx, "ignoring collection type %q", s.CollectionType)
-                       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
-       }
-
-       fitbitClient, err := fitbit.NewClient(ctx, s.OwnerID, u)
-       if err != nil {
-               return err
-       }
-
-       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
-       }
-
-       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
-       }
-
-       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() {
-               defer wg.Done()
-
-               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))
-                       return
-               }
-       }()
-
-       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.Add(1)
-       go func() {
-               defer wg.Done()
-
-               var activities []gfit.Activity
-               for _, a := range summary.Activities {
-                       if !a.HasStartTime {
-                               continue
-                       }
-
-                       startTime, err := time.ParseInLocation("2006-01-02T15:04", a.StartDate+"T"+a.StartTime, profile.Timezone)
-                       if err != nil {
-                               errs = append(errs, fmt.Errorf("gfitClient.SetActivities() = %v", err))
-                               return
-                       }
-                       endTime := startTime.Add(time.Duration(a.Duration) * time.Millisecond)
-
-                       activities = append(activities, gfit.Activity{
-                               Start: startTime,
-                               End:   endTime,
-                               Type:  gfit.ParseFitbitActivity(a.Name),
-                       })
-               }
-               if err := gfitClient.SetActivities(ctx, activities, tm); err != nil {
-                       errs = append(errs, fmt.Errorf("gfitClient.SetActivities() = %v", err))
-                       return
-               }
-       }()
-
-       wg.Wait()
-
-       if len(errs) != 0 {
-               return errs
-       }
-       return nil
-}
diff --git a/kraftakt.go b/kraftakt.go
new file mode 100644 (file)
index 0000000..e0ad321
--- /dev/null
@@ -0,0 +1,358 @@
+package kraftakt
+
+import (
+       "context"
+       "encoding/json"
+       "fmt"
+       "io/ioutil"
+       "net/http"
+       "sync"
+       "time"
+
+       "github.com/octo/kraftakt/app"
+       "github.com/octo/kraftakt/fitbit"
+       "github.com/octo/kraftakt/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, "<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.Fprintf(w, "<p>Hello %s</p>\n", user.Current(ctx).Email)
+       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="/google/setup">Authorize</a>)`)
+       }
+       fmt.Fprintln(w, "</li>")
+
+       fmt.Fprintln(w, "</ul>")
+       fmt.Fprintln(w, "</body></html>")
+
+       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
+       }
+
+       for _, collection := range []string{"activities", "sleep"} {
+               if err := c.Subscribe(ctx, collection); err != nil {
+                       return fmt.Errorf("c.Subscribe(%q) = %v", collection, err)
+               }
+               log.Infof(ctx, "Successfully subscribed to %q", collection)
+       }
+
+       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" {
+                       log.Warningf(ctx, "ignoring collection type %q", s.CollectionType)
+                       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
+       }
+
+       fitbitClient, err := fitbit.NewClient(ctx, s.OwnerID, u)
+       if err != nil {
+               return err
+       }
+
+       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
+       }
+
+       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
+       }
+
+       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() {
+               defer wg.Done()
+
+               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))
+                       return
+               }
+       }()
+
+       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.Add(1)
+       go func() {
+               defer wg.Done()
+
+               var activities []gfit.Activity
+               for _, a := range summary.Activities {
+                       if !a.HasStartTime {
+                               continue
+                       }
+
+                       startTime, err := time.ParseInLocation("2006-01-02T15:04", a.StartDate+"T"+a.StartTime, profile.Timezone)
+                       if err != nil {
+                               errs = append(errs, fmt.Errorf("gfitClient.SetActivities() = %v", err))
+                               return
+                       }
+                       endTime := startTime.Add(time.Duration(a.Duration) * time.Millisecond)
+
+                       activities = append(activities, gfit.Activity{
+                               Start: startTime,
+                               End:   endTime,
+                               Type:  gfit.ParseFitbitActivity(a.Name),
+                       })
+               }
+               if err := gfitClient.SetActivities(ctx, activities, tm); err != nil {
+                       errs = append(errs, fmt.Errorf("gfitClient.SetActivities() = %v", err))
+                       return
+               }
+       }()
+
+       wg.Wait()
+
+       if len(errs) != 0 {
+               return errs
+       }
+       return nil
+}