X-Git-Url: https://git.octo.it/?p=kraftakt.git;a=blobdiff_plain;f=kraftakt.go;h=72389bd1430b36450a071d09d8a491597d4805d7;hp=e0ad321e325d18b34d453c31893f74c3b8009b63;hb=HEAD;hpb=304308a757d192b9b80455017bda1cb82889d346
diff --git a/kraftakt.go b/kraftakt.go
index e0ad321..72389bd 100644
--- a/kraftakt.go
+++ b/kraftakt.go
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
+ "html/template"
"io/ioutil"
"net/http"
"sync"
@@ -21,13 +22,31 @@ import (
var delayedHandleNotifications = delay.Func("handleNotifications", handleNotifications)
+var templates *template.Template
+
func init() {
- http.HandleFunc("/fitbit/setup", fitbitSetupHandler)
+ http.Handle("/login", AuthenticatedHandler(loginHandler))
+ http.Handle("/fitbit/connect", AuthenticatedHandler(fitbitConnectHandler))
http.Handle("/fitbit/grant", AuthenticatedHandler(fitbitGrantHandler))
- http.Handle("/fitbit/notify", ContextHandler(fitbitNotifyHandler))
- http.HandleFunc("/google/setup", googleSetupHandler)
+ http.Handle("/fitbit/disconnect", AuthenticatedHandler(fitbitDisconnectHandler))
+ http.Handle("/google/connect", AuthenticatedHandler(googleConnectHandler))
http.Handle("/google/grant", AuthenticatedHandler(googleGrantHandler))
- http.Handle("/", AuthenticatedHandler(indexHandler))
+ http.Handle("/google/disconnect", AuthenticatedHandler(googleDisconnectHandler))
+ // unauthenticated
+ http.Handle("/fitbit/notify", ContextHandler(fitbitNotifyHandler))
+ http.Handle("/", ContextHandler(indexHandler))
+
+ t, err := template.ParseGlob("templates/*.html")
+ if err != nil {
+ panic(err)
+ }
+ templates = t
+}
+
+func internalServerError(ctx context.Context, w http.ResponseWriter, err error) {
+ log.Errorf(ctx, "%v", err)
+
+ http.Error(w, "Internal Server Error\n\nReference: "+appengine.RequestID(ctx), http.StatusInternalServerError)
}
// ContextHandler implements http.Handler
@@ -36,8 +55,13 @@ type ContextHandler func(context.Context, http.ResponseWriter, *http.Request) er
func (hndl ContextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)
+ if err := app.LoadConfig(ctx); err != nil {
+ internalServerError(ctx, w, fmt.Errorf("LoadConfig() = %v", err))
+ return
+ }
+
if err := hndl(ctx, w, r); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ internalServerError(ctx, w, err)
return
}
}
@@ -47,11 +71,16 @@ type AuthenticatedHandler func(context.Context, http.ResponseWriter, *http.Reque
func (hndl AuthenticatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)
+ if err := app.LoadConfig(ctx); err != nil {
+ internalServerError(ctx, w, fmt.Errorf("LoadConfig() = %v", err))
+ return
+ }
+
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)
+ internalServerError(ctx, w, fmt.Errorf("LoginURL() = %v", err))
return
}
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
@@ -60,68 +89,69 @@ func (hndl AuthenticatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
u, err := app.NewUser(ctx, gaeUser.Email)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ internalServerError(ctx, w, fmt.Errorf("NewUser(%q) = %v", gaeUser.Email, err))
return
}
if err := hndl(ctx, w, r, u); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ internalServerError(ctx, w, err)
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
+func indexHandler(ctx context.Context, w http.ResponseWriter, _ *http.Request) error {
+ var templateData struct {
+ HaveFitbit bool
+ HaveGoogleFit bool
+ *app.User
}
- haveGoogleToken := err == nil
+ templateName := "main.html"
- fmt.Fprintln(w, "
Kraftakt")
- fmt.Fprintln(w, "Kraftakt
")
+ if gaeUser := user.Current(ctx); gaeUser != nil {
+ templateName = "loggedin.html"
- fmt.Fprintln(w, "Kraftakt copies your Fitbit data to Google Fit, seconds after you sync.
")
-
- fmt.Fprintf(w, "Hello %s
\n", user.Current(ctx).Email)
- fmt.Fprintln(w, "")
+ u, err := app.NewUser(ctx, gaeUser.Email)
+ if err != nil {
+ return err
+ }
+ templateData.User = u
- fmt.Fprint(w, "- Fitbit: ")
- if haveFitbitToken {
- fmt.Fprint(w, `Authorized`)
- } else {
- fmt.Fprint(w, `Not authorized (Authorize)`)
- }
- fmt.Fprintln(w, "
")
+ _, err = u.Token(ctx, "Fitbit")
+ if err != nil && err != datastore.ErrNoSuchEntity {
+ return err
+ }
+ templateData.HaveFitbit = (err == nil)
- fmt.Fprint(w, "- Google Fit: ")
- if haveGoogleToken {
- fmt.Fprint(w, `Authorized`)
- } else {
- fmt.Fprint(w, `Not authorized (Authorize)`)
+ _, err = u.Token(ctx, "Google")
+ if err != nil && err != datastore.ErrNoSuchEntity {
+ return err
+ }
+ templateData.HaveGoogleFit = (err == nil)
}
- fmt.Fprintln(w, "
")
- fmt.Fprintln(w, "
")
- fmt.Fprintln(w, "")
+ return templates.ExecuteTemplate(w, templateName, &templateData)
+}
+func loginHandler(_ context.Context, w http.ResponseWriter, r *http.Request, _ *app.User) error {
+ // essentially a nop; all the heavy lifting (i.e. logging in) has been done by the AuthenticatedHandler wrapper.
+ redirectURL := r.URL
+ redirectURL.Path = "/"
+ redirectURL.RawQuery = ""
+ redirectURL.Fragment = ""
+ http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
return nil
}
-func fitbitSetupHandler(w http.ResponseWriter, r *http.Request) {
- http.Redirect(w, r, fitbit.AuthURL(), http.StatusTemporaryRedirect)
+func fitbitConnectHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
+ http.Redirect(w, r, fitbit.AuthURL(ctx, u), http.StatusTemporaryRedirect)
+ return nil
}
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)
+ c, err := fitbit.NewClient(ctx, "", u)
if err != nil {
return err
}
@@ -141,8 +171,31 @@ 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 fitbitDisconnectHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
+ c, err := fitbit.NewClient(ctx, "", u)
+ if err != nil {
+ return err
+ }
+
+ if err := c.UnsubscribeAll(ctx); err != nil {
+ return fmt.Errorf("UnsubscribeAll() = %v", err)
+ }
+
+ if err := c.DeleteToken(ctx); err != nil {
+ return err
+ }
+
+ redirectURL := r.URL
+ redirectURL.Path = "/"
+ redirectURL.RawQuery = ""
+ redirectURL.Fragment = ""
+ http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
+ return nil
+}
+
+func googleConnectHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
+ http.Redirect(w, r, gfit.AuthURL(ctx, u), http.StatusTemporaryRedirect)
+ return nil
}
func googleGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
@@ -158,6 +211,24 @@ func googleGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Requ
return nil
}
+func googleDisconnectHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
+ c, err := gfit.NewClient(ctx, u)
+ if err != nil {
+ return err
+ }
+
+ if err := c.DeleteToken(ctx); 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.
@@ -171,7 +242,7 @@ func fitbitNotifyHandler(ctx context.Context, w http.ResponseWriter, r *http.Req
// 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@" {
+ if verify == app.Config.FitbitSubscriberCode {
w.WriteHeader(http.StatusNoContent)
} else {
w.WriteHeader(http.StatusNotFound)
@@ -187,6 +258,7 @@ func fitbitNotifyHandler(ctx context.Context, w http.ResponseWriter, r *http.Req
// Fitbit recommendation: "If signature verification fails, you should
// respond with a 404"
if !fitbit.CheckSignature(ctx, data, r.Header.Get("X-Fitbit-Signature")) {
+ log.Errorf(ctx, "signature mismatch")
w.WriteHeader(http.StatusNotFound)
return nil
}
@@ -202,28 +274,49 @@ func fitbitNotifyHandler(ctx context.Context, w http.ResponseWriter, r *http.Req
// 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 {
+ log.Debugf(ctx, "NOTIFY -> %s", payload)
+
+ if err := app.LoadConfig(ctx); err != nil {
+ return err
+ }
+
var subscriptions []fitbit.Subscription
if err := json.Unmarshal(payload, &subscriptions); err != nil {
return err
}
+ wg := &sync.WaitGroup{}
+
for _, s := range subscriptions {
- if s.CollectionType != "activities" {
+ switch s.CollectionType {
+ case "activities":
+ wg.Add(1)
+ go func(s fitbit.Subscription) {
+ defer wg.Done()
+ if err := activitiesNotification(ctx, &s); err != nil {
+ log.Warningf(ctx, "activitiesNotification() = %v", err)
+ }
+ }(s) // copies s
+ case "sleep":
+ wg.Add(1)
+ go func(s fitbit.Subscription) {
+ defer wg.Done()
+ if err := sleepNotification(ctx, &s); err != nil {
+ log.Warningf(ctx, "sleepNotification() = %v", err)
+ }
+ }(s) // copies s
+ default:
log.Warningf(ctx, "ignoring collection type %q", s.CollectionType)
- continue
- }
- if err := handleNotification(ctx, &s); err != nil {
- log.Errorf(ctx, "handleNotification() = %v", err)
- continue
}
}
+ wg.Wait()
return nil
}
-func handleNotification(ctx context.Context, s *fitbit.Subscription) error {
- u, err := app.UserByID(ctx, s.SubscriptionID)
+func activitiesNotification(ctx context.Context, s *fitbit.Subscription) error {
+ u, err := fitbit.UserFromSubscriberID(ctx, s.SubscriptionID)
if err != nil {
return err
}
@@ -307,7 +400,7 @@ func handleNotification(ctx context.Context, s *fitbit.Subscription) error {
break
}
if err := gfitClient.SetDistance(ctx, distanceMeters, tm); err != nil {
- errs = append(errs, fmt.Errorf("gfitClient.SetDistance(%d) = %v", distanceMeters, err))
+ errs = append(errs, fmt.Errorf("gfitClient.SetDistance(%g) = %v", distanceMeters, err))
return
}
}()
@@ -340,7 +433,7 @@ func handleNotification(ctx context.Context, s *fitbit.Subscription) error {
activities = append(activities, gfit.Activity{
Start: startTime,
End: endTime,
- Type: gfit.ParseFitbitActivity(a.Name),
+ Type: a.Name,
})
}
if err := gfitClient.SetActivities(ctx, activities, tm); err != nil {
@@ -356,3 +449,78 @@ func handleNotification(ctx context.Context, s *fitbit.Subscription) error {
}
return nil
}
+
+func sleepNotification(ctx context.Context, s *fitbit.Subscription) error {
+ u, err := fitbit.UserFromSubscriberID(ctx, s.SubscriptionID)
+ if err != nil {
+ return err
+ }
+
+ var (
+ wg = &sync.WaitGroup{}
+ gfitClient *gfit.Client
+ gfitErr error
+ )
+
+ wg.Add(1)
+ go func() {
+ gfitClient, gfitErr = gfit.NewClient(ctx, u)
+ wg.Done()
+ }()
+
+ fitbitClient, err := fitbit.NewClient(ctx, s.OwnerID, u)
+ if err != nil {
+ return err
+ }
+
+ profile, err := fitbitClient.Profile(ctx)
+ if err != nil {
+ return err
+ }
+
+ tm, err := time.ParseInLocation("2006-01-02", s.Date, profile.Timezone)
+ if err != nil {
+ return err
+ }
+
+ sleep, err := fitbitClient.Sleep(ctx, tm)
+ if err != nil {
+ return err
+ }
+ log.Debugf(ctx, "fitbitClient.Sleep(%v) returned %d sleep stages", tm, len(sleep.Stages))
+
+ var activities []gfit.Activity
+ for _, stg := range sleep.Stages {
+ a := gfit.Activity{
+ Start: stg.StartTime,
+ End: stg.EndTime,
+ }
+ switch stg.Level {
+ case fitbit.SleepLevelDeep:
+ a.Type = "Deep sleep"
+ case fitbit.SleepLevelLight:
+ a.Type = "Light sleep"
+ case fitbit.SleepLevelREM:
+ a.Type = "REM sleep"
+ case fitbit.SleepLevelWake:
+ a.Type = "Awake (during sleep cycle)"
+ default:
+ log.Warningf(ctx, "unexpected sleep level %v", stg.Level)
+ continue
+ }
+
+ activities = append(activities, a)
+ }
+
+ wg.Wait()
+ if gfitErr != nil {
+ return gfitErr
+ }
+
+ log.Debugf(ctx, "passing %d activities to gfitClient.SetActivities()", len(activities))
+ if err := gfitClient.SetActivities(ctx, activities, tm); err != nil {
+ return fmt.Errorf("SetActivities() = %v", err)
+ }
+
+ return nil
+}