simplificator-withings 0.4.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|