Write step count summary to Google Fit.
[kraftakt.git] / gfit / gfit.go
1 package gfit
2
3 import (
4         "context"
5         "fmt"
6         "net/http"
7         "time"
8
9         "github.com/octo/gfitsync/app"
10         "golang.org/x/oauth2"
11         oauth2google "golang.org/x/oauth2/google"
12         fitness "google.golang.org/api/fitness/v1"
13         "google.golang.org/appengine"
14         "google.golang.org/appengine/log"
15 )
16
17 var oauthConfig = &oauth2.Config{
18         ClientID:     "@GOOGLE_CLIENT_ID@",
19         ClientSecret: "@GOOGLE_CLIENT_SECRET@",
20         Endpoint:     oauth2google.Endpoint,
21         RedirectURL:  "https://fitbit-gfit-sync.appspot.com/google/grant",
22         Scopes: []string{
23                 fitness.FitnessActivityWriteScope,
24                 fitness.FitnessBodyWriteScope,
25         },
26 }
27
28 const csrfToken = "@CSRFTOKEN@"
29
30 func Application(ctx context.Context) *fitness.Application {
31         return &fitness.Application{
32                 Name:       "Fitbit to Google Fit sync",
33                 Version:    appengine.VersionID(ctx),
34                 DetailsUrl: "", // optional
35         }
36 }
37
38 func AuthURL() string {
39         return oauthConfig.AuthCodeURL(csrfToken, oauth2.AccessTypeOffline)
40 }
41
42 func ParseToken(ctx context.Context, r *http.Request, u *app.User) error {
43         if state := r.FormValue("state"); state != csrfToken {
44                 return fmt.Errorf("invalid state parameter: %q", state)
45         }
46
47         tok, err := oauthConfig.Exchange(ctx, r.FormValue("code"))
48         if err != nil {
49                 return err
50         }
51
52         return u.SetToken(ctx, "Google", tok)
53 }
54
55 type Client struct {
56         *fitness.Service
57 }
58
59 func NewClient(ctx context.Context, u *app.User) (*Client, error) {
60         c, err := u.OAuthClient(ctx, "Google", oauthConfig)
61         if err != nil {
62                 return nil, err
63         }
64
65         service, err := fitness.New(c)
66         if err != nil {
67                 return nil, err
68         }
69
70         return &Client{
71                 Service: service,
72         }, nil
73 }
74
75 func (c *Client) SetSteps(ctx context.Context, steps int, date time.Time) error {
76         const userID = "me"
77         const dataTypeName = "com.google.step_count.delta"
78         dataSource := &fitness.DataSource{
79                 Application:    Application(ctx),
80                 DataStreamName: "", // "daily summary"?
81                 DataType: &fitness.DataType{
82                         Field: []*fitness.DataTypeField{
83                                 &fitness.DataTypeField{
84                                         Format: "integer",
85                                         Name:   "steps",
86                                 },
87                         },
88                         Name: dataTypeName,
89                 },
90                 Name: "Step Count",
91                 Type: "raw",
92         }
93
94         dataSource, err := c.Service.Users.DataSources.Create(userID, dataSource).Context(ctx).Do()
95         if err != nil {
96                 log.Errorf(ctx, "c.Service.Users.DataSources.Create() = (%+v, %v)", dataSource, err)
97                 return err
98         }
99         dataSourceID := dataSource.DataStreamId
100
101         startTimeNanos := date.UnixNano()
102         endTimeNanos := date.Add(86399999999999 * time.Nanosecond).UnixNano()
103         datasetID := fmt.Sprintf("%d-%d", startTimeNanos, endTimeNanos)
104         dataset := &fitness.Dataset{
105                 MinStartTimeNs: startTimeNanos,
106                 MaxEndTimeNs:   endTimeNanos,
107                 Point: []*fitness.DataPoint{
108                         &fitness.DataPoint{
109                                 ComputationTimeMillis: time.Now().UnixNano() / 1000000,
110                                 DataTypeName:          dataTypeName,
111                                 StartTimeNanos:        startTimeNanos,
112                                 EndTimeNanos:          endTimeNanos,
113                                 Value: []*fitness.Value{
114                                         &fitness.Value{
115                                                 IntVal: int64(steps),
116                                         },
117                                 },
118                         },
119                 },
120         }
121
122         dataset, err = c.Service.Users.DataSources.Datasets.Patch(userID, dataSourceID, datasetID, dataset).Context(ctx).Do()
123         if err != nil {
124                 log.Errorf(ctx, "c.Service.Users.DataSources.Datasets.Patch() = (%+v, %v)", dataset, err)
125                 return err
126         }
127         return nil
128 }