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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b5824494b234019afe3260508db4d161c874771a65b041f005cda5e3d75ed3e
4
- data.tar.gz: ef00a0a5c959255e10550c606a463fa9c4914aa95cd6f1dd864de5bbe990f697
3
+ metadata.gz: '08f576804646a18cf1c980d7649f3cf621f7e98871d291bec881ba8232e69e6a'
4
+ data.tar.gz: 3740f58f68f0f6514b59295a7c3e8f57321fc10b4b5ca3fd6515da7b67a48c50
5
5
  SHA512:
6
- metadata.gz: aedb1c5f57d57426aad408626ee6b47adffeb1db0114a36679b5ef2958acd95308e7fd631dc93a2eb4fcf054ec7bed9eaeb1add8725086402a29439674ee15f2
7
- data.tar.gz: 8587334f92c73d0247e19f44543af05d9a2bfdd7ecca7125f4d0f828ed53e51ec8bf5d9cc1b0b7f0816d4a70c0de7484614473a411141a54469d1229fb962e0d
6
+ metadata.gz: 5cd3358940be41c9e441ab348249b65628cb489df329687df949ff3f4a48a2749642f31f0cde56170d895acf35f63bd90c30c00d3491efb74ec8323eb501bd29
7
+ data.tar.gz: 8888d348d25e8c67645a50084443c79cba8a6d14c7aa7fbb43ae743abd2be8092b3a5ea5815edd35c115268c60bac1933438177c31f0e5ae0b27821e3b3d02b3
@@ -1,4 +1,5 @@
1
1
  AllCops:
2
+ NewCops: enable
2
3
  TargetRubyVersion: 2.6
3
4
 
4
5
  Layout/AccessModifierIndentation:
@@ -1,5 +1,10 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.0.0
4
+
5
+ - add Calendar App Client. (refs #28)
6
+ - rename `TimeTree::OAuthApp::Client` from `TimeTree::Client`.
7
+
3
8
  ## 0.3.2
4
9
 
5
10
  - remove zeitwerk dependency. (refs #29)
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 access token.
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.token = '<YOUR_ACCESS_TOKEN>'
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('<YOUR_ACCESS_TOKEN>')
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