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
43 func (hndl AuthenticatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
44 ctx := appengine.NewContext(r)
46 gaeUser := user.Current(ctx)
48 url, err := user.LoginURL(ctx, r.URL.String())
50 http.Error(w, err.Error(), http.StatusInternalServerError)
53 http.Redirect(w, r, url, http.StatusTemporaryRedirect)
57 u, err := app.NewUser(ctx, gaeUser.Email)
59 http.Error(w, err.Error(), http.StatusInternalServerError)
63 if err := hndl(ctx, w, r, u); err != nil {
64 http.Error(w, err.Error(), http.StatusInternalServerError)
69 func indexHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
70 _, err := u.Token(ctx, "Fitbit")
71 if err != nil && err != datastore.ErrNoSuchEntity {
74 haveToken := err == nil
76 fmt.Fprintln(w, "<html><body><h1>Fitbit to Google Fit sync</h1>")
77 fmt.Fprintf(w, "<p>Hello %s</p>\n", user.Current(ctx).Email)
78 fmt.Fprint(w, "<p>Fitbit: ")
80 fmt.Fprint(w, `<strong style="color: DarkGreen;">Authorized</strong>`)
82 fmt.Fprint(w, `<strong style="color: DarkRed;">Not authorized</strong> (<a href="/setup">Authorize</a>)`)
84 fmt.Fprintln(w, "</p>")
85 fmt.Fprintln(w, "</body></html>")
90 func setupHandler(w http.ResponseWriter, r *http.Request) {
91 http.Redirect(w, r, fitbit.AuthURL(), http.StatusTemporaryRedirect)
94 func fitbitGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
95 if err := fitbit.ParseToken(ctx, r, u); err != nil {
98 c, err := fitbit.NewClient(ctx, "-", u)
103 if err := c.Subscribe(ctx, "activities"); err != nil {
104 return fmt.Errorf("c.Subscribe() = %v", err)
108 redirectURL.Path = "/"
109 redirectURL.RawQuery = ""
110 redirectURL.Fragment = ""
111 http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
115 // fitbitNotifyHandler is called by Fitbit whenever there are updates to a
116 // subscription. It verifies the payload, splits it into individual
117 // notifications and adds it to the taskqueue service.
118 func fitbitNotifyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
121 fitbitTimeout := 3 * time.Second
122 ctx, cancel := context.WithTimeout(ctx, fitbitTimeout)
125 // this is used when setting up a new subscriber in the UI. Once set
126 // up, this code path should not be triggered.
127 if verify := r.FormValue("verify"); verify != "" {
128 if verify == "@FITBIT_SUBSCRIBER_CODE@" {
129 w.WriteHeader(http.StatusNoContent)
131 w.WriteHeader(http.StatusNotFound)
136 data, err := ioutil.ReadAll(r.Body)
141 // Fitbit recommendation: "If signature verification fails, you should
142 // respond with a 404"
143 if !fitbit.CheckSignature(ctx, data, r.Header.Get("X-Fitbit-Signature")) {
144 w.WriteHeader(http.StatusNotFound)
148 if err := delayedHandleNotifications.Call(ctx, data); err != nil {
152 w.WriteHeader(http.StatusCreated)
156 // handleNotifications parses fitbit notifications and requests the individual
157 // activities from Fitbit. It is executed asynchronously via the delay package.
158 func handleNotifications(ctx context.Context, payload []byte) error {
159 var subscriptions []fitbit.Subscription
160 if err := json.Unmarshal(payload, &subscriptions); err != nil {
164 for _, s := range subscriptions {
165 if s.CollectionType != "activities" {
169 if err := handleNotification(ctx, &s); err != nil {
170 log.Errorf(ctx, "handleNotification() = %v", err)
178 func handleNotification(ctx context.Context, s *fitbit.Subscription) error {
179 u, err := app.UserByID(ctx, s.SubscriptionID)
183 c, err := fitbit.NewClient(ctx, s.OwnerID, u)
188 tm, err := time.Parse("2006-01-02", s.Date)
193 summary, err := c.ActivitySummary(tm)
198 log.Debugf(ctx, "ActivitySummary for %s = %+v", u.Email, summary)