withings/client.go

361 lines
9.1 KiB
Go

// license that can be found in the LICENSE file.
// Package withings is UNOFFICIAL sdk of withings API for Go client.
//
package withings
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
"golang.org/x/oauth2"
"gopkg.in/yaml.v2"
)
const (
authURL = "http://account.withings.com/oauth2_user/authorize2"
tokenURL = "https://wbsapi.withings.net/v2/oauth2"
defaultTokenFile = ".access_token.json"
)
// Client type
type Client struct {
Client *http.Client
Conf *oauth2.Config
Token *oauth2.Token
Timeout time.Duration
MeasureURL string
MeasureURLv2 string
SleepURLv2 string
}
// ClientOption type for to customize http.Client
type ClientOption func(*http.Client) error
// AuthorizeOffline provides oauth2 authorization for withings in CLI.
// See example/main.go to know the detail.
func AuthorizeOffline(conf *oauth2.Config) (*oauth2.Token, error) {
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
fmt.Printf("URL to authorize:%s\n", url)
var grantcode string
fmt.Printf("Open url your browser and Enter your grant code here.\n Grant Code:")
fmt.Scan(&grantcode)
token, err := conf.Exchange(newOauthContext(), grantcode)
if err != nil {
fmt.Println("Failed to oauth2 exchange.")
return nil, err
}
return token, nil
}
// ReadSettings read setting file which is yaml file and returns the settings.
func ReadSettings(path2settings string) map[string]string {
f, err := os.Open(path2settings)
if err != nil {
log.Fatal(err)
}
defer f.Close()
d := yaml.NewDecoder(f)
var m map[string]string
if err := d.Decode(&m); err != nil {
log.Fatal(err)
}
return m
}
// getNewContext returns new context that has Timeout settings.
func getNewContext(timeout time.Duration) (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), timeout)
}
// New returns new client.
// cid is client id, secret and redirectURL are parameters that you got them when you setup withings API.
func New(cid, secret, redirectURL string, options ...ClientOption) (*Client, error) {
conf := GetNewConf(cid, secret, redirectURL)
c := &Client{}
c.Conf = &conf
c.Token = &oauth2.Token{}
c.Client = GetClient(c.Conf, c.Token)
c.Timeout = 5 * time.Second
c.MeasureURL = defaultMeasureURL
c.MeasureURLv2 = defaultMeasureURLv2
c.SleepURLv2 = defaultSleepURLv2
for _, option := range options {
err := option(c.Client)
if err != nil {
return nil, err
}
}
return c, nil
}
// SetScope sets scope for oauth2 client.
func (c *Client) SetScope(scopes ...string) {
c.Conf.Scopes = []string{strings.Join([]string(scopes), ",")}
}
// SetTimeout sets timeout setting for http client.
func (c *Client) SetTimeout(timeout time.Duration) {
c.Timeout = timeout * time.Second
}
// PrintTimeout print timeout setting.
func (c *Client) PrintTimeout() {
fmt.Printf("Timeout=%v\n", c.Timeout)
}
// ReadToken read from a file and that token is set to client.
func (c *Client) ReadToken(path2file string) (*oauth2.Token, error) {
t, err := readToken(path2file)
if err != nil {
return nil, err
}
c.Token = t
c.Client = GetClient(c.Conf, c.Token)
return c.Token, nil
}
func readToken(path2file string) (*oauth2.Token, error) {
token := &oauth2.Token{}
file, err := os.Open(path2file)
if err == nil {
json.NewDecoder(file).Decode(token)
}
return token, err
}
// SaveToken save the token in the file.
func (c *Client) SaveToken(path2file string) error {
var fname string = path2file
if fname == "" {
fname = defaultTokenFile
}
return saveToken(c.Token, fname)
}
func saveToken(t *oauth2.Token, path2file string) error {
file, err := os.Create(path2file)
if err != nil {
return err
}
err = json.NewEncoder(file).Encode(t)
if err != nil {
return err
}
return nil
}
// RefreshToken get new token if necessary.
func (c *Client) RefreshToken() (*oauth2.Token, bool, error) {
newToken, err := refreshToken(c.Conf, c.Token)
if err != nil {
return nil, false, err
}
var isNewToken bool = false
if newToken != c.Token {
c.Token = newToken
c.Client = GetClient(c.Conf, c.Token)
isNewToken = true
}
return c.Token, isNewToken, nil
}
func refreshToken(conf *oauth2.Config, token *oauth2.Token) (*oauth2.Token, error) {
newToken, err := (conf.TokenSource(newOauthContext(), token).Token())
if err != nil {
return nil, err
}
return newToken, nil
}
// Do is just call `Do` of http.Client.
func (c *Client) Do(req *http.Request) (*http.Response, error) {
ret, err := c.Client.Do(req)
return ret, err
}
// GetNewConf returns oauth2.Config with client id, secret, and redirectURL
func GetNewConf(cid, secret, redirectURL string) oauth2.Config {
scopes := []string{ScopeActivity, ScopeMetrics, ScopeInfo}
conf := oauth2.Config{
RedirectURL: redirectURL,
ClientID: cid,
ClientSecret: secret,
Scopes: []string{strings.Join([]string(scopes), ",")},
Endpoint: oauth2.Endpoint{
AuthURL: authURL,
TokenURL: tokenURL,
AuthStyle: oauth2.AuthStyleInParams,
},
}
return conf
}
// GetClient returns *http.Client which based on conf, token.
func GetClient(conf *oauth2.Config, token *oauth2.Token) *http.Client {
client := oauth2.NewClient(context.Background(), conf.TokenSource(newOauthContext(), token))
return client
}
// PrintToken print token information.
func (c *Client) PrintToken() {
printToken(c.Token)
}
func printToken(t *oauth2.Token) {
layout := "2006-01-02 15:04:05"
extraKeys := []string{"access_token", "expires_in", "refresh_token", "scope", "token_type", "userid"}
fmt.Printf("--Token Information--\n")
fmt.Printf("AccessToken:%s\n", t.AccessToken)
fmt.Printf("RefreshToken:%s\n", t.RefreshToken)
fmt.Printf("ExpiryDate:%s\n", t.Expiry.Format(layout))
fmt.Printf("TokenType:%s\n", t.TokenType)
// Extra returns interface{}
for _, k := range extraKeys {
switch val := t.Extra(k).(type) {
case string:
fmt.Printf("%s:%s\n", k, val)
case float64:
fmt.Printf("%s:%g\n", k, val)
default:
if val != nil {
fmt.Println(val)
}
}
}
}
// PrintConf print conf information.
func (c *Client) PrintConf() {
printConf(c.Conf)
}
func printConf(conf *oauth2.Config) {
fmt.Printf("RedirectURL: %v\n", conf.RedirectURL)
fmt.Printf("ClientID: %v\n", conf.ClientID)
fmt.Printf("ClientSecret: %v\n", conf.ClientSecret)
fmt.Printf("Scopes: %v\n", conf.Scopes)
fmt.Printf("Endpoint(AuthURL): %v\n", conf.Endpoint.AuthURL)
fmt.Printf("Endpoint(TokenURL): %v\n", conf.Endpoint.TokenURL)
}
// newOauthContext returns context.Context with
// custom http client for withings's access and refresh tokens endpoints.
func newOauthContext() context.Context {
c := &http.Client{Transport: &oauthTransport{}}
return context.WithValue(context.Background(), oauth2.HTTPClient, c)
}
// oauthTransport is making custom request and response for withings api.
type oauthTransport struct{}
// RoundTrip customize request and response for withings api.
func (t *oauthTransport) RoundTrip(r *http.Request) (*http.Response, error) {
if err := interceptRequest(r); err != nil {
return nil, err
}
res, err := http.DefaultTransport.RoundTrip(r)
if err != nil {
return nil, err
}
if err := interceptResponse(res); err != nil {
return nil, err
}
return res, nil
}
// interceptRequest sets action=requesttoken param.
// this param is required for withings api, but not oauth specification.
func interceptRequest(req *http.Request) error {
if err := req.ParseForm(); err != nil {
return fmt.Errorf("cannot parse request form: %v", err)
}
req.PostForm.Set("action", "requesttoken")
encoded := req.PostForm.Encode()
req.Body = ioutil.NopCloser(strings.NewReader(encoded))
req.ContentLength = int64(len(encoded))
return nil
}
// interceptResponse flattens response.
// withings's response body is nested.
// example)
// from:
// {
// "status": 0,
// "body": {
// "userid": "363",
// "access_token": "a075f8c14fb8df40b08ebc8508533dc332a6910a",
// "refresh_token": "f631236f02b991810feb774765b6ae8e6c6839ca",
// "expires_in": 10800,
// "scope": "user.info,user.metrics",
// "csrf_token": "PACnnxwHTaBQOzF7bQqwFUUotIuvtzSM",
// "token_type": "Bearer"
// }
// }
// to:
// {
// "userid": "363",
// "access_token": "a075f8c14fb8df40b08ebc8508533dc332a6910a",
// "refresh_token": "f631236f02b991810feb774765b6ae8e6c6839ca",
// "expires_in": 10800,
// "scope": "user.info,user.metrics",
// "csrf_token": "PACnnxwHTaBQOzF7bQqwFUUotIuvtzSM",
// "token_type": "Bearer"
// }
func interceptResponse(res *http.Response) error {
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
return fmt.Errorf("cannot read response body: %v", err)
}
var withingsRes struct {
Status int `json:"status"`
Body json.RawMessage `json:"body"`
Error string `json:"error,omitempty"`
}
err = json.NewDecoder(bytes.NewReader(body)).Decode(&withingsRes)
if err != nil || withingsRes.Error != "" {
return &oauth2.RetrieveError{
Response: res,
Body: body,
}
}
res.Body = ioutil.NopCloser(bytes.NewReader(withingsRes.Body))
res.ContentLength = int64(len(withingsRes.Body))
return nil
}