From a1803b210e15430a724d10e7370fd0a0e3256f8b Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Thu, 11 Jan 2018 13:19:56 +0100 Subject: [PATCH] Package gfit: Implement initial client code for Google Fit. --- gfit/gfit.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ gfitsync.go | 52 +++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 gfit/gfit.go diff --git a/gfit/gfit.go b/gfit/gfit.go new file mode 100644 index 0000000..028eee0 --- /dev/null +++ b/gfit/gfit.go @@ -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 +} diff --git a/gfitsync.go b/gfitsync.go index 25df282..2bc3b73 100644 --- a/gfitsync.go +++ b/gfitsync.go @@ -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, "

Fitbit to Google Fit sync

") fmt.Fprintf(w, "

Hello %s

\n", user.Current(ctx).Email) - fmt.Fprint(w, "

Fitbit: ") - if haveToken { + fmt.Fprintln(w, "

") fmt.Fprintln(w, "") 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. -- 2.11.0