ff4c26dd1e5ee90eef87e391606a1b7e7d544443
[kraftakt.git] / gfit / gfit.go
1 package gfit
2
3 import (
4         "context"
5         "fmt"
6         "net/http"
7         "strings"
8         "time"
9
10         "github.com/octo/gfitsync/app"
11         "golang.org/x/oauth2"
12         oauth2google "golang.org/x/oauth2/google"
13         fitness "google.golang.org/api/fitness/v1"
14         "google.golang.org/api/googleapi"
15         "google.golang.org/appengine"
16         "google.golang.org/appengine/log"
17 )
18
19 const (
20         csrfToken = "@CSRFTOKEN@"
21         userID    = "me"
22 )
23
24 var oauthConfig = &oauth2.Config{
25         ClientID:     "@GOOGLE_CLIENT_ID@",
26         ClientSecret: "@GOOGLE_CLIENT_SECRET@",
27         Endpoint:     oauth2google.Endpoint,
28         RedirectURL:  "https://fitbit-gfit-sync.appspot.com/google/grant",
29         Scopes: []string{
30                 fitness.FitnessActivityWriteScope,
31                 fitness.FitnessBodyWriteScope,
32         },
33 }
34
35 func Application(ctx context.Context) *fitness.Application {
36         return &fitness.Application{
37                 Name:       "Fitbit to Google Fit sync",
38                 Version:    appengine.VersionID(ctx),
39                 DetailsUrl: "", // optional
40         }
41 }
42
43 func AuthURL() string {
44         return oauthConfig.AuthCodeURL(csrfToken, oauth2.AccessTypeOffline)
45 }
46
47 func ParseToken(ctx context.Context, r *http.Request, u *app.User) error {
48         if state := r.FormValue("state"); state != csrfToken {
49                 return fmt.Errorf("invalid state parameter: %q", state)
50         }
51
52         tok, err := oauthConfig.Exchange(ctx, r.FormValue("code"))
53         if err != nil {
54                 return err
55         }
56
57         return u.SetToken(ctx, "Google", tok)
58 }
59
60 type Client struct {
61         *fitness.Service
62 }
63
64 func NewClient(ctx context.Context, u *app.User) (*Client, error) {
65         c, err := u.OAuthClient(ctx, "Google", oauthConfig)
66         if err != nil {
67                 return nil, err
68         }
69
70         service, err := fitness.New(c)
71         if err != nil {
72                 return nil, err
73         }
74
75         return &Client{
76                 Service: service,
77         }, nil
78 }
79
80 func DataStreamID(dataSource *fitness.DataSource) string {
81         fields := []string{
82                 dataSource.Type,
83                 dataSource.DataType.Name,
84                 "@PROJECT_NUMBER@", // FIXME
85         }
86
87         if dev := dataSource.Device; dev != nil {
88                 if dev.Manufacturer != "" {
89                         fields = append(fields, dev.Manufacturer)
90                 }
91                 if dev.Model != "" {
92                         fields = append(fields, dev.Model)
93                 }
94                 if dev.Uid != "" {
95                         fields = append(fields, dev.Uid)
96                 }
97         }
98
99         return strings.Join(fields, ":")
100 }
101
102 func (c *Client) DataSourceCreate(ctx context.Context, dataSource *fitness.DataSource) (string, error) {
103         res, err := c.Service.Users.DataSources.Create(userID, dataSource).Context(ctx).Do()
104         if err != nil {
105                 if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == http.StatusConflict {
106                         if dataSource.DataStreamId != "" {
107                                 return dataSource.DataStreamId, nil
108                         }
109                         return DataStreamID(dataSource), nil
110                 }
111                 log.Errorf(ctx, "c.Service.Users.DataSources.Create() = (%+v, %v)", res, err)
112                 return "", err
113         }
114         return res.DataStreamId, nil
115 }
116
117 func (c *Client) DataSetPatch(ctx context.Context, dataSourceID string, points []*fitness.DataPoint) error {
118         startTimeNanos, endTimeNanos := int64(-1), int64(-1)
119         for _, p := range points {
120                 if startTimeNanos == -1 || startTimeNanos > p.StartTimeNanos {
121                         startTimeNanos = p.StartTimeNanos
122                 }
123                 if endTimeNanos == -1 || endTimeNanos < p.EndTimeNanos {
124                         endTimeNanos = p.EndTimeNanos
125                 }
126         }
127         datasetID := fmt.Sprintf("%d-%d", startTimeNanos, endTimeNanos)
128
129         dataset := &fitness.Dataset{
130                 DataSourceId:   dataSourceID,
131                 MinStartTimeNs: startTimeNanos,
132                 MaxEndTimeNs:   endTimeNanos,
133                 Point:          points,
134         }
135
136         _, err := c.Service.Users.DataSources.Datasets.Patch(userID, dataSourceID, datasetID, dataset).Context(ctx).Do()
137         if err != nil {
138                 log.Errorf(ctx, "c.Service.Users.DataSources.Datasets.Patch() = %v", err)
139                 return err
140         }
141         return nil
142 }
143
144 func (c *Client) SetSteps(ctx context.Context, steps int, date time.Time) error {
145         const dataTypeName = "com.google.step_count.delta"
146
147         dataSourceID, err := c.DataSourceCreate(ctx, &fitness.DataSource{
148                 Application:    Application(ctx),
149                 DataStreamId:   "", // COMPUTED
150                 DataStreamName: "", // "daily summary"?
151                 DataType: &fitness.DataType{
152                         Field: []*fitness.DataTypeField{
153                                 &fitness.DataTypeField{
154                                         Format: "integer",
155                                         Name:   "steps",
156                                 },
157                         },
158                         Name: dataTypeName,
159                 },
160                 Name: "Step Count",
161                 Type: "raw",
162         })
163         if err != nil {
164                 return err
165         }
166
167         return c.DataSetPatch(ctx, dataSourceID, []*fitness.DataPoint{
168                 &fitness.DataPoint{
169                         ComputationTimeMillis: time.Now().UnixNano() / 1000000,
170                         DataTypeName:          dataTypeName,
171                         StartTimeNanos:        date.UnixNano(),
172                         EndTimeNanos:          date.Add(24 * time.Hour).Add(-1 * time.Nanosecond).UnixNano(),
173                         Value: []*fitness.Value{
174                                 &fitness.Value{
175                                         IntVal: int64(steps),
176                                 },
177                         },
178                 },
179         })
180 }