Package gfit: Implement initial client code for Google Fit.
authorFlorian Forster <ff@octo.it>
Thu, 11 Jan 2018 12:19:56 +0000 (13:19 +0100)
committerFlorian Forster <ff@octo.it>
Thu, 11 Jan 2018 12:19:56 +0000 (13:19 +0100)
gfit/gfit.go [new file with mode: 0644]
gfitsync.go

diff --git a/gfit/gfit.go b/gfit/gfit.go
new file mode 100644 (file)
index 0000000..028eee0
--- /dev/null
@@ -0,0 +1,62 @@
+package gfit
+
+import (
+       "context"
+       "fmt"
+       "net/http"
+
+       "github.com/octo/gfitsync/app"
+       "golang.org/x/oauth2"
+       oauth2google "golang.org/x/oauth2/google"
+       fitness "google.golang.org/api/fitness/v1"
+)
+
+var oauthConfig = &oauth2.Config{
+       ClientID:     "@GOOGLE_CLIENT_ID@",
+       ClientSecret: "@GOOGLE_CLIENT_SECRET@",
+       Endpoint:     oauth2google.Endpoint,
+       RedirectURL:  "https://fitbit-gfit-sync.appspot.com/google/grant",
+       Scopes: []string{
+               fitness.FitnessActivityWriteScope,
+               fitness.FitnessBodyWriteScope,
+       },
+}
+
+const csrfToken = "@CSRFTOKEN@"
+
+func AuthURL() string {
+       return oauthConfig.AuthCodeURL(csrfToken, oauth2.AccessTypeOffline)
+}
+
+func ParseToken(ctx context.Context, r *http.Request, u *app.User) error {
+       if state := r.FormValue("state"); state != csrfToken {
+               return fmt.Errorf("invalid state parameter: %q", state)
+       }
+
+       tok, err := oauthConfig.Exchange(ctx, r.FormValue("code"))
+       if err != nil {
+               return err
+       }
+
+       return u.SetToken(ctx, "Google", tok)
+}
+
+type Client struct {
+       *fitness.Service
+}
+
+func NewClient(ctx context.Context, u *app.User) (*Client, error) {
+       c, err := u.OAuthClient(ctx, "Google", oauthConfig)
+       if err != nil {
+               return nil, err
+       }
+
+       service, err := fitness.New(c)
+       if err != nil {
+               return nil, err
+       }
+
+       return &Client{
+               Service: service,
+       }, nil
+}
index 25df282..2bc3b73 100644 (file)
@@ -10,6 +10,7 @@ import (
 
        "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 +21,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))
 }
 
@@ -71,23 +74,41 @@ 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><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: ")
+       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) {
+func fitbitSetupHandler(w http.ResponseWriter, r *http.Request) {
        http.Redirect(w, r, fitbit.AuthURL(), http.StatusTemporaryRedirect)
 }
 
@@ -112,6 +133,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.