11 "github.com/octo/gfitsync/app"
12 "github.com/octo/gfitsync/fitbit"
13 "github.com/octo/gfitsync/gfit"
14 "google.golang.org/appengine"
15 "google.golang.org/appengine/datastore"
16 "google.golang.org/appengine/delay"
17 "google.golang.org/appengine/log"
18 "google.golang.org/appengine/user"
21 var delayedHandleNotifications = delay.Func("handleNotifications", handleNotifications)
24 http.HandleFunc("/fitbit/setup", fitbitSetupHandler)
25 http.Handle("/fitbit/grant", AuthenticatedHandler(fitbitGrantHandler))
26 http.Handle("/fitbit/notify", ContextHandler(fitbitNotifyHandler))
27 http.HandleFunc("/google/setup", googleSetupHandler)
28 http.Handle("/google/grant", AuthenticatedHandler(googleGrantHandler))
29 http.Handle("/", AuthenticatedHandler(indexHandler))
32 // ContextHandler implements http.Handler
33 type ContextHandler func(context.Context, http.ResponseWriter, *http.Request) error
35 func (hndl ContextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
36 ctx := appengine.NewContext(r)
38 if err := hndl(ctx, w, r); err != nil {
39 http.Error(w, err.Error(), http.StatusInternalServerError)
44 type AuthenticatedHandler func(context.Context, http.ResponseWriter, *http.Request, *app.User) error
46 func (hndl AuthenticatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
47 ctx := appengine.NewContext(r)
49 gaeUser := user.Current(ctx)
51 url, err := user.LoginURL(ctx, r.URL.String())
53 http.Error(w, err.Error(), http.StatusInternalServerError)
56 http.Redirect(w, r, url, http.StatusTemporaryRedirect)
60 u, err := app.NewUser(ctx, gaeUser.Email)
62 http.Error(w, err.Error(), http.StatusInternalServerError)
66 if err := hndl(ctx, w, r, u); err != nil {
67 http.Error(w, err.Error(), http.StatusInternalServerError)
72 func indexHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
73 _, err := u.Token(ctx, "Fitbit")
74 if err != nil && err != datastore.ErrNoSuchEntity {
77 haveFitbitToken := err == nil
79 _, err = u.Token(ctx, "Google")
80 if err != nil && err != datastore.ErrNoSuchEntity {
83 haveGoogleToken := err == nil
85 fmt.Fprintln(w, "<html><body><h1>Fitbit to Google Fit sync</h1>")
86 fmt.Fprintf(w, "<p>Hello %s</p>\n", user.Current(ctx).Email)
87 fmt.Fprintln(w, "<ul>")
89 fmt.Fprint(w, "<li>Fitbit: ")
91 fmt.Fprint(w, `<strong style="color: DarkGreen;">Authorized</strong>`)
93 fmt.Fprint(w, `<strong style="color: DarkRed;">Not authorized</strong> (<a href="/fitbit/setup">Authorize</a>)`)
95 fmt.Fprintln(w, "</li>")
97 fmt.Fprint(w, "<li>Google: ")
99 fmt.Fprint(w, `<strong style="color: DarkGreen;">Authorized</strong>`)
101 fmt.Fprint(w, `<strong style="color: DarkRed;">Not authorized</strong> (<a href="/google/setup">Authorize</a>)`)
103 fmt.Fprintln(w, "</li>")
105 fmt.Fprintln(w, "</ul>")
106 fmt.Fprintln(w, "</body></html>")
111 func fitbitSetupHandler(w http.ResponseWriter, r *http.Request) {
112 http.Redirect(w, r, fitbit.AuthURL(), http.StatusTemporaryRedirect)
115 func fitbitGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
116 if err := fitbit.ParseToken(ctx, r, u); err != nil {
119 c, err := fitbit.NewClient(ctx, "-", u)
124 if err := c.Subscribe(ctx, "activities"); err != nil {
125 return fmt.Errorf("c.Subscribe() = %v", err)
129 redirectURL.Path = "/"
130 redirectURL.RawQuery = ""
131 redirectURL.Fragment = ""
132 http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
136 func googleSetupHandler(w http.ResponseWriter, r *http.Request) {
137 http.Redirect(w, r, gfit.AuthURL(), http.StatusTemporaryRedirect)
140 func googleGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
141 if err := gfit.ParseToken(ctx, r, u); err != nil {
146 redirectURL.Path = "/"
147 redirectURL.RawQuery = ""
148 redirectURL.Fragment = ""
149 http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
153 // fitbitNotifyHandler is called by Fitbit whenever there are updates to a
154 // subscription. It verifies the payload, splits it into individual
155 // notifications and adds it to the taskqueue service.
156 func fitbitNotifyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
159 fitbitTimeout := 3 * time.Second
160 ctx, cancel := context.WithTimeout(ctx, fitbitTimeout)
163 // this is used when setting up a new subscriber in the UI. Once set
164 // up, this code path should not be triggered.
165 if verify := r.FormValue("verify"); verify != "" {
166 if verify == "@FITBIT_SUBSCRIBER_CODE@" {
167 w.WriteHeader(http.StatusNoContent)
169 w.WriteHeader(http.StatusNotFound)
174 data, err := ioutil.ReadAll(r.Body)
179 // Fitbit recommendation: "If signature verification fails, you should
180 // respond with a 404"
181 if !fitbit.CheckSignature(ctx, data, r.Header.Get("X-Fitbit-Signature")) {
182 w.WriteHeader(http.StatusNotFound)
186 if err := delayedHandleNotifications.Call(ctx, data); err != nil {
190 w.WriteHeader(http.StatusCreated)
194 // handleNotifications parses fitbit notifications and requests the individual
195 // activities from Fitbit. It is executed asynchronously via the delay package.
196 func handleNotifications(ctx context.Context, payload []byte) error {
197 var subscriptions []fitbit.Subscription
198 if err := json.Unmarshal(payload, &subscriptions); err != nil {
202 for _, s := range subscriptions {
203 if s.CollectionType != "activities" {
207 if err := handleNotification(ctx, &s); err != nil {
208 log.Errorf(ctx, "handleNotification() = %v", err)
216 func handleNotification(ctx context.Context, s *fitbit.Subscription) error {
217 u, err := app.UserByID(ctx, s.SubscriptionID)
221 c, err := fitbit.NewClient(ctx, s.OwnerID, u)
226 tm, err := time.Parse("2006-01-02", s.Date)
231 summary, err := c.ActivitySummary(tm)
236 log.Debugf(ctx, "ActivitySummary for %s = %+v", u.Email, summary)