package gfit import ( "context" "fmt" "net/http" "strings" "time" "github.com/octo/gfitsync/app" "golang.org/x/oauth2" oauth2google "golang.org/x/oauth2/google" fitness "google.golang.org/api/fitness/v1" "google.golang.org/api/googleapi" "google.golang.org/appengine" "google.golang.org/appengine/log" ) const ( csrfToken = "@CSRFTOKEN@" userID = "me" ) var oauthConfig = &oauth2.Config{ ClientID: "@GOOGLE_CLIENT_ID@", ClientSecret: "@GOOGLE_CLIENT_SECRET@", Endpoint: oauth2google.Endpoint, RedirectURL: "https://fitbit-gfit-sync.appspot.com/google/grant", Scopes: []string{ fitness.FitnessActivityWriteScope, fitness.FitnessBodyWriteScope, }, } func Application(ctx context.Context) *fitness.Application { return &fitness.Application{ Name: "Fitbit to Google Fit sync", Version: appengine.VersionID(ctx), DetailsUrl: "", // optional } } func AuthURL() string { return oauthConfig.AuthCodeURL(csrfToken, oauth2.AccessTypeOffline) } func ParseToken(ctx context.Context, r *http.Request, u *app.User) error { if state := r.FormValue("state"); state != csrfToken { return fmt.Errorf("invalid state parameter: %q", state) } tok, err := oauthConfig.Exchange(ctx, r.FormValue("code")) if err != nil { return err } return u.SetToken(ctx, "Google", tok) } type Client struct { *fitness.Service } func NewClient(ctx context.Context, u *app.User) (*Client, error) { c, err := u.OAuthClient(ctx, "Google", oauthConfig) if err != nil { return nil, err } service, err := fitness.New(c) if err != nil { return nil, err } return &Client{ Service: service, }, nil } func DataStreamID(dataSource *fitness.DataSource) string { fields := []string{ dataSource.Type, dataSource.DataType.Name, "@PROJECT_NUMBER@", // FIXME } if dev := dataSource.Device; dev != nil { if dev.Manufacturer != "" { fields = append(fields, dev.Manufacturer) } if dev.Model != "" { fields = append(fields, dev.Model) } if dev.Uid != "" { fields = append(fields, dev.Uid) } } return strings.Join(fields, ":") } func (c *Client) DataSourceCreate(ctx context.Context, dataSource *fitness.DataSource) (string, error) { res, err := c.Service.Users.DataSources.Create(userID, dataSource).Context(ctx).Do() if err != nil { if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == http.StatusConflict { if dataSource.DataStreamId != "" { return dataSource.DataStreamId, nil } return DataStreamID(dataSource), nil } log.Errorf(ctx, "c.Service.Users.DataSources.Create() = (%+v, %v)", res, err) return "", err } return res.DataStreamId, nil } func (c *Client) DataSetPatch(ctx context.Context, dataSourceID string, points []*fitness.DataPoint) error { startTimeNanos, endTimeNanos := int64(-1), int64(-1) for _, p := range points { if startTimeNanos == -1 || startTimeNanos > p.StartTimeNanos { startTimeNanos = p.StartTimeNanos } if endTimeNanos == -1 || endTimeNanos < p.EndTimeNanos { endTimeNanos = p.EndTimeNanos } } datasetID := fmt.Sprintf("%d-%d", startTimeNanos, endTimeNanos) dataset := &fitness.Dataset{ DataSourceId: dataSourceID, MinStartTimeNs: startTimeNanos, MaxEndTimeNs: endTimeNanos, Point: points, } _, err := c.Service.Users.DataSources.Datasets.Patch(userID, dataSourceID, datasetID, dataset).Context(ctx).Do() if err != nil { log.Errorf(ctx, "c.Service.Users.DataSources.Datasets.Patch() = %v", err) return err } return nil } func (c *Client) SetSteps(ctx context.Context, steps int, date time.Time) error { const dataTypeName = "com.google.step_count.delta" dataSourceID, err := c.DataSourceCreate(ctx, &fitness.DataSource{ Application: Application(ctx), DataStreamId: "", // COMPUTED DataStreamName: "", // "daily summary"? DataType: &fitness.DataType{ Field: []*fitness.DataTypeField{ &fitness.DataTypeField{ Format: "integer", Name: "steps", }, }, Name: dataTypeName, }, Name: "Step Count", Type: "raw", }) if err != nil { return err } return c.DataSetPatch(ctx, dataSourceID, []*fitness.DataPoint{ &fitness.DataPoint{ ComputationTimeMillis: time.Now().UnixNano() / 1000000, DataTypeName: dataTypeName, StartTimeNanos: date.UnixNano(), EndTimeNanos: date.Add(24 * time.Hour).Add(-1 * time.Nanosecond).UnixNano(), Value: []*fitness.Value{ &fitness.Value{ IntVal: int64(steps), }, }, }, }) }