15 legacy_context "golang.org/x/net/context"
17 "golang.org/x/oauth2/fitbit"
18 "google.golang.org/appengine"
19 "google.golang.org/appengine/datastore"
20 "google.golang.org/appengine/delay"
21 "google.golang.org/appengine/log"
22 "google.golang.org/appengine/user"
25 const csrfToken = "@CSRFTOKEN@"
27 // var delayedHandleNotifications = delay.Func("handleNotifications", func(ctx legacy_context.Context, payload []byte) error {
28 // return handleNotifications(ctx, payload)
30 var delayedHandleNotifications = delay.Func("handleNotifications", handleNotifications)
32 var oauthConfig = &oauth2.Config{
33 ClientID: "@FITBIT_CLIENT_ID@",
34 ClientSecret: "@FITBIT_CLIENT_SECRET@",
35 Endpoint: fitbit.Endpoint,
36 RedirectURL: "https://fitbit-gfit-sync.appspot.com/fitbit/grant", // TODO(octo): make full URL
37 Scopes: []string{"activity"},
40 type storedToken struct {
46 http.HandleFunc("/setup", setupHandler)
47 http.Handle("/fitbit/grant", AuthenticatedHandler(fitbitGrantHandler))
48 http.Handle("/fitbit/notify", ContextHandler(fitbitNotifyHandler))
49 http.Handle("/", AuthenticatedHandler(indexHandler))
52 // ContextHandler implements http.Handler
53 type ContextHandler func(context.Context, http.ResponseWriter, *http.Request) error
55 func (hndl ContextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
56 ctx := appengine.NewContext(r)
58 if err := hndl(ctx, w, r); err != nil {
59 http.Error(w, err.Error(), http.StatusInternalServerError)
64 type AuthenticatedHandler func(context.Context, http.ResponseWriter, *http.Request, *user.User) error
66 func (hndl AuthenticatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
67 ctx := appengine.NewContext(r)
69 u := user.Current(ctx)
71 url, err := user.LoginURL(ctx, r.URL.String())
73 http.Error(w, err.Error(), http.StatusInternalServerError)
76 http.Redirect(w, r, url, http.StatusTemporaryRedirect)
80 err := datastore.RunInTransaction(ctx, func(ctx legacy_context.Context) error {
81 key := RootKey(ctx, u)
82 if err := datastore.Get(ctx, key, &struct{}{}); err != datastore.ErrNoSuchEntity {
83 return err // may be nil
85 _, err := datastore.Put(ctx, key, &struct{}{})
89 http.Error(w, err.Error(), http.StatusInternalServerError)
93 if err := hndl(ctx, w, r, u); err != nil {
94 http.Error(w, err.Error(), http.StatusInternalServerError)
99 func RootKey(ctx context.Context, u *user.User) *datastore.Key {
100 return datastore.NewKey(ctx, "User", u.Email, 0, nil)
103 func indexHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *user.User) error {
109 key := datastore.NewKey(ctx, "Token", "Fitbit", 0, RootKey(ctx, u))
110 err := datastore.Get(ctx, key, &tok)
111 if err != nil && err != datastore.ErrNoSuchEntity {
118 // fmt.Fprintf(w, "u = %v, tok = %v, haveToken = %v\n", u, tok, haveToken)
119 fmt.Fprintln(w, "<html><body><h1>Fitbit to Google Fit sync</h1>")
121 fmt.Fprintf(w, "<p>Hello %s</p>\n", u.Email)
122 fmt.Fprint(w, "<p>Fitbit: ")
124 fmt.Fprint(w, `<strong style="color: DarkGreen;">Authorized</strong>`)
126 fmt.Fprint(w, `<strong style="color: DarkRed;">Not authorized</strong> (<a href="/setup">Authorize</a>)`)
128 fmt.Fprintln(w, "</p>")
129 fmt.Fprintln(w, "</body></html>")
131 // TODO(octo): print summary to user
135 func setupHandler(w http.ResponseWriter, r *http.Request) {
136 url := oauthConfig.AuthCodeURL(csrfToken, oauth2.AccessTypeOffline)
137 http.Redirect(w, r, url, http.StatusTemporaryRedirect)
140 func fitbitGrantHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, u *user.User) error {
141 if state := r.FormValue("state"); state != csrfToken {
142 return fmt.Errorf("invalid state parameter: %q", state)
145 tok, err := oauthConfig.Exchange(ctx, r.FormValue("code"))
150 key := datastore.NewKey(ctx, "Token", "Fitbit", 0, RootKey(ctx, u))
151 if _, err := datastore.Put(ctx, key, tok); err != nil {
155 c := oauthConfig.Client(ctx, tok)
157 // create a subscription
158 url := fmt.Sprintf("https://api.fitbit.com/1/user/-/activities/apiSubscriptions/%s.json",
159 RootKey(ctx, u).Encode())
160 res, err := c.Post(url, "", nil)
164 defer res.Body.Close()
166 if res.StatusCode >= 400 {
167 data, _ := ioutil.ReadAll(r.Body)
168 log.Errorf(ctx, "creating subscription failed: status %d %q", res.StatusCode, data)
169 return fmt.Errorf("creating subscription failed")
173 redirectURL.Path = "/"
174 redirectURL.RawQuery = ""
175 redirectURL.Fragment = ""
176 http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
180 type fitbitNotification struct {
181 CollectionType string `json:"collectionType"`
182 Date string `json:"date"`
183 OwnerID string `json:"ownerId"`
184 OwnerType string `json:"ownerType"`
185 SubscriptionID string `json:"subscriptionId"`
188 func (n *fitbitNotification) URLValues() url.Values {
190 "CollectionType": []string{n.CollectionType},
191 "Date": []string{n.Date},
192 "OwnerID": []string{n.OwnerID},
193 "OwnerType": []string{n.OwnerType},
194 "SubscriptionID": []string{n.SubscriptionID},
198 func (n *fitbitNotification) URL() string {
199 return fmt.Sprintf("https://api.fitbit.com/1/user/%s/%s/date/%s.json",
200 n.OwnerID, n.CollectionType, n.Date)
203 func checkSignature(ctx context.Context, payload []byte, rawSig string) bool {
204 base64Sig, err := url.QueryUnescape(rawSig)
206 log.Errorf(ctx, "QueryUnescape(%q) = %v", rawSig, err)
209 signatureGot, err := base64.StdEncoding.DecodeString(base64Sig)
211 log.Errorf(ctx, "base64.StdEncoding.DecodeString(%q) = %v", base64Sig, err)
215 mac := hmac.New(sha1.New, []byte(oauthConfig.ClientSecret+"&"))
217 signatureWant := mac.Sum(nil)
219 return hmac.Equal(signatureGot, signatureWant)
222 // fitbitNotifyHandler is called by Fitbit whenever there are updates to a
223 // subscription. It verifies the payload, splits it into individual
224 // notifications and adds it to the taskqueue service.
225 func fitbitNotifyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
228 fitbitTimeout := 3 * time.Second
229 ctx, cancel := context.WithTimeout(ctx, fitbitTimeout)
232 // this is used when setting up a new subscriber in the UI. Once set
233 // up, this code path should not be triggered.
234 if verify := r.FormValue("verify"); verify != "" {
235 if verify == "@FITBIT_SUBSCRIBER_CODE@" {
236 w.WriteHeader(http.StatusNoContent)
238 w.WriteHeader(http.StatusNotFound)
243 data, err := ioutil.ReadAll(r.Body)
248 // Fitbit recommendation: "If signature verification fails, you should
249 // respond with a 404"
250 if !checkSignature(ctx, data, r.Header.Get("X-Fitbit-Signature")) {
251 w.WriteHeader(http.StatusNotFound)
255 if err := delayedHandleNotifications.Call(ctx, data); err != nil {
259 w.WriteHeader(http.StatusCreated)
263 // handleNotifications parses fitbit notifications and requests the individual
264 // activities from Fitbit. It is executed asynchronously via the delay package.
265 func handleNotifications(ctx context.Context, payload []byte) error {
266 var notifications []fitbitNotification
267 if err := json.Unmarshal(payload, notifications); err != nil {
271 for _, n := range notifications {
272 if n.CollectionType != "activities" {
276 if err := handleNotification(ctx, &n); err != nil {
277 log.Errorf(ctx, "handleNotification() = %v", err)
285 func handleNotification(ctx context.Context, n *fitbitNotification) error {
286 rootKey, err := datastore.DecodeKey(n.SubscriptionID)
290 key := datastore.NewKey(ctx, "Token", "Fitbit", 0, rootKey)
293 if err := datastore.Get(ctx, key, &tok); err != nil {
297 c := oauthConfig.Client(ctx, &tok)
298 res, err := c.Get(n.URL())
303 log.Infof(ctx, "GET %s = %v", n.URL(), res)