11 "github.com/octo/gfitsync/app"
12 "github.com/octo/gfitsync/fitbit"
13 "google.golang.org/appengine"
14 "google.golang.org/appengine/datastore"
15 "google.golang.org/appengine/delay"
16 "google.golang.org/appengine/log"
17 "google.golang.org/appengine/user"
20 var delayedHandleNotifications = delay.Func("handleNotifications", handleNotifications)
23 http.HandleFunc("/setup", setupHandler)
24 http.Handle("/fitbit/grant", AuthenticatedHandler(fitbitGrantHandler))
25 http.Handle("/fitbit/notify", ContextHandler(fitbitNotifyHandler))
26 http.Handle("/", AuthenticatedHandler(indexHandler))
29 // ContextHandler implements http.Handler
30 type ContextHandler func(context.Context, http.ResponseWriter, *http.Request) error
32 func (hndl ContextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
33 ctx := appengine.NewContext(r)
35 if err := hndl(ctx, w, r); err != nil {
36 http.Error(w, err.Error(), http.StatusInternalServerError)
41 type AuthenticatedHandler func(context.Context, http.ResponseWriter, *http.Request, *app.User) error
47 func (hndl AuthenticatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
48 ctx := appengine.NewContext(r)
50 gaeUser := user.Current(ctx)
52 url, err := user.LoginURL(ctx, r.URL.String())
54 http.Error(w, err.Error(), http.StatusInternalServerError)
57 http.Redirect(w, r, url, http.StatusTemporaryRedirect)
61 u, err := app.NewUser(ctx, gaeUser.Email)
63 http.Error(w, err.Error(), http.StatusInternalServerError)
67 if err := hndl(ctx, w, r, u); err != nil {
68 http.Error(w, err.Error(), http.StatusInternalServerError)
73 func indexHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
74 _, err := u.Token(ctx, "Fitbit")
75 if err != nil && err != datastore.ErrNoSuchEntity {
78 haveToken := err == nil
80 fmt.Fprintln(w, "<html><body><h1>Fitbit to Google Fit sync</h1>")
81 fmt.Fprintf(w, "<p>Hello %s</p>\n", user.Current(ctx).Email)
82 fmt.Fprint(w, "<p>Fitbit: ")
84 fmt.Fprint(w, `<strong style="color: DarkGreen;">Authorized</strong>`)
86 fmt.Fprint(w, `<strong style="color: DarkRed;">Not authorized</strong> (<a href="/setup">Authorize</a>)`)
88 fmt.Fprintln(w, "</p>")
89 fmt.Fprintln(w, "</body></html>")
94 func setupHandler(w http.ResponseWriter, r *http.Request) {
95 url := fitbit.AuthURL()
96 http.Redirect(w, r, url, http.StatusTemporaryRedirect)
99 func fitbitGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
100 if err := fitbit.ParseToken(ctx, r, u); err != nil {
103 c, err := fitbit.NewClient(ctx, "-", u)
108 if err := c.Subscribe(ctx, "activities"); err != nil {
109 return fmt.Errorf("c.Subscribe() = %v", err)
113 redirectURL.Path = "/"
114 redirectURL.RawQuery = ""
115 redirectURL.Fragment = ""
116 http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
120 // fitbitNotifyHandler is called by Fitbit whenever there are updates to a
121 // subscription. It verifies the payload, splits it into individual
122 // notifications and adds it to the taskqueue service.
123 func fitbitNotifyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
126 fitbitTimeout := 3 * time.Second
127 ctx, cancel := context.WithTimeout(ctx, fitbitTimeout)
130 // this is used when setting up a new subscriber in the UI. Once set
131 // up, this code path should not be triggered.
132 if verify := r.FormValue("verify"); verify != "" {
133 if verify == "@FITBIT_SUBSCRIBER_CODE@" {
134 w.WriteHeader(http.StatusNoContent)
136 w.WriteHeader(http.StatusNotFound)
141 data, err := ioutil.ReadAll(r.Body)
146 // Fitbit recommendation: "If signature verification fails, you should
147 // respond with a 404"
148 if !fitbit.CheckSignature(ctx, data, r.Header.Get("X-Fitbit-Signature")) {
149 w.WriteHeader(http.StatusNotFound)
153 if err := delayedHandleNotifications.Call(ctx, data); err != nil {
157 w.WriteHeader(http.StatusCreated)
161 // handleNotifications parses fitbit notifications and requests the individual
162 // activities from Fitbit. It is executed asynchronously via the delay package.
163 func handleNotifications(ctx context.Context, payload []byte) error {
164 var subscriptions []fitbit.Subscription
165 if err := json.Unmarshal(payload, &subscriptions); err != nil {
169 for _, s := range subscriptions {
170 if s.CollectionType != "activities" {
174 if err := handleNotification(ctx, &s); err != nil {
175 log.Errorf(ctx, "handleNotification() = %v", err)
183 func handleNotification(ctx context.Context, s *fitbit.Subscription) error {
184 u, err := app.UserByID(ctx, s.SubscriptionID)
188 c, err := fitbit.NewClient(ctx, s.OwnerID, u)
193 tm, err := time.Parse("2006-01-02", s.Date)
198 summary, err := c.ActivitySummary(tm)
203 log.Debugf(ctx, "ActivitySummary for %s = %+v", u.Email, summary)