timetree 0.3.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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