15 "github.com/google/uuid"
16 "github.com/octo/gfitsync/fitbit"
17 legacy_context "golang.org/x/net/context"
19 oauth2fitbit "golang.org/x/oauth2/fitbit"
20 "google.golang.org/appengine"
21 "google.golang.org/appengine/datastore"
22 "google.golang.org/appengine/delay"
23 "google.golang.org/appengine/log"
24 "google.golang.org/appengine/user"
27 const csrfToken = "@CSRFTOKEN@"
29 // var delayedHandleNotifications = delay.Func("handleNotifications", func(ctx legacy_context.Context, payload []byte) error {
30 // return handleNotifications(ctx, payload)
32 var delayedHandleNotifications = delay.Func("handleNotifications", handleNotifications)
34 var oauthConfig = &oauth2.Config{
35 ClientID: "@FITBIT_CLIENT_ID@",
36 ClientSecret: "@FITBIT_CLIENT_SECRET@",
37 Endpoint: oauth2fitbit.Endpoint,
38 RedirectURL: "https://fitbit-gfit-sync.appspot.com/fitbit/grant", // TODO(octo): make full URL
39 Scopes: []string{"activity"},
43 http.HandleFunc("/setup", setupHandler)
44 http.Handle("/fitbit/grant", AuthenticatedHandler(fitbitGrantHandler))
45 http.Handle("/fitbit/notify", ContextHandler(fitbitNotifyHandler))
46 http.Handle("/", AuthenticatedHandler(indexHandler))
49 // ContextHandler implements http.Handler
50 type ContextHandler func(context.Context, http.ResponseWriter, *http.Request) error
52 func (hndl ContextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
53 ctx := appengine.NewContext(r)
55 if err := hndl(ctx, w, r); err != nil {
56 http.Error(w, err.Error(), http.StatusInternalServerError)
61 type AuthenticatedHandler func(context.Context, http.ResponseWriter, *http.Request, *datastore.Key) error
67 func (hndl AuthenticatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
68 ctx := appengine.NewContext(r)
70 u := user.Current(ctx)
72 url, err := user.LoginURL(ctx, r.URL.String())
74 http.Error(w, err.Error(), http.StatusInternalServerError)
77 http.Redirect(w, r, url, http.StatusTemporaryRedirect)
81 err := datastore.RunInTransaction(ctx, func(ctx legacy_context.Context) error {
82 key := datastore.NewKey(ctx, "User", u.Email, 0, nil)
84 if err := datastore.Get(ctx, key, &User{}); err != datastore.ErrNoSuchEntity {
85 return err // may be nil
88 _, err := datastore.Put(ctx, key, &User{
89 ID: uuid.New().String(),
94 http.Error(w, err.Error(), http.StatusInternalServerError)
98 rootKey := datastore.NewKey(ctx, "User", u.Email, 0, nil)
99 if err := hndl(ctx, w, r, rootKey); err != nil {
100 http.Error(w, err.Error(), http.StatusInternalServerError)
105 func indexHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, rootKey *datastore.Key) error {
111 key := datastore.NewKey(ctx, "Token", "Fitbit", 0, rootKey)
112 err := datastore.Get(ctx, key, &tok)
113 if err != nil && err != datastore.ErrNoSuchEntity {
120 u := user.Current(ctx)
122 // fmt.Fprintf(w, "u = %v, tok = %v, haveToken = %v\n", u, tok, haveToken)
123 fmt.Fprintln(w, "<html><body><h1>Fitbit to Google Fit sync</h1>")
125 fmt.Fprintf(w, "<p>Hello %s</p>\n", u.Email)
126 fmt.Fprint(w, "<p>Fitbit: ")
128 fmt.Fprint(w, `<strong style="color: DarkGreen;">Authorized</strong>`)
130 fmt.Fprint(w, `<strong style="color: DarkRed;">Not authorized</strong> (<a href="/setup">Authorize</a>)`)
132 fmt.Fprintln(w, "</p>")
133 fmt.Fprintln(w, "</body></html>")
135 // TODO(octo): print summary to user
139 func setupHandler(w http.ResponseWriter, r *http.Request) {
140 url := oauthConfig.AuthCodeURL(csrfToken, oauth2.AccessTypeOffline)
141 http.Redirect(w, r, url, http.StatusTemporaryRedirect)
144 func fitbitGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, rootKey *datastore.Key) error {
145 if state := r.FormValue("state"); state != csrfToken {
146 return fmt.Errorf("invalid state parameter: %q", state)
149 tok, err := oauthConfig.Exchange(ctx, r.FormValue("code"))
154 key := datastore.NewKey(ctx, "Token", "Fitbit", 0, rootKey)
155 if _, err := datastore.Put(ctx, key, tok); err != nil {
158 c := oauthConfig.Client(ctx, tok)
161 if err := datastore.Get(ctx, rootKey, &u); err != nil {
165 // create a subscription
166 url := fmt.Sprintf("https://api.fitbit.com/1/user/-/activities/apiSubscriptions/%s.json", u.ID)
167 res, err := c.Post(url, "", nil)
171 defer res.Body.Close()
173 if res.StatusCode == http.StatusConflict {
174 var n fitbitSubscription
175 if err := json.NewDecoder(res.Body).Decode(&n); err != nil {
178 log.Warningf(ctx, "conflict with existing subscription %v", n)
181 if res.StatusCode >= 400 {
182 data, _ := ioutil.ReadAll(res.Body)
183 log.Errorf(ctx, "creating subscription failed: status %d %q", res.StatusCode, data)
184 return fmt.Errorf("creating subscription failed")
188 redirectURL.Path = "/"
189 redirectURL.RawQuery = ""
190 redirectURL.Fragment = ""
191 http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
195 type fitbitSubscription struct {
196 CollectionType string `json:"collectionType"`
197 Date string `json:"date"`
198 OwnerID string `json:"ownerId"`
199 OwnerType string `json:"ownerType"`
200 SubscriptionID string `json:"subscriptionId"`
203 func (s *fitbitSubscription) URLValues() url.Values {
205 "CollectionType": []string{s.CollectionType},
206 "Date": []string{s.Date},
207 "OwnerID": []string{s.OwnerID},
208 "OwnerType": []string{s.OwnerType},
209 "SubscriptionID": []string{s.SubscriptionID},
213 func (s *fitbitSubscription) URL() string {
214 // daily summary: GET https://api.fitbit.com/1/user/[user-id]/activities/date/[date].json
215 return fmt.Sprintf("https://api.fitbit.com/1/user/%s/%s/date/%s.json",
216 s.OwnerID, s.CollectionType, s.Date)
219 func checkSignature(ctx context.Context, payload []byte, rawSig string) bool {
220 base64Sig, err := url.QueryUnescape(rawSig)
222 log.Errorf(ctx, "QueryUnescape(%q) = %v", rawSig, err)
225 signatureGot, err := base64.StdEncoding.DecodeString(base64Sig)
227 log.Errorf(ctx, "base64.StdEncoding.DecodeString(%q) = %v", base64Sig, err)
231 mac := hmac.New(sha1.New, []byte(oauthConfig.ClientSecret+"&"))
233 signatureWant := mac.Sum(nil)
235 return hmac.Equal(signatureGot, signatureWant)
238 // fitbitNotifyHandler is called by Fitbit whenever there are updates to a
239 // subscription. It verifies the payload, splits it into individual
240 // notifications and adds it to the taskqueue service.
241 func fitbitNotifyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
244 fitbitTimeout := 3 * time.Second
245 ctx, cancel := context.WithTimeout(ctx, fitbitTimeout)
248 // this is used when setting up a new subscriber in the UI. Once set
249 // up, this code path should not be triggered.
250 if verify := r.FormValue("verify"); verify != "" {
251 if verify == "@FITBIT_SUBSCRIBER_CODE@" {
252 w.WriteHeader(http.StatusNoContent)
254 w.WriteHeader(http.StatusNotFound)
259 data, err := ioutil.ReadAll(r.Body)
264 // Fitbit recommendation: "If signature verification fails, you should
265 // respond with a 404"
266 if !checkSignature(ctx, data, r.Header.Get("X-Fitbit-Signature")) {
267 w.WriteHeader(http.StatusNotFound)
271 if err := delayedHandleNotifications.Call(ctx, data); err != nil {
275 w.WriteHeader(http.StatusCreated)
279 // handleNotifications parses fitbit notifications and requests the individual
280 // activities from Fitbit. It is executed asynchronously via the delay package.
281 func handleNotifications(ctx context.Context, payload []byte) error {
282 var subscriptions []fitbitSubscription
283 if err := json.Unmarshal(payload, &subscriptions); err != nil {
287 for _, s := range subscriptions {
288 if s.CollectionType != "activities" {
292 if err := handleNotification(ctx, &s); err != nil {
293 log.Errorf(ctx, "handleNotification() = %v", err)
301 func handleNotification(ctx context.Context, s *fitbitSubscription) error {
302 q := datastore.NewQuery("User").Filter("ID=", s.SubscriptionID).KeysOnly()
303 keys, err := q.GetAll(ctx, nil)
305 return fmt.Errorf("datastore.Query.GetAll(): %v", err)
308 return fmt.Errorf("len(keys) = %d, want 1", len(keys))
312 key := datastore.NewKey(ctx, "Token", "Fitbit", 0, rootKey)
315 if err := datastore.Get(ctx, key, &tok); err != nil {
319 c := fitbit.NewClient(ctx, s.OwnerID, &tok)
321 t, err := time.Parse("2006-01-02", s.Date)
326 summary, err := c.ActivitySummary(t)
331 log.Debugf(ctx, "ActivitySummary() = %v", summary)