timetree 0.3.2 → 1.0.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +5 -0
- data/README.md +46 -17
- data/lib/timetree/base_client.rb +52 -0
- data/lib/timetree/calendar_app/access_token.rb +25 -0
- data/lib/timetree/calendar_app/client.rb +228 -0
- data/lib/timetree/configuration.rb +10 -3
- data/lib/timetree/http_command.rb +2 -2
- data/lib/timetree/models/activity.rb +11 -1
- data/lib/timetree/models/application.rb +15 -0
- data/lib/timetree/models/base_model.rb +25 -12
- data/lib/timetree/models/calendar.rb +38 -4
- data/lib/timetree/models/event.rb +27 -3
- data/lib/timetree/oauth_app/client.rb +256 -0
- data/lib/timetree/version.rb +1 -1
- data/timetree.gemspec +1 -0
- metadata +22 -4
- data/lib/timetree/client.rb +0 -298
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '08f576804646a18cf1c980d7649f3cf621f7e98871d291bec881ba8232e69e6a'
|
4
|
+
data.tar.gz: 3740f58f68f0f6514b59295a7c3e8f57321fc10b4b5ca3fd6515da7b67a48c50
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5cd3358940be41c9e441ab348249b65628cb489df329687df949ff3f4a48a2749642f31f0cde56170d895acf35f63bd90c30c00d3491efb74ec8323eb501bd29
|
7
|
+
data.tar.gz: 8888d348d25e8c67645a50084443c79cba8a6d14c7aa7fbb43ae743abd2be8092b3a5ea5815edd35c115268c60bac1933438177c31f0e5ae0b27821e3b3d02b3
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -25,60 +25,85 @@ Or install it yourself as:
|
|
25
25
|
|
26
26
|
$ gem install timetree
|
27
27
|
|
28
|
-
## Usage
|
28
|
+
## Usage for Calendar App
|
29
29
|
|
30
|
-
The APIs client needs
|
31
|
-
Set a `token` variable to the value you got by above:
|
30
|
+
The APIs client for Calendar App needs installation_id, application_id and private key.
|
32
31
|
|
33
32
|
```ruby
|
34
33
|
# set token by TimeTree.configure methods.
|
35
34
|
TimeTree.configure do |config|
|
36
|
-
config.
|
35
|
+
config.calendar_app_application_id = '<YOUR_APPLICATION_ID>'
|
36
|
+
config.calendar_app_private_key = File.read('<YOUR_PATH_TO_PEM>')
|
37
37
|
end
|
38
|
-
client = TimeTree::Client.new
|
38
|
+
client = TimeTree::CalendarApp::Client.new('<INSTALLATION_ID>')
|
39
39
|
|
40
|
-
# set token by TimeTree::Client initializer.
|
41
|
-
client = TimeTree::Client.new('<
|
40
|
+
# set token by TimeTree::CalendarApp::Client initializer.
|
41
|
+
client = TimeTree::CalendarApp::Client.new('<INSTALLATION_ID>', '<YOUR_APPLICATION_ID>', '<YOUR_PRIVATE_KEY_CONTENT>')
|
42
|
+
|
43
|
+
# get connected calendar's information.
|
44
|
+
cal = client.calendar
|
45
|
+
# => #<TimeTree::Calendar id:xxx_cal001>
|
46
|
+
|
47
|
+
# get upcoming events on the calendar.
|
48
|
+
evs = cal.upcoming_events
|
49
|
+
# => [#<TimeTree::Event id:xxx_ev001>, #<TimeTree::Event id:xxx_ev002>, ...]
|
50
|
+
ev = evs.first.title
|
51
|
+
# => "Event Title"
|
52
|
+
```
|
53
|
+
|
54
|
+
## Usage for OAuth App
|
55
|
+
|
56
|
+
The APIs client for OAuth App needs access token.
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
# set token by TimeTree.configure methods.
|
60
|
+
TimeTree.configure do |config|
|
61
|
+
config.oauth_app_token = '<YOUR_ACCESS_TOKEN>'
|
62
|
+
end
|
63
|
+
client = TimeTree::OAuthApp::Client.new
|
64
|
+
|
65
|
+
# set token by TimeTree::OAuthApp::Client initializer.
|
66
|
+
client = TimeTree::OAuthApp::Client.new('<YOUR_ACCESS_TOKEN>')
|
42
67
|
|
43
68
|
# get a current user's information.
|
44
69
|
user = client.current_user
|
45
|
-
=> #<TimeTree::User id:xxx_u001>
|
70
|
+
# => #<TimeTree::User id:xxx_u001>
|
46
71
|
user.name
|
47
|
-
=> "USER Name"
|
72
|
+
# => "USER Name"
|
48
73
|
|
49
74
|
# get current user's calendars.
|
50
75
|
cals = client.calendars
|
51
|
-
=> [#<TimeTree::Calendar id:xxx_cal001>, #<TimeTree::Calendar id:xxx_cal002>, ...]
|
76
|
+
# => [#<TimeTree::Calendar id:xxx_cal001>, #<TimeTree::Calendar id:xxx_cal002>, ...]
|
52
77
|
cal = cals.first
|
53
78
|
cal.name
|
54
|
-
=> "Calendar Name"
|
79
|
+
# => "Calendar Name"
|
55
80
|
|
56
81
|
# get upcoming events on the calendar.
|
57
82
|
evs = cal.upcoming_events
|
58
|
-
=> [#<TimeTree::Event id:xxx_ev001>, #<TimeTree::Event id:xxx_ev002>, ...]
|
83
|
+
# => [#<TimeTree::Event id:xxx_ev001>, #<TimeTree::Event id:xxx_ev002>, ...]
|
59
84
|
ev = evs.first
|
60
85
|
ev.title
|
61
|
-
=> "Event Title"
|
86
|
+
# => "Event Title"
|
62
87
|
|
63
88
|
# updates an event.
|
64
89
|
ev.title += ' Updated'
|
65
90
|
ev.start_at = Time.parse('2020-06-20 09:00 +09:00')
|
66
91
|
ev.end_at = Time.parse('2020-06-20 10:00 +09:00')
|
67
92
|
ev.update
|
68
|
-
=> #<TimeTree::Event id:xxx_ev001>
|
93
|
+
# => #<TimeTree::Event id:xxx_ev001>
|
69
94
|
|
70
95
|
# creates an event.
|
71
96
|
copy_ev = ev.dup
|
72
97
|
new_ev = copy_ev.create
|
73
|
-
=> #<TimeTree::Event id:xxx_new_ev001>
|
98
|
+
# => #<TimeTree::Event id:xxx_new_ev001>
|
74
99
|
|
75
100
|
# deletes an event.
|
76
101
|
ev.delete
|
77
|
-
=> true
|
102
|
+
# => true
|
78
103
|
|
79
104
|
# creates a comment to an event.
|
80
105
|
ev.create_comment 'Hi there!'
|
81
|
-
=> #<TimeTree::Activity id:xxx_act001>
|
106
|
+
# => #<TimeTree::Activity id:xxx_act001>
|
82
107
|
|
83
108
|
# handles APIs error.
|
84
109
|
begin
|
@@ -90,7 +115,11 @@ rescue TimeTree::ApiError => e
|
|
90
115
|
e.response
|
91
116
|
=> #<Faraday::Response>
|
92
117
|
end
|
118
|
+
```
|
93
119
|
|
120
|
+
## Logging
|
121
|
+
|
122
|
+
```ruby
|
94
123
|
# if the log level set :debug, you can get the request/response information.
|
95
124
|
TimeTree.configuration.logger.level = :debug
|
96
125
|
=> #<TimeTree::Event id:event_id_001_not_found>
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TimeTree
|
4
|
+
class BaseClient
|
5
|
+
API_HOST = 'https://timetreeapis.com'
|
6
|
+
# @return [Integer]
|
7
|
+
attr_reader :ratelimit_limit
|
8
|
+
# @return [Integer]
|
9
|
+
attr_reader :ratelimit_remaining
|
10
|
+
# @return [Time]
|
11
|
+
attr_reader :ratelimit_reset_at
|
12
|
+
|
13
|
+
#
|
14
|
+
# update ratelimit properties
|
15
|
+
#
|
16
|
+
# @param res [Faraday::Response]
|
17
|
+
# apis http response.
|
18
|
+
def update_ratelimit(res)
|
19
|
+
limit = res.headers['x-ratelimit-limit']
|
20
|
+
remaining = res.headers['x-ratelimit-remaining']
|
21
|
+
reset = res.headers['x-ratelimit-reset']
|
22
|
+
@ratelimit_limit = limit.to_i if limit
|
23
|
+
@ratelimit_remaining = remaining.to_i if remaining
|
24
|
+
@ratelimit_reset_at = Time.at reset.to_i if reset
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def check_event_id(value)
|
30
|
+
check_required_property(value, 'event_id')
|
31
|
+
end
|
32
|
+
|
33
|
+
def check_required_property(value, name)
|
34
|
+
err = Error.new "#{name} is required."
|
35
|
+
raise err if value.nil?
|
36
|
+
raise err if value.to_s.empty?
|
37
|
+
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_model(data, included: nil)
|
42
|
+
TimeTree::BaseModel.to_model data, client: self, included: included
|
43
|
+
end
|
44
|
+
|
45
|
+
def relationships_params(relationships, default)
|
46
|
+
params = {}
|
47
|
+
relationships ||= default
|
48
|
+
params[:include] = relationships.join ',' if relationships.is_a? Array
|
49
|
+
params
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TimeTree
|
4
|
+
module CalendarApp
|
5
|
+
class AccessToken
|
6
|
+
# @return [String]
|
7
|
+
attr_reader :token
|
8
|
+
# @return [Integer]
|
9
|
+
attr_reader :expire_at
|
10
|
+
|
11
|
+
def initialize(token, expire_at)
|
12
|
+
@token = token
|
13
|
+
@expire_at = expire_at
|
14
|
+
end
|
15
|
+
|
16
|
+
#
|
17
|
+
# Returns the access token is expired or not.
|
18
|
+
#
|
19
|
+
# @return [Boolean]
|
20
|
+
def expired?
|
21
|
+
Time.now.to_i > expire_at
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'jwt'
|
5
|
+
|
6
|
+
module TimeTree
|
7
|
+
module CalendarApp
|
8
|
+
# TimeTree API CalendarApp client.
|
9
|
+
class Client < BaseClient
|
10
|
+
# @return [Integer]
|
11
|
+
attr_reader :installation_id
|
12
|
+
# @return [String]
|
13
|
+
attr_reader :application_id
|
14
|
+
# @return [String]
|
15
|
+
attr_reader :private_key
|
16
|
+
# @return [String]
|
17
|
+
attr_reader :token
|
18
|
+
|
19
|
+
# @param installation_id [Integer] CalendarApp's installation id
|
20
|
+
# @param application_id [String] CalendarApp id
|
21
|
+
# @param private_key [String] RSA private key for CalendarApp
|
22
|
+
def initialize(installation_id, application_id = nil, private_key = nil)
|
23
|
+
@installation_id = installation_id
|
24
|
+
@application_id = application_id || TimeTree.configuration.calendar_app_application_id
|
25
|
+
@private_key = OpenSSL::PKey::RSA.new((private_key || TimeTree.configuration.calendar_app_private_key).to_s)
|
26
|
+
check_client_requirement
|
27
|
+
@http_cmd = HttpCommand.new(API_HOST, self)
|
28
|
+
rescue OpenSSL::PKey::RSAError
|
29
|
+
raise Error.new 'private_key must be RSA private key.'
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Get a calendar information related to CalendarApp
|
34
|
+
#
|
35
|
+
# @param include_relationships [Array<symbol>]
|
36
|
+
# includes association's object in the response.
|
37
|
+
# @return [TimeTree::Calendar]
|
38
|
+
# @raise [TimeTree::ApiError] if the http response status will not success.
|
39
|
+
# @since 1.0.0
|
40
|
+
def calendar(include_relationships: nil)
|
41
|
+
check_access_token
|
42
|
+
params = relationships_params(include_relationships, Calendar::RELATIONSHIPS)
|
43
|
+
res = http_cmd.get('/calendar', params)
|
44
|
+
raise ApiError.new(res) if res.status != 200
|
45
|
+
|
46
|
+
to_model(res.body[:data], included: res.body[:included])
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Get a calendar's member information.
|
51
|
+
#
|
52
|
+
# @return [Array<TimeTree::User>]
|
53
|
+
# @raise [TimeTree::ApiError] if the http response status will not success.
|
54
|
+
# @since 1.0.0
|
55
|
+
def calendar_members
|
56
|
+
check_access_token
|
57
|
+
res = http_cmd.get('/calendar/members')
|
58
|
+
raise ApiError.new(res) if res.status != 200
|
59
|
+
|
60
|
+
res.body[:data].map { |item| to_model(item) }
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# Get an event's information.
|
65
|
+
#
|
66
|
+
# @param event_id [String] event's id.
|
67
|
+
# @param include_relationships [Array<symbol>]
|
68
|
+
# includes association's object in the response.
|
69
|
+
# @return [TimeTree::Event]
|
70
|
+
# @raise [TimeTree::Error] if the event_id arg is empty.
|
71
|
+
# @raise [TimeTree::ApiError] if the http response status will not success.
|
72
|
+
# @since 1.0.0
|
73
|
+
def event(event_id, include_relationships: nil)
|
74
|
+
check_event_id event_id
|
75
|
+
check_access_token
|
76
|
+
params = relationships_params(include_relationships, Event::RELATIONSHIPS)
|
77
|
+
res = http_cmd.get("/calendar/events/#{event_id}", params)
|
78
|
+
raise ApiError.new(res) if res.status != 200
|
79
|
+
|
80
|
+
to_model(res.body[:data], included: res.body[:included])
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Get events' information after a request date.
|
85
|
+
#
|
86
|
+
# @param days [Integer] The number of days to get.
|
87
|
+
# @param timezone [String] Timezone.
|
88
|
+
# @param include_relationships [Array<symbol>]
|
89
|
+
# includes association's object in the response.
|
90
|
+
# @return [Array<TimeTree::Event>]
|
91
|
+
# @raise [TimeTree::ApiError] if the http response status will not success.
|
92
|
+
# @since 1.0.0
|
93
|
+
def upcoming_events(days: 7, timezone: 'UTC', include_relationships: nil)
|
94
|
+
check_access_token
|
95
|
+
params = relationships_params(include_relationships, Event::RELATIONSHIPS)
|
96
|
+
params.merge!(days: days, timezone: timezone)
|
97
|
+
res = http_cmd.get('/calendar/upcoming_events', params)
|
98
|
+
raise ApiError.new(res) if res.status != 200
|
99
|
+
|
100
|
+
included = res.body[:included]
|
101
|
+
res.body[:data].map { |item| to_model(item, included: included) }
|
102
|
+
end
|
103
|
+
|
104
|
+
#
|
105
|
+
# Creates an event.
|
106
|
+
#
|
107
|
+
# @param params [Hash] TimeTree request body format.
|
108
|
+
# @return [TimeTree::Event]
|
109
|
+
# @raise [TimeTree::Error] if the cal_id arg is empty.
|
110
|
+
# @raise [TimeTree::ApiError] if the http response status will not success.
|
111
|
+
# @since 1.0.0
|
112
|
+
def create_event(params)
|
113
|
+
check_access_token
|
114
|
+
res = http_cmd.post('/calendar/events', params)
|
115
|
+
raise ApiError.new(res) if res.status != 201
|
116
|
+
|
117
|
+
to_model(res.body[:data])
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Updates an event.
|
122
|
+
#
|
123
|
+
# @param event_id [String] event's id.
|
124
|
+
# @param params [Hash]
|
125
|
+
# event's information specified in TimeTree request body format.
|
126
|
+
# @return [TimeTree::Event]
|
127
|
+
# @raise [TimeTree::Error] if the event_id arg is empty.
|
128
|
+
# @raise [TimeTree::ApiError] if the http response status will not success.
|
129
|
+
# @since 1.0.0
|
130
|
+
def update_event(event_id, params)
|
131
|
+
check_event_id event_id
|
132
|
+
check_access_token
|
133
|
+
res = http_cmd.put("/calendar/events/#{event_id}", params)
|
134
|
+
raise ApiError.new(res) if res.status != 200
|
135
|
+
|
136
|
+
to_model(res.body[:data])
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Deletes an event.
|
141
|
+
#
|
142
|
+
# @param event_id [String] event's id.
|
143
|
+
# @return [true] if the operation succeeded.
|
144
|
+
# @raise [TimeTree::Error] if the event_id arg is empty.
|
145
|
+
# @raise [TimeTree::ApiError] if the http response status will not success.
|
146
|
+
# @since 1.0.0
|
147
|
+
def delete_event(event_id)
|
148
|
+
check_event_id event_id
|
149
|
+
check_access_token
|
150
|
+
res = http_cmd.delete("/calendar/events/#{event_id}")
|
151
|
+
raise ApiError.new(res) if res.status != 204
|
152
|
+
|
153
|
+
true
|
154
|
+
end
|
155
|
+
|
156
|
+
#
|
157
|
+
# Creates a comment.
|
158
|
+
#
|
159
|
+
# @param event_id [String] event's id.
|
160
|
+
# @param params [Hash]
|
161
|
+
# comment's information specified in TimeTree request body format.
|
162
|
+
# @return [TimeTree::Activity]
|
163
|
+
# @raise [TimeTree::Error] if the event_id arg is empty.
|
164
|
+
# @raise [TimeTree::ApiError] if the http response status is not success.
|
165
|
+
# @since 1.0.0
|
166
|
+
def create_activity(event_id, params)
|
167
|
+
check_event_id event_id
|
168
|
+
check_access_token
|
169
|
+
res = http_cmd.post("/calendar/events/#{event_id}/activities", params)
|
170
|
+
raise ApiError.new(res) if res.status != 201
|
171
|
+
|
172
|
+
activity = to_model(res.body[:data])
|
173
|
+
activity.event_id = event_id
|
174
|
+
activity
|
175
|
+
end
|
176
|
+
|
177
|
+
def inspect
|
178
|
+
limit_info = nil
|
179
|
+
if defined?(@ratelimit_limit) && @ratelimit_limit
|
180
|
+
limit_info = " ratelimit:#{ratelimit_remaining}/#{ratelimit_limit}"
|
181
|
+
end
|
182
|
+
if defined?(@ratelimit_reset_at) && @ratelimit_reset_at
|
183
|
+
limit_info = "#{limit_info}, reset_at:#{ratelimit_reset_at.strftime('%m/%d %R')}"
|
184
|
+
end
|
185
|
+
"\#<#{self.class}:#{object_id}#{limit_info}>"
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
attr_reader :http_cmd, :access_token
|
191
|
+
|
192
|
+
def check_client_requirement
|
193
|
+
check_required_property(installation_id, 'installation_id')
|
194
|
+
check_required_property(application_id, 'application_id')
|
195
|
+
end
|
196
|
+
|
197
|
+
def check_access_token
|
198
|
+
return if access_token?
|
199
|
+
|
200
|
+
get_access_token
|
201
|
+
end
|
202
|
+
|
203
|
+
def access_token?
|
204
|
+
access_token && !access_token.expired?
|
205
|
+
end
|
206
|
+
|
207
|
+
def get_access_token
|
208
|
+
res = http_cmd.post("/installations/#{installation_id}/access_tokens") do |req|
|
209
|
+
req.headers['Authorization'] = "Bearer #{jwt}"
|
210
|
+
end
|
211
|
+
raise ApiError.new(res) if res.status != 200
|
212
|
+
|
213
|
+
@access_token = AccessToken.new(res.body[:access_token], res.body[:expire_at])
|
214
|
+
@token = access_token.token
|
215
|
+
end
|
216
|
+
|
217
|
+
def jwt
|
218
|
+
now = Time.now.to_i
|
219
|
+
payload = {
|
220
|
+
iat: now,
|
221
|
+
exp: now + (10 * 60), # JWT expires in 10 minutes
|
222
|
+
iss: application_id
|
223
|
+
}
|
224
|
+
JWT.encode(payload, private_key, 'RS256')
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|