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 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