simplificator-withings 0.4.5 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +9 -20
- data/lib/withings/base.rb +18 -0
- data/lib/withings/connection.rb +71 -5
- data/lib/withings/error.rb +4 -0
- data/lib/withings/user.rb +10 -54
- data/lib/withings.rb +2 -1
- data/simplificator-withings.gemspec +1 -1
- data/test/users_test.rb +2 -84
- metadata +6 -8
data/README.rdoc
CHANGED
@@ -4,6 +4,10 @@ This is a ruby implementation for the Withings API. Description of the API can b
|
|
4
4
|
|
5
5
|
== Versions ==
|
6
6
|
|
7
|
+
=== 0.6.0 ===
|
8
|
+
|
9
|
+
OAuth implemented. This version is not compatible to previous releases.
|
10
|
+
Authentication via userid/publickey is not supported anymore.
|
7
11
|
|
8
12
|
=== 0.4.5 ===
|
9
13
|
E-Mail Address is downcased before hashing it (authentication)
|
@@ -35,16 +39,11 @@ The old method is still there but please update your code. It will be removed in
|
|
35
39
|
== Installation
|
36
40
|
|
37
41
|
gem install simplificator-withings
|
42
|
+
gem install ruby-hmac
|
38
43
|
|
39
44
|
== Authentication
|
40
45
|
|
41
|
-
The WBS API
|
42
|
-
(they can be found in the sharing overlay on my.withings.com) or ask them for email/password once and then use those credentials
|
43
|
-
to authenticate through the API (User.authenticate(email, password)). The User instance returned will have the
|
44
|
-
user_id and public_key attributes populated and you can store them for further use.
|
45
|
-
|
46
|
-
As soon as you have user_id/public_key available you can either use User.info or User.new to create a User instance. While User.info
|
47
|
-
will make an API call to populate the attributes, User.new just requires user_id/public_key.
|
46
|
+
The WBS API now uses OAuth. See the API documentation for details.
|
48
47
|
|
49
48
|
== TODO
|
50
49
|
|
@@ -85,19 +84,9 @@ Require the API implementation
|
|
85
84
|
All classes are name-spaced, if your other code allows you can include Withings
|
86
85
|
include Withings
|
87
86
|
|
88
|
-
A user can be authenticated
|
89
|
-
user = User.
|
90
|
-
|
91
|
-
Or you can fetch details if you have the user id and public key
|
92
|
-
user = User.info('<YOUR USER ID>', '<YOUR PUBLIC KEY>')
|
93
|
-
|
94
|
-
If you already have user id and public key and you do not need further information on the user
|
95
|
-
user = User.new(:user_id => '<YOUR USER ID>', :public_key => '<YOUR PUBLIC_KEY>')
|
87
|
+
A user can be authenticated using user id and oauth token/secret
|
88
|
+
user = User.authenticate('<YOUR USER ID>', '<YOUR OAUTH TOKEN>', '<YOUR OAUTH TOKEN SECRET>')
|
96
89
|
|
97
|
-
enable/disable sharing, disabling it will reset the public key
|
98
|
-
user.share() # share all
|
99
|
-
user.share(Withings::SCALE, Withings::BLOOD_PRESSURE_MONITOR)
|
100
|
-
user.share(0)
|
101
90
|
|
102
91
|
You can handle subscriptions through the API (all devices currently known => Scale + Blood Pressure Monitor)
|
103
92
|
user.subscribe_notification('http://foo.bar.com', 'test subscription')
|
@@ -147,4 +136,4 @@ Thanks for these Gems.
|
|
147
136
|
|
148
137
|
== Copyright
|
149
138
|
|
150
|
-
Copyright (c)
|
139
|
+
Copyright (c) 2012 simplificator. See LICENSE for details.
|
data/lib/withings/base.rb
CHANGED
@@ -1,6 +1,24 @@
|
|
1
1
|
module Withings
|
2
2
|
SCALE = 1
|
3
3
|
BLOOD_PRESSURE_MONITOR = 4
|
4
|
+
|
5
|
+
def self.consumer_secret=(value)
|
6
|
+
@consumer_secret = value
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.consumer_secret
|
10
|
+
raise 'Please specify consumer_secret' unless @consumer_secret
|
11
|
+
@consumer_secret
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.consumer_key=(value)
|
15
|
+
@consumer_key = value
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.consumer_key
|
19
|
+
raise 'Please specify consumer_key' unless @consumer_key
|
20
|
+
@consumer_key
|
21
|
+
end
|
4
22
|
end
|
5
23
|
|
6
24
|
# Copied over from ActiveSupport
|
data/lib/withings/connection.rb
CHANGED
@@ -14,19 +14,71 @@ class Withings::Connection
|
|
14
14
|
@user = user
|
15
15
|
end
|
16
16
|
|
17
|
-
def self.get_request(path, params)
|
17
|
+
def self.get_request(path, token, secret, params)
|
18
|
+
signature = Connection.sign(base_uri + path, params, token, secret)
|
19
|
+
params.merge!({:oauth_signature => signature})
|
20
|
+
|
18
21
|
response = self.get(path, :query => params)
|
19
22
|
verify_response!(response, path, params)
|
20
23
|
end
|
21
24
|
|
22
|
-
|
25
|
+
|
23
26
|
def get_request(path, params)
|
24
|
-
params
|
27
|
+
params.merge!({:userid => @user.user_id})
|
28
|
+
signature = Connection.sign(self.class.base_uri + path, params, @user.oauth_token, @user.oauth_token_secret)
|
29
|
+
params.merge!({:oauth_signature => signature})
|
30
|
+
|
25
31
|
response = self.class.get(path, :query => params)
|
26
32
|
self.class.verify_response!(response, path, params)
|
27
33
|
end
|
28
|
-
|
34
|
+
|
29
35
|
protected
|
36
|
+
|
37
|
+
def self.sign(url, params, token, secret)
|
38
|
+
params.merge!({
|
39
|
+
:oauth_consumer_key => Withings.consumer_key,
|
40
|
+
:oauth_nonce => oauth_nonce,
|
41
|
+
:oauth_signature_method => oauth_signature_method,
|
42
|
+
:oauth_timestamp => oauth_timestamp,
|
43
|
+
:oauth_version => oauth_version,
|
44
|
+
:oauth_token => token
|
45
|
+
})
|
46
|
+
calculate_oauth_signature('GET', url, params, secret)
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def self.oauth_timestamp
|
51
|
+
Time.now.to_i
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.oauth_version
|
55
|
+
'1.0'
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.oauth_signature_method
|
59
|
+
'HMAC-SHA1'
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.oauth_nonce
|
63
|
+
rand(10 ** 30).to_s(16)
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.calculate_oauth_signature(method, url, params, oauth_token_secret)
|
67
|
+
# oauth signing is picky with sorting (based on a digest)
|
68
|
+
params = params.to_a.map() do |item|
|
69
|
+
[item.first.to_s, item.last]
|
70
|
+
end.sort
|
71
|
+
|
72
|
+
param_string = params.map() {|key, value| "#{key}=#{value}"}.join('&')
|
73
|
+
base_string = [method, CGI.escape(url), CGI.escape(param_string)].join('&')
|
74
|
+
|
75
|
+
secret = [Withings.consumer_secret, oauth_token_secret].join('&')
|
76
|
+
|
77
|
+
digest = HMAC::SHA1.digest(secret, base_string)
|
78
|
+
Base64.encode64(digest).chomp.gsub( /\n/, '' )
|
79
|
+
end
|
80
|
+
|
81
|
+
|
30
82
|
# Verifies the status code in the JSON response and returns either the body element or raises ApiError
|
31
83
|
def self.verify_response!(response, path, params)
|
32
84
|
if response['status'] == 0
|
@@ -35,4 +87,18 @@ class Withings::Connection
|
|
35
87
|
raise Withings::ApiError.new(response['status'], path, params)
|
36
88
|
end
|
37
89
|
end
|
38
|
-
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
#http://wbsapi.withings.net/measure?action=getmeas&
|
94
|
+
#oauth_consumer_key=7e563166232c6821742b4c277350494a455f392b353e5d49712a34762a&
|
95
|
+
#oauth_nonce=f22d74f2209ddf0c6558a47c02841fb1&
|
96
|
+
#oauth_signature=yAF9SgZa09SPl3H1Y5aAoXgyauc=&
|
97
|
+
#oauth_token=c68567f1760552958d713e92088db9f5c5189754dfe4e92068971f4e25d64&
|
98
|
+
#oauth_version=1.0&
|
99
|
+
#userid=1229
|
100
|
+
|
101
|
+
#User: Tobias Miesel
|
102
|
+
#user_id: 666088
|
103
|
+
#oauth_token: 284948c9b4b9cce1cc76bbb77283431d9bbb9b46beddfccb79241cc12
|
104
|
+
#oauth_token_secret: 02f01f0e60182684676644ddbef2638e8e4de909f776340e1b5dd612dcbf
|
data/lib/withings/error.rb
CHANGED
@@ -3,12 +3,16 @@ class Withings::ApiError < StandardError
|
|
3
3
|
STATUS_CODES = {
|
4
4
|
100 => lambda() {|status, path, params| "The hash '#{params[:hash]}' does not match the email '#{params[:email]}'"},
|
5
5
|
247 => lambda() {|status, path, params| "The userid '#{params[:userid]}' is invalid"},
|
6
|
+
249 => lambda() {|status, path, params| "Called an action with invalid oauth credentials"},
|
6
7
|
250 => lambda() {|status, path, params| "The userid '#{params[:userid]}' and publickey '#{params[:publickey]}' do not match, or the user does not share its data"},
|
7
8
|
264 => lambda() {|status, path, params| "The email address '#{params[:email]}' is either unknown or invalid"},
|
9
|
+
284 => lambda() {|status, path, params| "Temporary Server Error" },
|
8
10
|
286 => lambda() {|status, path, params| "No subscription for '#{params[:callbackurl]}' was found" },
|
9
11
|
293 => lambda() {|status, path, params| "The callback URL '#{params[:callbackurl]}' is either unknown or invalid"},
|
10
12
|
294 => lambda() {|status, path, params| "Could not delete subscription for '#{params[:callbackurl]}'"},
|
11
13
|
304 => lambda() {|status, path, params| "The comment '#{params[:comment]}' is invalid"},
|
14
|
+
342 => lambda() {|status, path, params| "Failed to verify signature"},
|
15
|
+
343 => lambda() {|status, path, params| "No notification matching the criteria was found: '#{params[:callbackurl]}'"},
|
12
16
|
2554 => lambda() {|status, path, params| "Unknown action '#{params[:action]}' for '#{path}'"},
|
13
17
|
2555 => lambda() {|status, path, params| "An unknown error occurred"},
|
14
18
|
}
|
data/lib/withings/user.rb
CHANGED
@@ -1,52 +1,27 @@
|
|
1
1
|
class Withings::User
|
2
|
-
attr_reader :short_name, :
|
2
|
+
attr_reader :short_name, :user_id, :birthdate, :fat_method, :first_name, :last_name, :gender, :oauth_token, :oauth_token_secret
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
response['users'].map do |item|
|
10
|
-
Withings::User.new(item)
|
11
|
-
end
|
4
|
+
def self.authenticate(user_id, oauth_token, oauth_token_secret)
|
5
|
+
response = Withings::Connection.get_request('/user', oauth_token, oauth_token_secret, :action => :getbyuserid, :userid => user_id)
|
6
|
+
user_data = response['users'].detect { |item| item['id'] == user_id }
|
7
|
+
raise Withings::ApiError.new(2555, 'No user found', '') unless user_data
|
8
|
+
Withings::User.new(user_data.merge({:oauth_token => oauth_token, :oauth_token_secret => oauth_token_secret}))
|
12
9
|
end
|
13
10
|
|
14
|
-
|
15
|
-
# Authenticate a user by email/password
|
16
|
-
#
|
17
|
-
def self.authenticate(email, password)
|
18
|
-
$stderr.puts <<-EOS
|
19
|
-
User.authenticate(email, pwd) has been deprecated in favour of User.userlist(email, pwd) as there is no description or guarantee
|
20
|
-
about the order the users are returned.
|
21
|
-
If you need the same behaviour as before: User.userlist(email, pwd).first
|
22
|
-
EOS
|
23
|
-
response = Withings::Connection.get_request('/account', :action => :getuserslist, :email => email, :hash => auth_hash(email, password))
|
24
|
-
Withings::User.new(response['users'].first)
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.info(user_id, public_key)
|
28
|
-
response = Withings::Connection.get_request('/user', :action => :getbyuserid, :userid => user_id, :publickey => public_key)
|
29
|
-
Withings::User.new(response['users'].first.merge({'public_key' => public_key}))
|
30
|
-
end
|
31
|
-
|
32
|
-
|
33
|
-
#
|
34
|
-
# If you create a user yourself, then the only attributes of interest (required for calls to the API) are 'user_id' and 'public_key'
|
35
|
-
#
|
11
|
+
# If you create a user yourself, then the only attributes of interest (required for calls to the API) are 'user_id' and 'oauth_token' and 'oauth_token_secret'
|
36
12
|
def initialize(params)
|
37
13
|
params = params.stringify_keys
|
38
14
|
@short_name = params['shortname']
|
39
15
|
@first_name = params['firstname']
|
40
16
|
@last_name = params['lastname']
|
41
|
-
@public_key = params['publickey'] || params['public_key']
|
42
17
|
@user_id = params['id'] || params['user_id']
|
43
|
-
@share = params['ispublic']
|
44
18
|
@birthdate = Time.at(params['birthdate']) if params['birthdate']
|
45
19
|
@gender = params['gender'] == 0 ? :male : params['gender'] == 1 ? :female : nil
|
46
20
|
@fat_method = params['fatmethod']
|
21
|
+
@oauth_token = params['oauth_token']
|
22
|
+
@oauth_token_secret = params['oauth_token_secret']
|
47
23
|
end
|
48
24
|
|
49
|
-
|
50
25
|
def subscribe_notification(callback_url, description, device = SCALE)
|
51
26
|
connection.get_request('/notify', :action => :subscribe, :callbackurl => callback_url, :comment => description, :appli => device)
|
52
27
|
end
|
@@ -89,16 +64,6 @@ class Withings::User
|
|
89
64
|
end
|
90
65
|
end
|
91
66
|
|
92
|
-
def share(*devices)
|
93
|
-
@share = devices_bitmask(devices)
|
94
|
-
connection.get_request('/user', :action => :update, :ispublic => @share)
|
95
|
-
end
|
96
|
-
|
97
|
-
# sharing enabled for a device?
|
98
|
-
def share?(device = Withings::SCALE | Withings::BLOOD_PRESSURE_MONITOR)
|
99
|
-
@share & device
|
100
|
-
end
|
101
|
-
|
102
67
|
def to_s
|
103
68
|
"[User #{short_name} / #{:user_id} / #{share?}]"
|
104
69
|
end
|
@@ -114,14 +79,5 @@ class Withings::User
|
|
114
79
|
def connection
|
115
80
|
@connection ||= Withings::Connection.new(self)
|
116
81
|
end
|
117
|
-
|
118
|
-
def self.auth_hash(email, password)
|
119
|
-
hashed_password = Digest::MD5.hexdigest(password)
|
120
|
-
Digest::MD5.hexdigest("#{email.downcase}:#{hashed_password}:#{once}")
|
121
|
-
end
|
122
|
-
|
123
|
-
def self.once()
|
124
|
-
Withings::Connection.get_request('/once', :action => :get)['once']
|
125
|
-
end
|
126
|
-
|
82
|
+
|
127
83
|
end
|
data/lib/withings.rb
CHANGED
data/test/users_test.rb
CHANGED
@@ -3,76 +3,9 @@ require 'helper'
|
|
3
3
|
include Withings
|
4
4
|
|
5
5
|
class UsersTest < Test::Unit::TestCase
|
6
|
-
context 'test connection calls' do
|
7
|
-
setup do
|
8
|
-
@user = User.new('user_id' => 12345, 'public_key' => 67890)
|
9
|
-
end
|
10
|
-
should 'update user' do
|
11
|
-
Withings::Connection.any_instance.expects(:get_request).with('/user', :action => :update, :ispublic => 1)
|
12
|
-
@user.share = true
|
13
|
-
end
|
14
|
-
|
15
|
-
should 'subscribe to notification' do
|
16
|
-
Withings::Connection.any_instance.expects(:get_request).with('/notify', :action => :subscribe, :callbackurl => 'http://schni.com', :comment => 'descri')
|
17
|
-
@user.subscribe_notification('http://schni.com', 'descri')
|
18
|
-
end
|
19
|
-
|
20
|
-
should 'revoke notification' do
|
21
|
-
Withings::Connection.any_instance.expects(:get_request).with('/notify', :action => :revoke, :callbackurl => 'http://schni.com')
|
22
|
-
@user.revoke_notification('http://schni.com')
|
23
|
-
end
|
24
|
-
|
25
|
-
context 'describe notification' do
|
26
|
-
setup do
|
27
|
-
Withings::Connection.
|
28
|
-
any_instance.expects(:get_request).with('/notify', :action => :get, :callbackurl => 'http://schni.com').
|
29
|
-
returns({'comment' => 'blabla', 'expires' => 1234})
|
30
|
-
@description = @user.describe_notification('http://schni.com')
|
31
|
-
end
|
32
|
-
should 'merge the callback url into the descripton' do
|
33
|
-
assert_equal 'http://schni.com', @description.callback_url
|
34
|
-
end
|
35
|
-
|
36
|
-
should 'contain the expires_at time' do
|
37
|
-
assert_equal Time.at(1234), @description.expires_at
|
38
|
-
end
|
39
|
-
|
40
|
-
should 'contain the description' do
|
41
|
-
assert_equal 'blabla', @description.description
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
context 'authentication by email' do
|
47
|
-
setup do
|
48
|
-
password = 'kongking'
|
49
|
-
email = 'king@kong.com'
|
50
|
-
once = 'abcdef'
|
51
|
-
hashed = Digest::MD5.hexdigest("#{email}:#{Digest::MD5.hexdigest(password)}:#{once}")
|
52
|
-
Connection.expects(:get_request).with('/once', :action => :get).returns({'once' => once})
|
53
|
-
Connection.expects(:get_request).with('/account', :action => :getuserslist, :email => email, :hash => hashed).
|
54
|
-
returns({'users' => [{}]})
|
55
|
-
end
|
56
|
-
should 'authenticate with hashed password' do
|
57
|
-
User.authenticate('king@kong.com', 'kongking')
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
context 'info by user_id' do
|
62
|
-
setup do
|
63
|
-
user_id = 'kongking'
|
64
|
-
public_key = 'abcdef'
|
65
|
-
Connection.expects(:get_request).with('/user', :action => :getbyuserid, :userid => user_id, :publickey => public_key).
|
66
|
-
returns({'users' => [{}]})
|
67
|
-
end
|
68
|
-
should 'authenticate with hashed password' do
|
69
|
-
User.info('kongking', 'abcdef')
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
6
|
context 'measurement_groups' do
|
74
7
|
setup do
|
75
|
-
@user = User.new(:user_id => 'lala'
|
8
|
+
@user = User.new(:user_id => 'lala')
|
76
9
|
@returns = {'measuregrps' => []}
|
77
10
|
end
|
78
11
|
should 'not require parameters' do
|
@@ -131,28 +64,13 @@ class UsersTest < Test::Unit::TestCase
|
|
131
64
|
assert_equal 'Pascal', User.new('lastname' => 'Pascal').last_name
|
132
65
|
end
|
133
66
|
|
134
|
-
should 'assign public_key' do
|
135
|
-
assert_equal '1234', User.new('publickey' => '1234').public_key
|
136
|
-
end
|
137
|
-
should 'assign public_key with alternative key' do
|
138
|
-
assert_equal '1234', User.new('public_key' => '1234').public_key
|
139
|
-
end
|
140
|
-
|
141
67
|
should 'assign user_id' do
|
142
68
|
assert_equal '1234', User.new('id' => '1234').user_id
|
143
69
|
end
|
144
70
|
should 'assign user_id with alternative key' do
|
145
71
|
assert_equal '1234', User.new('user_id' => '1234').user_id
|
146
72
|
end
|
147
|
-
|
148
|
-
|
149
|
-
should 'assign share (to true)' do
|
150
|
-
assert_equal true, User.new('ispublic' => 1).share?
|
151
|
-
end
|
152
|
-
should 'assign share (to false)' do
|
153
|
-
assert_equal false, User.new('ispublic' => 0).share?
|
154
|
-
end
|
155
|
-
|
73
|
+
|
156
74
|
should 'assign gender (to true)' do
|
157
75
|
assert_equal :male, User.new('gender' => 0).gender
|
158
76
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simplificator-withings
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 7
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 6
|
9
|
+
- 0
|
10
|
+
version: 0.6.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- pascalbetz
|
@@ -15,8 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-04-18 00:00:00
|
19
|
-
default_executable:
|
18
|
+
date: 2011-04-18 00:00:00 Z
|
20
19
|
dependencies:
|
21
20
|
- !ruby/object:Gem::Dependency
|
22
21
|
name: shoulda
|
@@ -84,7 +83,6 @@ files:
|
|
84
83
|
- simplificator-withings.gemspec
|
85
84
|
- test/helper.rb
|
86
85
|
- test/users_test.rb
|
87
|
-
has_rdoc: true
|
88
86
|
homepage: http://github.com/simplificator/simplificator-withings
|
89
87
|
licenses: []
|
90
88
|
|
@@ -114,7 +112,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
114
112
|
requirements: []
|
115
113
|
|
116
114
|
rubyforge_project:
|
117
|
-
rubygems_version: 1.
|
115
|
+
rubygems_version: 1.8.15
|
118
116
|
signing_key:
|
119
117
|
specification_version: 3
|
120
118
|
summary: API implementation for withings.com
|