Disable signature verification due to a problem on Fitbit's side.
[kraftakt.git] / kraftakt.go
1 package kraftakt
2
3 import (
4         "context"
5         "encoding/json"
6         "fmt"
7         "html/template"
8         "io/ioutil"
9         "net/http"
10         "sync"
11         "time"
12
13         "github.com/octo/kraftakt/app"
14         "github.com/octo/kraftakt/fitbit"
15         "github.com/octo/kraftakt/gfit"
16         "google.golang.org/appengine"
17         "google.golang.org/appengine/datastore"
18         "google.golang.org/appengine/delay"
19         "google.golang.org/appengine/log"
20         "google.golang.org/appengine/user"
21 )
22
23 var delayedHandleNotifications = delay.Func("handleNotifications", handleNotifications)
24
25 var templates *template.Template
26
27 func init() {
28         http.Handle("/login", AuthenticatedHandler(loginHandler))
29         http.Handle("/fitbit/connect", AuthenticatedHandler(fitbitConnectHandler))
30         http.Handle("/fitbit/grant", AuthenticatedHandler(fitbitGrantHandler))
31         http.Handle("/fitbit/disconnect", AuthenticatedHandler(fitbitDisconnectHandler))
32         http.Handle("/google/connect", AuthenticatedHandler(googleConnectHandler))
33         http.Handle("/google/grant", AuthenticatedHandler(googleGrantHandler))
34         http.Handle("/google/disconnect", AuthenticatedHandler(googleDisconnectHandler))
35         // unauthenticated
36         http.Handle("/fitbit/notify", ContextHandler(fitbitNotifyHandler))
37         http.Handle("/", ContextHandler(indexHandler))
38
39         t, err := template.ParseGlob("templates/*.html")
40         if err != nil {
41                 panic(err)
42         }
43         templates = t
44 }
45
46 func internalServerError(ctx context.Context, w http.ResponseWriter, err error) {
47         log.Errorf(ctx, "%v", err)
48
49         http.Error(w, "Internal Server Error\n\nReference: "+appengine.RequestID(ctx), http.StatusInternalServerError)
50 }
51
52 // ContextHandler implements http.Handler
53 type ContextHandler func(context.Context, http.ResponseWriter, *http.Request) error
54
55 func (hndl ContextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
56         ctx := appengine.NewContext(r)
57
58         if err := app.LoadConfig(ctx); err != nil {
59                 internalServerError(ctx, w, fmt.Errorf("LoadConfig() = %v", err))
60                 return
61         }
62
63         if err := hndl(ctx, w, r); err != nil {
64                 internalServerError(ctx, w, err)
65                 return
66         }
67 }
68
69 type AuthenticatedHandler func(context.Context, http.ResponseWriter, *http.Request, *app.User) error
70
71 func (hndl AuthenticatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
72         ctx := appengine.NewContext(r)
73
74         if err := app.LoadConfig(ctx); err != nil {
75                 internalServerError(ctx, w, fmt.Errorf("LoadConfig() = %v", err))
76                 return
77         }
78
79         gaeUser := user.Current(ctx)
80         if gaeUser == nil {
81                 url, err := user.LoginURL(ctx, r.URL.String())
82                 if err != nil {
83                         internalServerError(ctx, w, fmt.Errorf("LoginURL() = %v", err))
84                         return
85                 }
86                 http.Redirect(w, r, url, http.StatusTemporaryRedirect)
87                 return
88         }
89
90         u, err := app.NewUser(ctx, gaeUser.Email)
91         if err != nil {
92                 internalServerError(ctx, w, fmt.Errorf("NewUser(%q) = %v", gaeUser.Email, err))
93                 return
94         }
95
96         if err := hndl(ctx, w, r, u); err != nil {
97                 internalServerError(ctx, w, err)
98                 return
99         }
100 }
101
102 func indexHandler(ctx context.Context, w http.ResponseWriter, _ *http.Request) error {
103         var templateData struct {
104                 HaveFitbit    bool
105                 HaveGoogleFit bool
106                 *app.User
107         }
108         templateName := "main.html"
109
110         if gaeUser := user.Current(ctx); gaeUser != nil {
111                 templateName = "loggedin.html"
112
113                 u, err := app.NewUser(ctx, gaeUser.Email)
114                 if err != nil {
115                         return err
116                 }
117                 templateData.User = u
118
119                 _, err = u.Token(ctx, "Fitbit")
120                 if err != nil && err != datastore.ErrNoSuchEntity {
121                         return err
122                 }
123                 templateData.HaveFitbit = (err == nil)
124
125                 _, err = u.Token(ctx, "Google")
126                 if err != nil && err != datastore.ErrNoSuchEntity {
127                         return err
128                 }
129                 templateData.HaveGoogleFit = (err == nil)
130         }
131
132         return templates.ExecuteTemplate(w, templateName, &templateData)
133 }
134
135 func loginHandler(_ context.Context, w http.ResponseWriter, r *http.Request, _ *app.User) error {
136         // essentially a nop; all the heavy lifting (i.e. logging in) has been done by the AuthenticatedHandler wrapper.
137         redirectURL := r.URL
138         redirectURL.Path = "/"
139         redirectURL.RawQuery = ""
140         redirectURL.Fragment = ""
141         http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
142         return nil
143 }
144
145 func fitbitConnectHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
146         http.Redirect(w, r, fitbit.AuthURL(ctx, u), http.StatusTemporaryRedirect)
147         return nil
148 }
149
150 func fitbitGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
151         if err := fitbit.ParseToken(ctx, r, u); err != nil {
152                 return err
153         }
154         c, err := fitbit.NewClient(ctx, "", u)
155         if err != nil {
156                 return err
157         }
158
159         for _, collection := range []string{"activities", "sleep"} {
160                 if err := c.Subscribe(ctx, collection); err != nil {
161                         return fmt.Errorf("c.Subscribe(%q) = %v", collection, err)
162                 }
163                 log.Infof(ctx, "Successfully subscribed to %q", collection)
164         }
165
166         redirectURL := r.URL
167         redirectURL.Path = "/"
168         redirectURL.RawQuery = ""
169         redirectURL.Fragment = ""
170         http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
171         return nil
172 }
173
174 func fitbitDisconnectHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
175         c, err := fitbit.NewClient(ctx, "", u)
176         if err != nil {
177                 return err
178         }
179
180         if err := c.UnsubscribeAll(ctx); err != nil {
181                 return fmt.Errorf("UnsubscribeAll() = %v", err)
182         }
183
184         if err := c.DeleteToken(ctx); err != nil {
185                 return err
186         }
187
188         redirectURL := r.URL
189         redirectURL.Path = "/"
190         redirectURL.RawQuery = ""
191         redirectURL.Fragment = ""
192         http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
193         return nil
194 }
195
196 func googleConnectHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
197         http.Redirect(w, r, gfit.AuthURL(ctx, u), http.StatusTemporaryRedirect)
198         return nil
199 }
200
201 func googleGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
202         if err := gfit.ParseToken(ctx, r, u); err != nil {
203                 return err
204         }
205
206         redirectURL := r.URL
207         redirectURL.Path = "/"
208         redirectURL.RawQuery = ""
209         redirectURL.Fragment = ""
210         http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
211         return nil
212 }
213
214 func googleDisconnectHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *app.User) error {
215         c, err := gfit.NewClient(ctx, u)
216         if err != nil {
217                 return err
218         }
219
220         if err := c.DeleteToken(ctx); err != nil {
221                 return err
222         }
223
224         redirectURL := r.URL
225         redirectURL.Path = "/"
226         redirectURL.RawQuery = ""
227         redirectURL.Fragment = ""
228         http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
229         return nil
230 }
231
232 // fitbitNotifyHandler is called by Fitbit whenever there are updates to a
233 // subscription. It verifies the payload, splits it into individual
234 // notifications and adds it to the taskqueue service.
235 func fitbitNotifyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
236         defer r.Body.Close()
237
238         fitbitTimeout := 3 * time.Second
239         ctx, cancel := context.WithTimeout(ctx, fitbitTimeout)
240         defer cancel()
241
242         // this is used when setting up a new subscriber in the UI. Once set
243         // up, this code path should not be triggered.
244         if verify := r.FormValue("verify"); verify != "" {
245                 if verify == app.Config.FitbitSubscriberCode {
246                         w.WriteHeader(http.StatusNoContent)
247                 } else {
248                         w.WriteHeader(http.StatusNotFound)
249                 }
250                 return nil
251         }
252
253         data, err := ioutil.ReadAll(r.Body)
254         if err != nil {
255                 return err
256         }
257
258         // Fitbit recommendation: "If signature verification fails, you should
259         // respond with a 404"
260         if !fitbit.CheckSignature(ctx, data, r.Header.Get("X-Fitbit-Signature")) {
261                 /*
262                         log.Errorf(ctx, "signature mismatch")
263                         w.WriteHeader(http.StatusNotFound)
264                         return nil
265                 */
266         } else {
267                 log.Warningf(ctx, "TODO(octo): re-enable signature checking, see https://community.fitbit.com/t5/Web-API-Development/Push-notification-signatures-are-currently-invalid/m-p/2496159")
268         }
269
270         if err := delayedHandleNotifications.Call(ctx, data); err != nil {
271                 return err
272         }
273
274         w.WriteHeader(http.StatusCreated)
275         return nil
276 }
277
278 // handleNotifications parses fitbit notifications and requests the individual
279 // activities from Fitbit. It is executed asynchronously via the delay package.
280 func handleNotifications(ctx context.Context, payload []byte) error {
281         log.Debugf(ctx, "NOTIFY -> %s", payload)
282
283         if err := app.LoadConfig(ctx); err != nil {
284                 return err
285         }
286
287         var subscriptions []fitbit.Subscription
288         if err := json.Unmarshal(payload, &subscriptions); err != nil {
289                 return err
290         }
291
292         wg := &sync.WaitGroup{}
293
294         for _, s := range subscriptions {
295                 switch s.CollectionType {
296                 case "activities":
297                         wg.Add(1)
298                         go func() {
299                                 defer wg.Done()
300                                 if err := activitiesNotification(ctx, &s); err != nil {
301                                         log.Warningf(ctx, "activitiesNotification() = %v", err)
302                                 }
303                         }()
304                 case "sleep":
305                         wg.Add(1)
306                         go func() {
307                                 defer wg.Done()
308                                 if err := sleepNotification(ctx, &s); err != nil {
309                                         log.Warningf(ctx, "sleepNotification() = %v", err)
310                                 }
311                         }()
312                 default:
313                         log.Warningf(ctx, "ignoring collection type %q", s.CollectionType)
314
315                 }
316         }
317
318         wg.Wait()
319         return nil
320 }
321
322 func activitiesNotification(ctx context.Context, s *fitbit.Subscription) error {
323         u, err := fitbit.UserFromSubscriberID(ctx, s.SubscriptionID)
324         if err != nil {
325                 return err
326         }
327
328         fitbitClient, err := fitbit.NewClient(ctx, s.OwnerID, u)
329         if err != nil {
330                 return err
331         }
332
333         var (
334                 wg      = &sync.WaitGroup{}
335                 errs    appengine.MultiError
336                 summary *fitbit.ActivitySummary
337                 profile *fitbit.Profile
338         )
339
340         wg.Add(1)
341         go func() {
342                 var err error
343                 summary, err = fitbitClient.ActivitySummary(ctx, s.Date)
344                 if err != nil {
345                         errs = append(errs, fmt.Errorf("fitbitClient.ActivitySummary(%q) = %v", s.Date, err))
346                 }
347                 wg.Done()
348         }()
349
350         wg.Add(1)
351         go func() {
352                 var err error
353                 profile, err = fitbitClient.Profile(ctx)
354                 if err != nil {
355                         errs = append(errs, fmt.Errorf("fitbitClient.Profile(%q) = %v", s.Date, err))
356                 }
357                 wg.Done()
358         }()
359
360         wg.Wait()
361         if len(errs) != 0 {
362                 return errs
363         }
364
365         tm, err := time.ParseInLocation("2006-01-02", s.Date, profile.Timezone)
366         if err != nil {
367                 return err
368         }
369
370         log.Debugf(ctx, "%s (%s) took %d steps on %s",
371                 profile.Name, u.Email, summary.Summary.Steps, tm)
372
373         gfitClient, err := gfit.NewClient(ctx, u)
374         if err != nil {
375                 return err
376         }
377
378         wg.Add(1)
379         go func() {
380                 if err := gfitClient.SetSteps(ctx, summary.Summary.Steps, tm); err != nil {
381                         errs = append(errs, fmt.Errorf("gfitClient.SetSteps(%d) = %v", summary.Summary.Steps, err))
382                 }
383                 wg.Done()
384         }()
385
386         wg.Add(1)
387         go func() {
388                 if err := gfitClient.SetCalories(ctx, summary.Summary.CaloriesOut, tm); err != nil {
389                         errs = append(errs, fmt.Errorf("gfitClient.SetCalories(%d) = %v", summary.Summary.CaloriesOut, err))
390                 }
391                 wg.Done()
392         }()
393
394         wg.Add(1)
395         go func() {
396                 defer wg.Done()
397
398                 var distanceMeters float64
399                 for _, d := range summary.Summary.Distances {
400                         if d.Activity != "total" {
401                                 continue
402                         }
403                         distanceMeters = 1000.0 * d.Distance
404                         break
405                 }
406                 if err := gfitClient.SetDistance(ctx, distanceMeters, tm); err != nil {
407                         errs = append(errs, fmt.Errorf("gfitClient.SetDistance(%g) = %v", distanceMeters, err))
408                         return
409                 }
410         }()
411
412         wg.Add(1)
413         go func() {
414                 if err := gfitClient.SetHeartRate(ctx, summary.Summary.HeartRateZones, summary.Summary.RestingHeartRate, tm); err != nil {
415                         errs = append(errs, fmt.Errorf("gfitClient.SetHeartRate() = %v", err))
416                 }
417                 wg.Done()
418         }()
419
420         wg.Add(1)
421         go func() {
422                 defer wg.Done()
423
424                 var activities []gfit.Activity
425                 for _, a := range summary.Activities {
426                         if !a.HasStartTime {
427                                 continue
428                         }
429
430                         startTime, err := time.ParseInLocation("2006-01-02T15:04", a.StartDate+"T"+a.StartTime, profile.Timezone)
431                         if err != nil {
432                                 errs = append(errs, fmt.Errorf("gfitClient.SetActivities() = %v", err))
433                                 return
434                         }
435                         endTime := startTime.Add(time.Duration(a.Duration) * time.Millisecond)
436
437                         activities = append(activities, gfit.Activity{
438                                 Start: startTime,
439                                 End:   endTime,
440                                 Type:  gfit.ParseFitbitActivity(a.Name),
441                         })
442                 }
443                 if err := gfitClient.SetActivities(ctx, activities, tm); err != nil {
444                         errs = append(errs, fmt.Errorf("gfitClient.SetActivities() = %v", err))
445                         return
446                 }
447         }()
448
449         wg.Wait()
450
451         if len(errs) != 0 {
452                 return errs
453         }
454         return nil
455 }
456
457 func sleepNotification(ctx context.Context, s *fitbit.Subscription) error {
458         u, err := fitbit.UserFromSubscriberID(ctx, s.SubscriptionID)
459         if err != nil {
460                 return err
461         }
462
463         var (
464                 wg         = &sync.WaitGroup{}
465                 gfitClient *gfit.Client
466                 gfitErr    error
467         )
468
469         wg.Add(1)
470         go func() {
471                 gfitClient, gfitErr = gfit.NewClient(ctx, u)
472                 wg.Done()
473         }()
474
475         fitbitClient, err := fitbit.NewClient(ctx, s.OwnerID, u)
476         if err != nil {
477                 return err
478         }
479
480         profile, err := fitbitClient.Profile(ctx)
481         if err != nil {
482                 return err
483         }
484
485         tm, err := time.ParseInLocation("2006-01-02", s.Date, profile.Timezone)
486         if err != nil {
487                 return err
488         }
489
490         sleep, err := fitbitClient.Sleep(ctx, tm)
491         if err != nil {
492                 return err
493         }
494
495         var activities []gfit.Activity
496         for _, stg := range sleep.Stages {
497                 a := gfit.Activity{
498                         Start: stg.StartTime,
499                         End:   stg.EndTime,
500                 }
501                 switch stg.Level {
502                 case fitbit.SleepLevelDeep:
503                         a.Type = 110 // Deep sleep
504                 case fitbit.SleepLevelLight:
505                         a.Type = 109 // Light sleep
506                 case fitbit.SleepLevelREM:
507                         a.Type = 111 // REM sleep
508                 case fitbit.SleepLevelWake:
509                         a.Type = 112 // Awake (during sleep cycle)
510                 default:
511                         log.Warningf(ctx, "unexpected sleep level %v", stg.Level)
512                         continue
513                 }
514         }
515
516         wg.Wait()
517         if gfitErr != nil {
518                 return gfitErr
519         }
520
521         if err := gfitClient.SetActivities(ctx, activities, tm); err != nil {
522                 return fmt.Errorf("SetActivities() = %v", err)
523         }
524
525         return nil
526 }