12 "github.com/octo/gfitsync/app"
13 "github.com/octo/gfitsync/fitbit"
14 "github.com/octo/gfitsync/gfit"
15 "google.golang.org/appengine"
16 "google.golang.org/appengine/datastore"
17 "google.golang.org/appengine/delay"
18 "google.golang.org/appengine/log"
19 "google.golang.org/appengine/user"
22 var delayedHandleNotifications = delay.Func("handleNotifications", handleNotifications)
25 http.HandleFunc("/fitbit/setup", fitbitSetupHandler)
26 http.Handle("/fitbit/grant", AuthenticatedHandler(fitbitGrantHandler))
27 http.Handle("/fitbit/notify", ContextHandler(fitbitNotifyHandler))
28 http.HandleFunc("/google/setup", googleSetupHandler)
29 http.Handle("/google/grant", AuthenticatedHandler(googleGrantHandler))
30 http.Handle("/", AuthenticatedHandler(indexHandler))
33 // ContextHandler implements http.Handler
34 type ContextHandler func(context.Context, http.ResponseWriter, *http.Request) error
36 func (hndl ContextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
37 ctx := appengine.NewContext(r)
39 if err := hndl(ctx, w, r); err != nil {
40 http.Error(w, err.Error(), http.StatusInternalServerError)
45 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 haveFitbitToken := err == nil
80 _, err = u.Token(ctx, "Google")
81 if err != nil && err != datastore.ErrNoSuchEntity {
84 haveGoogleToken := err == nil
86 fmt.Fprintln(w, "<html><head><title>Kraftakt</title></head>")
87 fmt.Fprintln(w, "<body><h1>Kraftakt</h1>")
89 fmt.Fprintln(w, "<p><strong>Kraftakt</strong> copies your <em>Fitbit</em> data to <em>Google Fit</em>, seconds after you sync.</p>")
91 fmt.Fprintf(w, "<p>Hello %s</p>\n", user.Current(ctx).Email)
92 fmt.Fprintln(w, "<ul>")
94 fmt.Fprint(w, "<li>Fitbit: ")
96 fmt.Fprint(w, `<strong style="color: DarkGreen;">Authorized</strong>`)
98 fmt.Fprint(w, `<strong style="color: DarkRed;">Not authorized</strong> (<a href="/fitbit/setup">Authorize</a>)`)
100 fmt.Fprintln(w, "</li>")
102 fmt.Fprint(w, "<li>Google Fit: ")
104 fmt.Fprint(w, `<strong style="color: DarkGreen;">Authorized</strong>`)
106 fmt.Fprint(w, `<strong style="color: DarkRed;">Not authorized</strong> (<a href="/google/setup">Authorize</a>)`)
108 fmt.Fprintln(w, "</li>")
110 fmt.Fprintln(w, "</ul>")
111 fmt.Fprintln(w, "</body></html>")
116 func fitbitSetupHandler(w http.ResponseWriter, r *http.Request) {
117 http.Redirect(w, r, fitbit.AuthURL(), http.StatusTemporaryRedirect)
120 func fitbitGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
121 if err := fitbit.ParseToken(ctx, r, u); err != nil {
124 c, err := fitbit.NewClient(ctx, "-", u)
129 if err := c.Subscribe(ctx, "activities"); err != nil {
130 return fmt.Errorf("c.Subscribe() = %v", err)
134 redirectURL.Path = "/"
135 redirectURL.RawQuery = ""
136 redirectURL.Fragment = ""
137 http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
141 func googleSetupHandler(w http.ResponseWriter, r *http.Request) {
142 http.Redirect(w, r, gfit.AuthURL(), http.StatusTemporaryRedirect)
145 func googleGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
146 if err := gfit.ParseToken(ctx, r, u); err != nil {
151 redirectURL.Path = "/"
152 redirectURL.RawQuery = ""
153 redirectURL.Fragment = ""
154 http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
158 // fitbitNotifyHandler is called by Fitbit whenever there are updates to a
159 // subscription. It verifies the payload, splits it into individual
160 // notifications and adds it to the taskqueue service.
161 func fitbitNotifyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
164 fitbitTimeout := 3 * time.Second
165 ctx, cancel := context.WithTimeout(ctx, fitbitTimeout)
168 // this is used when setting up a new subscriber in the UI. Once set
169 // up, this code path should not be triggered.
170 if verify := r.FormValue("verify"); verify != "" {
171 if verify == "@FITBIT_SUBSCRIBER_CODE@" {
172 w.WriteHeader(http.StatusNoContent)
174 w.WriteHeader(http.StatusNotFound)
179 data, err := ioutil.ReadAll(r.Body)
184 // Fitbit recommendation: "If signature verification fails, you should
185 // respond with a 404"
186 if !fitbit.CheckSignature(ctx, data, r.Header.Get("X-Fitbit-Signature")) {
187 w.WriteHeader(http.StatusNotFound)
191 if err := delayedHandleNotifications.Call(ctx, data); err != nil {
195 w.WriteHeader(http.StatusCreated)
199 // handleNotifications parses fitbit notifications and requests the individual
200 // activities from Fitbit. It is executed asynchronously via the delay package.
201 func handleNotifications(ctx context.Context, payload []byte) error {
202 var subscriptions []fitbit.Subscription
203 if err := json.Unmarshal(payload, &subscriptions); err != nil {
207 for _, s := range subscriptions {
208 if s.CollectionType != "activities" {
212 if err := handleNotification(ctx, &s); err != nil {
213 log.Errorf(ctx, "handleNotification() = %v", err)
221 func handleNotification(ctx context.Context, s *fitbit.Subscription) error {
222 u, err := app.UserByID(ctx, s.SubscriptionID)
227 fitbitClient, err := fitbit.NewClient(ctx, s.OwnerID, u)
232 profile, err := fitbitClient.Profile(ctx)
236 log.Debugf(ctx, "profile = %+v", profile)
238 tm, err := time.ParseInLocation("2006-01-02", s.Date, profile.Timezone)
243 summary, err := fitbitClient.ActivitySummary(ctx, tm)
247 log.Debugf(ctx, "%s (%s) took %d steps on %s",
248 profile.Name, u.Email, summary.Summary.Steps, s.Date)
250 gfitClient, err := gfit.NewClient(ctx, u)
255 var errs appengine.MultiError
256 wg := &sync.WaitGroup{}
260 if err := gfitClient.SetSteps(ctx, summary.Summary.Steps, tm); err != nil {
261 errs = append(errs, fmt.Errorf("gfitClient.SetSteps(%d) = %v", summary.Summary.Steps, err))
268 if err := gfitClient.SetCalories(ctx, summary.Summary.CaloriesOut, tm); err != nil {
269 errs = append(errs, fmt.Errorf("gfitClient.SetCalories(%d) = %v", summary.Summary.CaloriesOut, err))
276 var distanceMeters float64
277 for _, d := range summary.Summary.Distances {
278 if d.Activity != "total" {
281 distanceMeters = 1000.0 * d.Distance
284 if err := gfitClient.SetDistance(ctx, distanceMeters, tm); err != nil {
285 errs = append(errs, fmt.Errorf("gfitClient.SetDistance(%d) = %v", distanceMeters, err))
292 if err := gfitClient.SetHeartRate(ctx, summary.Summary.HeartRateZones, summary.Summary.RestingHeartRate, tm); err != nil {
293 errs = append(errs, fmt.Errorf("gfitClient.SetHeartRate() = %v", err))