withings-api 0.0.3
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/.gitignore +9 -0
- data/.simplecov +5 -0
- data/Gemfile +4 -0
- data/LICENSE +7 -0
- data/README.rdoc +84 -0
- data/Rakefile +14 -0
- data/cucumber.yml +2 -0
- data/examples/callback_landing.txt +1 -0
- data/examples/create_access_token.rb +62 -0
- data/features/get_access_token.feature +70 -0
- data/features/get_request_token.feature +68 -0
- data/features/measure_getmeas_api.feature +16 -0
- data/features/step_definitions/api.rb +113 -0
- data/features/support/method_mocker.rb +36 -0
- data/features/support/world.rb +34 -0
- data/lib/withings-api.rb +19 -0
- data/lib/withings-api/api_actions.rb +27 -0
- data/lib/withings-api/api_response.rb +23 -0
- data/lib/withings-api/consts.rb +12 -0
- data/lib/withings-api/errors.rb +23 -0
- data/lib/withings-api/oauth_actions.rb +94 -0
- data/lib/withings-api/oauth_base.rb +121 -0
- data/lib/withings-api/query_string.rb +16 -0
- data/lib/withings-api/results/measure_getmeas_results.rb +73 -0
- data/lib/withings-api/tokens.rb +35 -0
- data/lib/withings-api/types.rb +108 -0
- data/lib/withings-api/utils.rb +14 -0
- data/lib/withings-api/version.rb +5 -0
- data/spec/api_actions/measure_getmeas_spec.rb +22 -0
- data/spec/measure_getmeas_results_spec.rb +124 -0
- data/spec/method_aliaser_spec.rb +96 -0
- data/spec/query_string_spec.rb +20 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/tokens_spec.rb +38 -0
- data/spec/types/atttribution_type_spec.rb +15 -0
- data/spec/types/category_type_spec.rb +15 -0
- data/spec/types/device_type_spec.rb +15 -0
- data/spec/types/measurement_type_spec.rb +15 -0
- data/spec/withings_api_spec.rb +67 -0
- data/test/README +1 -0
- data/test/helpers/http_stubber.rb +32 -0
- data/test/helpers/method_aliaser.rb +48 -0
- data/test/helpers/stubbed_withings_api.rb +41 -0
- data/test/http_stub_responses/access_token/invalid.txt +10 -0
- data/test/http_stub_responses/access_token/success.txt +11 -0
- data/test/http_stub_responses/access_token/unauthorized_token.txt +11 -0
- data/test/http_stub_responses/authorization_callback/success.txt +9 -0
- data/test/http_stub_responses/measure_getmeas/forbidden.txt +12 -0
- data/test/http_stub_responses/measure_getmeas/oauth_error.txt +9 -0
- data/test/http_stub_responses/measure_getmeas/success.txt +9 -0
- data/test/http_stub_responses/request_token/invalid_consumer_credentials.txt +10 -0
- data/test/http_stub_responses/request_token/success.txt +11 -0
- data/test/sample_json/measure_getmeas.json +49 -0
- data/test/sample_json/notify_get.json +7 -0
- data/test/sample_json/notify_list.json +15 -0
- data/test/sample_json/notify_revoke.json +3 -0
- data/test/sample_json/notify_subscribe.json +3 -0
- data/test/sample_json/once_probe.json +1 -0
- data/test/sample_json/user_getbyuserid.json +16 -0
- data/withings-api.gemspec +32 -0
- metadata +220 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# ss
|
3
|
+
#
|
4
|
+
|
5
|
+
require_relative "../../test/helpers/method_aliaser"
|
6
|
+
require 'net/http'
|
7
|
+
|
8
|
+
def before_after_method_wrap(clazz, method_sym, &block)
|
9
|
+
wrap = nil
|
10
|
+
|
11
|
+
Before do
|
12
|
+
wrap = MethodAliaser.alias_it(clazz, method_sym, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
After do
|
16
|
+
wrap.unalias_it
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def print_http_req_resp
|
21
|
+
before_after_method_wrap(Net::HTTP, :transport_request) do |aliased, *arguments|
|
22
|
+
puts "HTTP Request: #{arguments.first.path}"
|
23
|
+
res = aliased.call(*arguments)
|
24
|
+
puts "HTTP Response:"
|
25
|
+
puts "HTTP/#{res.http_version} #{res.code} #{res.message}"
|
26
|
+
res.to_hash.each_pair do |key, value|
|
27
|
+
puts "#{key}: #{value.join("; ")}"
|
28
|
+
end
|
29
|
+
puts ""
|
30
|
+
puts res.body
|
31
|
+
|
32
|
+
res
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
print_http_req_resp
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'capybara/cucumber'
|
2
|
+
|
3
|
+
module ApiCucumberHelpers
|
4
|
+
# executes the given block, storing the return value
|
5
|
+
# in an instance variable named "name" or the exception
|
6
|
+
# result in "name"_exception
|
7
|
+
def result_or_exception(name, &block)
|
8
|
+
begin
|
9
|
+
self.instance_variable_set("@#{name}", yield)
|
10
|
+
rescue => e
|
11
|
+
puts e
|
12
|
+
self.instance_variable_set("@#{name}_exception", e)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def logger
|
17
|
+
Cucumber.logger
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
World(ApiCucumberHelpers)
|
22
|
+
|
23
|
+
# Capybara Setup
|
24
|
+
|
25
|
+
Capybara.default_driver = :selenium
|
26
|
+
Capybara.default_wait_time = 5
|
27
|
+
World(Capybara::DSL)
|
28
|
+
|
29
|
+
# overwrite puts to make it's output more
|
30
|
+
# harmonious with the stylized Cucumber
|
31
|
+
# output
|
32
|
+
def puts(o)
|
33
|
+
Kernel.puts " \33[36m#{o}\33[0m"
|
34
|
+
end
|
data/lib/withings-api.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "withings-api/version"
|
2
|
+
|
3
|
+
require "withings-api/consts"
|
4
|
+
require "withings-api/query_string"
|
5
|
+
require "withings-api/utils"
|
6
|
+
require "withings-api/types"
|
7
|
+
require "withings-api/tokens"
|
8
|
+
require "withings-api/errors"
|
9
|
+
require "withings-api/oauth_base"
|
10
|
+
require "withings-api/oauth_actions"
|
11
|
+
require "withings-api/api_response"
|
12
|
+
require "withings-api/api_actions"
|
13
|
+
|
14
|
+
module Withings
|
15
|
+
module Api
|
16
|
+
extend OAuthActions
|
17
|
+
extend ApiActions
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "withings-api/results/measure_getmeas_results"
|
2
|
+
|
3
|
+
module Withings
|
4
|
+
module Api
|
5
|
+
module ApiActions
|
6
|
+
include OAuthBase
|
7
|
+
|
8
|
+
def measure_getmeas(parameters = {}, access_token = nil, consumer_token = nil)
|
9
|
+
consumer_token = consumer_token(consumer_token)
|
10
|
+
access_token = access_token(access_token)
|
11
|
+
|
12
|
+
http_response = oauth_http_request!(consumer_token, access_token, {:path => "http://wbsapi.withings.net/measure?action=getmeas", :parameters => parameters})
|
13
|
+
Withings::Api::ApiResponse.create!(http_response, Withings::Api::MeasureGetmeasResults)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def consumer_token(o)
|
19
|
+
@consumer_token || o || raise(StandardError, "No consumer token")
|
20
|
+
end
|
21
|
+
|
22
|
+
def access_token(o)
|
23
|
+
@consumer_token || o || raise(StandardError, "No access token")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Withings::Api
|
2
|
+
class ApiResponse
|
3
|
+
include ResultsHelpers
|
4
|
+
|
5
|
+
attr_reader :status, :body
|
6
|
+
|
7
|
+
def self.create!(http_response, body_class)
|
8
|
+
raise HttpNotSuccessError.new(http_response.code, http_response.body) if http_response.code != '200'
|
9
|
+
|
10
|
+
self.new(http_response.body, body_class)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(string_or_json, body_class)
|
14
|
+
hash = coerce_hash string_or_json
|
15
|
+
|
16
|
+
@status = hash["status"] || raise(InvalidFormat, :status_field_missing)
|
17
|
+
|
18
|
+
if hash.key?("body")
|
19
|
+
@body = body_class.new(hash["body"])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Withings
|
2
|
+
module Api
|
3
|
+
module Defaults
|
4
|
+
API_BASE_URL = "http://wbsapi.withings.net"
|
5
|
+
OAUTH_BASE_URL = "https://oauth.withings.com"
|
6
|
+
|
7
|
+
OAUTH_REQUEST_TOKEN_PATH = "/account/request_token"
|
8
|
+
OAUTH_AUTHORIZE_PATH = "/account/authorize"
|
9
|
+
OAUTH_ACCESS_TOKEN_PATH = "/account/access_token"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Withings
|
2
|
+
module Api
|
3
|
+
class Error < StandardError
|
4
|
+
|
5
|
+
end
|
6
|
+
|
7
|
+
class InvalidFormat < Error
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
class HttpNotSuccessError < Error
|
12
|
+
attr_accessor :code, :body
|
13
|
+
|
14
|
+
def initialize(code, body = "")
|
15
|
+
super(code)
|
16
|
+
|
17
|
+
@code = code
|
18
|
+
@body = body
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'oauth'
|
2
|
+
|
3
|
+
module Withings
|
4
|
+
module Api
|
5
|
+
module OAuth
|
6
|
+
include ::OAuth
|
7
|
+
end
|
8
|
+
|
9
|
+
# Simple API to ease the OAuth setup steps for Withing API client apps.
|
10
|
+
#
|
11
|
+
# Specifically, this class provides methods for OAuth access token creation.
|
12
|
+
#
|
13
|
+
# 1. Request request tokens - via {#create_request_token}
|
14
|
+
# 1. Redirect to authorization URL (this is handled outside of these methods, ie: by the webapp, etc.)
|
15
|
+
# 1. Request access tokens (for permanent access to Withings content) - via {#create_access_token}
|
16
|
+
module OAuthActions
|
17
|
+
include OAuthBase
|
18
|
+
|
19
|
+
Defaults = Withings::Api::Defaults
|
20
|
+
|
21
|
+
# Issues the "request_token" oauth HTTP request to Withings.
|
22
|
+
#
|
23
|
+
# For call details @ Withings, see http://www.withings.com/en/api/oauthguide#access
|
24
|
+
#
|
25
|
+
# For details about registering your application with Withings, see http://www.withings.com/en/api/oauthguide#registration
|
26
|
+
#
|
27
|
+
# @param [String] consumer_token the consumer key Withings assigned on app registration
|
28
|
+
# @param [String] consumer_secret the consumer secret Withings assigned on app registration
|
29
|
+
# @param [String] callback_url the URL Withings should return the user to after authorization
|
30
|
+
#
|
31
|
+
# @return [RequestTokenResponse] something encapsulating the request response
|
32
|
+
#
|
33
|
+
# @raise [Timeout::Error] on connection, or read timeout
|
34
|
+
# @raise [SystemCallError] on low level system call errors (connection timeout, connection refused)
|
35
|
+
# @raise [ProtocolError] for HTTP 5XX error response codes
|
36
|
+
# @raise [OAuth::Unauthorized] for HTTP 4XX error reponse codes
|
37
|
+
# @raise [StandardError] for everything else
|
38
|
+
def create_request_token(consumer_token, *arguments)
|
39
|
+
_consumer_token, _consumer_secret, _callback_url = nil
|
40
|
+
|
41
|
+
if arguments.length == 1 && consumer_token.instance_of?(Withings::Api::ConsumerToken)
|
42
|
+
_consumer_token, _consumer_secret = consumer_token.to_a
|
43
|
+
elsif arguments.length == 2
|
44
|
+
_consumer_token = consumer_token
|
45
|
+
_consumer_secret = arguments.shift
|
46
|
+
else
|
47
|
+
raise(ArgumentError)
|
48
|
+
end
|
49
|
+
_callback_url = arguments.shift
|
50
|
+
|
51
|
+
# TODO: warn if the callback URL isn't HTTPS
|
52
|
+
consumer = create_consumer(_consumer_token, _consumer_secret)
|
53
|
+
oauth_request_token = consumer.get_request_token({:oauth_callback => _callback_url})
|
54
|
+
|
55
|
+
RequestTokenResponse.new oauth_request_token
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# Issues the "access_token" oauth HTTP request to Withings
|
60
|
+
#
|
61
|
+
# @param [RequestTokenResponse] request_token request token returned from {#create_request_token}
|
62
|
+
# @param [String] user_id user id as returned from Withings via the {RequestTokenResponse#authorization_url}
|
63
|
+
#
|
64
|
+
# @return [] the shit
|
65
|
+
def create_access_token(request_token, *arguments)
|
66
|
+
_consumer, _request_token, _user_id = nil
|
67
|
+
|
68
|
+
if request_token.instance_of?(RequestTokenResponse) && arguments.length == 1
|
69
|
+
_consumer = request_token.oauth_consumer
|
70
|
+
_request_token = request_token.oauth_request_token
|
71
|
+
_user_id = arguments.shift
|
72
|
+
elsif request_token.instance_of?(RequestToken) && arguments.length == 2
|
73
|
+
request_token.instance_of?(ConsumerToken)
|
74
|
+
_consumer = create_consumer(*arguments.shift.to_a)
|
75
|
+
_request_token = OAuth::RequestToken.new(_consumer, *request_token.to_a)
|
76
|
+
_user_id = arguments.shift
|
77
|
+
else
|
78
|
+
raise ArgumentError
|
79
|
+
end
|
80
|
+
|
81
|
+
oauth_access_token = _consumer.get_access_token(_request_token)
|
82
|
+
|
83
|
+
# test for unauthorized token, since oauth + withings doesn't turn this into an explicit
|
84
|
+
# error code / exception
|
85
|
+
if oauth_access_token.params.key?(:"unauthorized token")
|
86
|
+
raise StandardError, :"unauthorized token"
|
87
|
+
end
|
88
|
+
|
89
|
+
AccessTokenResponse.new oauth_access_token
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module Withings::Api
|
4
|
+
|
5
|
+
module OAuthBase
|
6
|
+
private
|
7
|
+
|
8
|
+
def create_consumer(consumer_key, consumer_secret)
|
9
|
+
OAuth::Consumer.new(consumer_key, consumer_secret, {
|
10
|
+
# todo, this needs to be parameterized
|
11
|
+
:site => Defaults::OAUTH_BASE_URL,
|
12
|
+
:scheme => :query_string,
|
13
|
+
:http_method => :get,
|
14
|
+
:signature_method => 'HMAC-SHA1',
|
15
|
+
:request_token_path => Defaults::OAUTH_REQUEST_TOKEN_PATH,
|
16
|
+
:authorize_path => Defaults::OAUTH_AUTHORIZE_PATH,
|
17
|
+
:access_token_path => Defaults::OAUTH_ACCESS_TOKEN_PATH,
|
18
|
+
})
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_signed_request(consumer_token, access_token, parameters = {})
|
23
|
+
default_parameters = {
|
24
|
+
:method => :get,
|
25
|
+
:parameters => {},
|
26
|
+
:headers => {}
|
27
|
+
}
|
28
|
+
|
29
|
+
parameters = default_parameters.merge parameters
|
30
|
+
|
31
|
+
method = parameters[:method].downcase
|
32
|
+
path = parameters[:path]
|
33
|
+
http_parameters = parameters[:parameters]
|
34
|
+
http_headers = parameters[:headers]
|
35
|
+
|
36
|
+
if method == :get && !http_parameters.empty?
|
37
|
+
query_string = http_parameters.to_query_string
|
38
|
+
|
39
|
+
path += "?" if ! path.include?("?")
|
40
|
+
path += "&" if ! path.end_with? "?"
|
41
|
+
|
42
|
+
path += query_string
|
43
|
+
end
|
44
|
+
|
45
|
+
consumer = create_consumer(consumer_token.key, consumer_token.secret)
|
46
|
+
|
47
|
+
_access_token = OAuth::AccessToken.new(consumer, *access_token.to_a)
|
48
|
+
if [:post, :put].include?(method)
|
49
|
+
consumer.create_signed_request(method, path, _access_token, http_parameters, http_headers)
|
50
|
+
else
|
51
|
+
consumer.create_signed_request(method, path, _access_token, http_headers)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def oauth_http_request!(consumer_token, access_token, parameters = {})
|
56
|
+
request = create_signed_request(consumer_token, access_token, parameters)
|
57
|
+
Net::HTTP.new("wbsapi.withings.net").request(request)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Simple wrapper class that encapsulates the results of a call to {StaticHelpers#create_request_token}
|
62
|
+
class RequestTokenResponse
|
63
|
+
def initialize(oauth_request_token)
|
64
|
+
self.oauth_request_token = oauth_request_token
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [String] the OAuth request token key
|
68
|
+
def token
|
69
|
+
self.oauth_request_token.token
|
70
|
+
end
|
71
|
+
|
72
|
+
alias :key :token
|
73
|
+
|
74
|
+
# @return [String] the OAuth request token secret
|
75
|
+
def secret
|
76
|
+
self.oauth_request_token.secret
|
77
|
+
end
|
78
|
+
|
79
|
+
# @return [String] URL to redirect the user to to authorize the access to their data
|
80
|
+
def authorization_url
|
81
|
+
self.oauth_request_token.authorize_url
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [RequestToken]
|
85
|
+
def request_token
|
86
|
+
RequestToken.new(self.key, self.secret)
|
87
|
+
end
|
88
|
+
|
89
|
+
attr_accessor :oauth_request_token
|
90
|
+
|
91
|
+
# :nodoc:
|
92
|
+
def oauth_consumer
|
93
|
+
self.oauth_request_token.consumer
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class AccessTokenResponse
|
98
|
+
def initialize(oauth_access_token)
|
99
|
+
@oauth_access_token = oauth_access_token
|
100
|
+
end
|
101
|
+
|
102
|
+
def token
|
103
|
+
@oauth_access_token.token
|
104
|
+
end
|
105
|
+
|
106
|
+
alias :key :token
|
107
|
+
|
108
|
+
def secret
|
109
|
+
@oauth_access_token.secret
|
110
|
+
end
|
111
|
+
|
112
|
+
def user_id
|
113
|
+
@oauth_access_token.params["userid"]
|
114
|
+
end
|
115
|
+
|
116
|
+
def access_token
|
117
|
+
AccessToken.new(self.key, self.secret)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
class Hash
|
4
|
+
QUERY_STRING_RESERVERED = /[\$&\+,\/:;=\?@ <>"#%\{\}\|\\\^~\[\]`]/
|
5
|
+
|
6
|
+
def to_query_string
|
7
|
+
hash = self
|
8
|
+
|
9
|
+
params = []
|
10
|
+
hash.keys.each do |key|
|
11
|
+
params << [key, hash[key]]
|
12
|
+
end
|
13
|
+
|
14
|
+
params.map { |p| p.map { |v| URI.escape(v.to_s, QUERY_STRING_RESERVERED) }.join("=") }.join("&")
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Withings::Api
|
4
|
+
# Class encapsulating a Measurement
|
5
|
+
#
|
6
|
+
# See www.withings.com/en/api/wbsapiv2
|
7
|
+
class Measurement
|
8
|
+
include ResultsHelpers
|
9
|
+
|
10
|
+
attr_accessor :measurement_type, :value_raw, :unit
|
11
|
+
|
12
|
+
def initialize(json_or_hash)
|
13
|
+
hash = coerce_hash json_or_hash
|
14
|
+
|
15
|
+
self.measurement_type = MeasurementType.lookup(hash["type"])
|
16
|
+
self.value_raw = hash["value"]
|
17
|
+
self.unit = hash["unit"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def value
|
21
|
+
value_raw * 10**unit
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
# Class encapsulating a MeasurementGroup
|
27
|
+
#
|
28
|
+
# See www.withings.com/en/api/wbsapiv2
|
29
|
+
class MeasurementGroup
|
30
|
+
include ResultsHelpers
|
31
|
+
|
32
|
+
attr_reader :id, :attribution, :date_raw, :category, :measurements;
|
33
|
+
|
34
|
+
def initialize(json_string_or_hash)
|
35
|
+
hash = coerce_hash json_string_or_hash
|
36
|
+
|
37
|
+
#"grpid"=>2909, "attrib"=>0, "date"=>1222930968, "category"=>1, "measures
|
38
|
+
@id = hash["grpid"]
|
39
|
+
@date_raw = hash["date"]
|
40
|
+
@attribution = AttributionType.lookup(hash["attrib"])
|
41
|
+
@category = CategoryType.lookup(hash["category"])
|
42
|
+
@measurements = hash["measures"].map { |h| Measurement.new(h) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def date
|
46
|
+
Time.at(date_raw)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Class encapsulating the response to a call to
|
51
|
+
# measure/getmeas
|
52
|
+
#
|
53
|
+
# See www.withings.com/en/api/wbsapiv2
|
54
|
+
class MeasureGetmeasResults
|
55
|
+
include ResultsHelpers
|
56
|
+
|
57
|
+
attr_accessor :update_time_raw, :more, :measure_groups
|
58
|
+
alias :more? :more
|
59
|
+
|
60
|
+
def initialize(json_or_hash)
|
61
|
+
hash = coerce_hash json_or_hash
|
62
|
+
|
63
|
+
self.update_time_raw = hash["updatetime"] || raise(ArgumentError)
|
64
|
+
self.more = (hash["more"] == true)
|
65
|
+
self.measure_groups = hash["measuregrps"].map { |h| MeasurementGroup.new(h) }
|
66
|
+
end
|
67
|
+
|
68
|
+
def update_time
|
69
|
+
Time.at(update_time_raw)
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|