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 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 provides a way to login by user_id and public_key. You can either ask your users to enter the user_id/public_key manually
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 with email address and password
89
- user = User.userlist('<YOUR EMAIL ADDRESS>', '<YOUR PASSWORD>').first # or any other user which is returned
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) 2010 simplificator. See LICENSE for details.
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
@@ -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
- # Merges the params with public_key and user_id for authentication.
25
+
23
26
  def get_request(path, params)
24
- params = params.merge(:publickey => @user.public_key, :userid => @user.user_id)
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
@@ -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, :public_key, :user_id, :birthdate, :fat_method, :first_name, :last_name, :gender
2
+ attr_reader :short_name, :user_id, :birthdate, :fat_method, :first_name, :last_name, :gender, :oauth_token, :oauth_token_secret
3
3
 
4
-
5
- # Listing the users for this account
6
- #
7
- def self.userlist(email, password)
8
- response = Withings::Connection.get_request('/account', :action => :getuserslist, :email => email, :hash => auth_hash(email, password))
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
@@ -1,5 +1,6 @@
1
1
  require 'httparty'
2
- require 'digest/md5'
2
+ require 'cgi'
3
+ require 'hmac-sha1'
3
4
 
4
5
  %w(base notification_description connection measurement_group error user).each do |part|
5
6
  require File.join(File.dirname(__FILE__), 'withings', part)
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{simplificator-withings}
5
- s.version = "0.4.5"
5
+ s.version = "0.6.0"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["pascalbetz"]
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', :public_key => 'lili')
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: 5
4
+ hash: 7
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 4
9
- - 5
10
- version: 0.4.5
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 +02: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.4.2
115
+ rubygems_version: 1.8.15
118
116
  signing_key:
119
117
  specification_version: 3
120
118
  summary: API implementation for withings.com