help_scout 0.12.1 → 1.0.0.beta1

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
- SHA256:
3
- metadata.gz: 975c65e3a727e0ff9dde7a883b6128dc48e3c0b1367a2318c05f89cb6c941741
4
- data.tar.gz: a8374deb4ffd828d2c5c025844d663684953e581e7a1c31081b7b4eb6a11e4d7
2
+ SHA1:
3
+ metadata.gz: ab8a0fda60b049cfcabe64b115dd2d35df71184e
4
+ data.tar.gz: 1f42dc562130e722dbf52588f4b0ffe46e732b1c
5
5
  SHA512:
6
- metadata.gz: 6d1a5764d2b872592ca7e81e83e6de8f73b0048e0f58c3464e8077da9a03675550ad4ce679933239ea19dc615cd7d9735949bb2ca797a191063b940150e5b5db
7
- data.tar.gz: af8f594af7cbdd12bc99081141651610131a917415b5a6324a929e67299eb5982c93146082ab3c2379f48c0a4fd34aae1cb1f08010eb49799857325570abc602
6
+ metadata.gz: f5c6c5ab4751da5bfb99b11e788f008d43e64092a5e1a2f3221512ab657cd4fed2bcd70521c1f6357a95c047f93ef87a742f3700f7ff207a9675c6c39ed2f236
7
+ data.tar.gz: 593e12e794ea41ab29075c3be541f5fb5ffa9e65eb293ce37833cf6d0870c57918458a654ee88b97fd5f0417bf25411cbff9b69cd60a11815bfd5951023a8f08
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in help_scout.gemspec
4
4
  gemspec
5
+
6
+ gem 'redis'
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_runtime_dependency "httparty", "~> 0.14"
22
22
 
23
- spec.add_development_dependency "bundler", "~> 1.12"
23
+ spec.add_development_dependency "bundler"
24
24
  spec.add_development_dependency "rake", "~> 10.5"
25
25
  spec.add_development_dependency "rspec", "~> 3.5"
26
26
  spec.add_development_dependency "webmock", "~> 2.1"
@@ -1,4 +1,7 @@
1
1
  require "help_scout/version"
2
+ require "help_scout/token_storage/memory"
3
+ require "help_scout/token_storage/redis"
4
+
2
5
  require "httparty"
3
6
 
4
7
  class HelpScout
@@ -10,188 +13,284 @@ class HelpScout
10
13
  class ForbiddenError < StandardError; end
11
14
  class ServiceUnavailable < StandardError; end
12
15
 
16
+ class InvalidDataError < StandardError; end
17
+
13
18
  # Status codes used by Help Scout, not all are implemented in this gem yet.
14
- # http://developer.helpscout.net/help-desk-api/status-codes/
19
+ # https://developer.helpscout.com/mailbox-api/overview/status_codes/
15
20
  HTTP_OK = 200
16
21
  HTTP_CREATED = 201
17
22
  HTTP_NO_CONTENT = 204
18
23
  HTTP_BAD_REQUEST = 400
24
+ HTTP_UNAUTHORIZED = 401
19
25
  HTTP_FORBIDDEN = 403
20
26
  HTTP_NOT_FOUND = 404
21
27
  HTTP_TOO_MANY_REQUESTS = 429
22
28
  HTTP_INTERNAL_SERVER_ERROR = 500
23
29
  HTTP_SERVICE_UNAVAILABLE = 503
24
30
 
31
+ # https://developer.helpscout.com/mailbox-api/endpoints/conversations/list/
32
+ CONVERSATION_STATUSES = ["active", "closed", "open", "pending", "spam"]
33
+
34
+ # https://developer.helpscout.com/mailbox-api/overview/authentication/#client-credentials-flow
35
+ def generate_oauth_token
36
+ options = {
37
+ headers: {
38
+ "Content-Type": "application/json"
39
+ },
40
+ body: {
41
+ grant_type: "client_credentials",
42
+ client_id: @api_key,
43
+ client_secret: @api_secret,
44
+ }
45
+ }
46
+ options[:body] = options[:body].to_json
47
+ response = HTTParty.post("https://api.helpscout.net/v2/oauth2/token", options)
48
+ JSON.parse(response.body)["access_token"]
49
+ end
50
+
25
51
  attr_accessor :last_response
26
52
 
27
- def initialize(api_key)
53
+ def initialize(api_key, api_secret, token_storage = TokenStorage::Memory.new)
28
54
  @api_key = api_key
55
+ @api_secret = api_secret
56
+ @token_storage = token_storage
29
57
  end
30
58
 
31
59
  # Public: Create conversation
32
60
  #
33
61
  # data - hash with data
62
+ # note: since v2 the status is now required, which had a default of "active" in v1.
34
63
  #
35
- # More info: http://developer.helpscout.net/help-desk-api/conversations/create/
64
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/conversations/create/
36
65
  #
37
66
  # Returns conversation ID
38
67
  def create_conversation(data)
68
+ # required_fields = ["subject", "type", "mailboxId", "status", "customer", "threads"]
69
+
39
70
  post("conversations", { body: data })
40
71
 
41
- # Extract ID of created conversation from the Location header
42
- conversation_uri = last_response.headers["location"]
43
- conversation_uri.match(/(\d+)\.json$/)[1]
72
+ last_response.headers["Resource-ID"]
44
73
  end
45
74
 
46
75
  # Public: Get conversation
47
76
  #
48
77
  # id - conversation ID
78
+ # embed_threads - boolean - This will load in subentities, currently only
79
+ # Threads are supported by HS
49
80
  #
50
- # More info: http://developer.helpscout.net/help-desk-api/objects/conversation/
81
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/conversations/get/
51
82
  #
52
83
  # Returns hash from HS with conversation data
53
- def get_conversation(id)
54
- get("conversations/#{id}")
84
+ def get_conversation(id, embed_threads: false)
85
+ if embed_threads
86
+ get("conversations/#{id}?embed=threads")
87
+ else
88
+ get("conversations/#{id}")
89
+ end
55
90
  end
56
91
 
57
- # Public: Get conversations
58
- #
59
- # mailbox_id - ID of mailbox (find these with get_mailboxes)
60
- # page - integer of page to fetch (default: 1)
61
- # modified_since - Only return conversations that have been modified since
62
- # this UTC datetime (default: nil)
92
+ # Public: Update conversation
63
93
  #
64
- # More info: http://developer.helpscout.net/help-desk-api/conversations/list/
94
+ # id - conversation id
95
+ # data - hash with data
65
96
  #
66
- # Returns hash from HS with conversation data
67
- def get_conversations(mailbox_id, page = 1, modified_since = nil)
68
- options = {
69
- query: {
70
- page: page,
71
- modifiedSince: modified_since,
97
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/conversations/update/
98
+ def update_conversation(id, data)
99
+ instructions = []
100
+
101
+ if data[:subject]
102
+ instructions << {
103
+ op: "replace",
104
+ path: "/subject",
105
+ value: data[:subject],
72
106
  }
73
- }
107
+ end
108
+ if data[:mailboxId]
109
+ instructions << {
110
+ op: "move",
111
+ path: "/mailboxId",
112
+ value: data[:mailboxId],
113
+ }
114
+ end
115
+ if data[:status]
116
+ status = data[:status]
117
+ if !CONVERSATION_STATUSES.include?(status)
118
+ raise InvalidDataError.new("status \"#{status}\" not supported, must be one of #{CONVERSATION_STATUSES}")
119
+ end
74
120
 
75
- get("mailboxes/#{mailbox_id}/conversations", options)
121
+ instructions << {
122
+ op: "replace",
123
+ path: "/status",
124
+ value: data[:status],
125
+ }
126
+ end
127
+ if data.key?(:assignTo)
128
+ # change owner
129
+ if data[:assignTo]
130
+ instructions << {
131
+ op: "replace",
132
+ path: "/assignTo",
133
+ value: data[:assignTo],
134
+ }
135
+ else
136
+ # un assign
137
+ instructions << {
138
+ op: "remove",
139
+ path: "/assignTo",
140
+ }
141
+ end
142
+ end
143
+
144
+ # Note: HelpScout currently does not support multiple
145
+ # instructions in the same request, well have to do them
146
+ # individually :-)
147
+ instructions.each do |instruction|
148
+ patch("conversations/#{id}", { body: instruction })
149
+ end
76
150
  end
77
151
 
78
- # Public: Update conversation
152
+ # Public: Update conversation tags
79
153
  #
80
- # id - conversation id
81
- # data - hash with data
154
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/conversations/tags/update/
155
+ def update_conversation_tags(id, tags)
156
+ data = { tags: tags }
157
+ put("conversations/#{id}/tags", { body: data })
158
+ end
159
+
160
+ # Public: Update conversation custom fields
82
161
  #
83
- # More info: http://developer.helpscout.net/help-desk-api/conversations/update/
84
- def update_conversation(id, data)
85
- put("conversations/#{id}", { body: data })
162
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/conversations/custom_fields/update/
163
+ def update_conversation_custom_fields(id, fields)
164
+ data = { fields: fields }
165
+ put("conversations/#{id}/fields", { body: data })
86
166
  end
87
167
 
88
168
  # Public: Search for conversations
89
169
  #
90
170
  # query - term to search for
91
171
  #
92
- # More info: http://developer.helpscout.net/help-desk-api/search/conversations/
172
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/conversations/list/
93
173
  def search_conversations(query)
94
- search("search/conversations", query)
174
+ search("conversations", query)
95
175
  end
96
176
 
97
177
  # Public: Delete conversation
98
178
  #
99
179
  # id - conversation id
100
180
  #
101
- # More info: https://developer.helpscout.com/help-desk-api/conversations/delete/
181
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/conversations/delete/
102
182
  def delete_conversation(id)
103
183
  delete("conversations/#{id}")
104
184
  end
105
185
 
106
- # Public: Get customer
107
- #
108
- # id - customer id
186
+ # Public: List all mailboxes
109
187
  #
110
- # More info: http://developer.helpscout.net/help-desk-api/customers/get/
111
- def get_customer(id)
112
- get("customers/#{id}")
113
- end
114
-
188
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/mailboxes/list/
115
189
  def get_mailboxes
116
190
  get("mailboxes")
117
191
  end
118
192
 
119
- # Public: Get ratings
193
+ # Public: Create note thread
120
194
  #
121
- # More info: http://developer.helpscout.net/help-desk-api/reports/user/ratings/
122
- # 'rating' parameter required: 0 (for all ratings), 1 (Great), 2 (Okay), 3 (Not Good)
123
- def reports_user_ratings(user_id, rating, start_date, end_date, options)
124
- options = {
125
- user: user_id,
126
- rating: rating,
127
- start: start_date,
128
- end: end_date,
195
+ # imported: no outgoing e-mails or notifications will be generated
196
+ #
197
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/conversations/threads/note/
198
+ def create_note(conversation_id:, text:, user: nil, imported: false)
199
+ data = {
200
+ text: text,
201
+ user: user,
202
+ imported: imported,
129
203
  }
204
+ post("conversations/#{conversation_id}/notes", body: data)
130
205
 
131
- get("reports/user/ratings", options)
206
+ last_response.code == HTTP_CREATED
132
207
  end
133
208
 
134
- # Public: Creates conversation thread
209
+ # Public: Create phone thread
135
210
  #
136
- # conversion_id - conversation id
137
- # thread - thread content to be created
138
- # imported - When set to true no outgoing emails or notifications will be
139
- # generated
140
- # reload - Set to true to get the entire conversation in the result
141
- #
142
- # More info: http://developer.helpscout.net/help-desk-api/conversations/create-thread/
143
- #
144
- # Returns true if created, false otherwise. When used with reload: true it
145
- # will return the entire conversation
146
- def create_thread(conversation_id:, thread:, imported: nil, reload: nil)
147
- query = {}
148
- { reload: reload, imported: imported }.each do |key, value|
149
- query[key] = value unless value.nil?
150
- end
151
-
152
- post("conversations/#{conversation_id}", body: thread, query: query)
211
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/conversations/threads/phone/
212
+ def create_phone(conversation_id:, text:, customer:, imported: false)
213
+ # Note, hs does not list user as an accepted type
214
+ # https://developer.helpscout.com/mailbox-api/endpoints/conversations/threads/phone/
215
+ data = {
216
+ text: text,
217
+ customer: {
218
+ id: customer
219
+ },
220
+ imported: imported,
221
+ }
222
+ post("conversations/#{conversation_id}/phones", body: data)
153
223
 
154
- if reload
155
- last_response.parsed_response
156
- else
157
- last_response.code == HTTP_CREATED
158
- end
224
+ last_response.code == HTTP_CREATED
159
225
  end
160
226
 
161
- # Public: Updates conversation thread
162
- #
163
- # conversion_id - conversation id
164
- # thread - thread content to be updated (only the body can be updated)
165
- # reload - Set to true to get the entire conversation in the result
227
+ # Public: Create reply thread
166
228
  #
167
- # More info: http://developer.helpscout.net/help-desk-api/conversations/update-thread/
168
- #
169
- # Returns true if updated, false otherwise. When used with reload: true it
170
- # will return the entire conversation
171
- def update_thread(conversation_id:, thread:, reload: nil)
172
- query = {}
173
- query[:reload] = reload if reload
174
- body = { body: thread[:body] }
229
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/conversations/threads/reply/
230
+ def create_reply(conversation_id:, text:, customer:, user: nil, imported: false)
231
+ data = {
232
+ text: text,
233
+ user: user,
234
+ customer: {
235
+ id: customer
236
+ },
237
+ imported: imported,
238
+ }
239
+ post("conversations/#{conversation_id}/reply", body: data)
175
240
 
176
- put("conversations/#{conversation_id}/threads/#{thread[:id]}", body: body, query: query)
241
+ last_response.code == HTTP_CREATED
242
+ end
177
243
 
178
- if reload
179
- last_response.parsed_response
180
- else
181
- last_response.code == HTTP_OK
182
- end
244
+ # Public: Get customer
245
+ #
246
+ # id - customer id
247
+ #
248
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/customers/get/
249
+ def get_customer(id)
250
+ get("customers/#{id}")
183
251
  end
184
252
 
185
- # Public: Update Customer
253
+ # Public: Update customer
254
+ #
255
+ # Note: to update address, chat handles, emails, phones, social profiles or
256
+ # websites, separate endpoints have to be used.
186
257
  #
187
258
  # id - customer id
188
259
  # data - hash with data
189
260
  #
190
- # More info: http://developer.helpscout.net/help-desk-api/customers/update/
261
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/customers/update/
191
262
  def update_customer(id, data)
192
263
  put("customers/#{id}", { body: data })
193
264
  end
194
265
 
266
+ # Public: Create phone number
267
+ #
268
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/customers/phones/create/
269
+ def create_customer_phone(customer_id, data)
270
+ post("customers/#{customer_id}/phones", { body: data })
271
+ end
272
+
273
+ # Public: Delete phone number
274
+ #
275
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/customers/phones/delete/
276
+ def delete_customer_phone(customer_id, phone_id)
277
+ delete("customers/#{customer_id}/phones/#{phone_id}")
278
+ end
279
+
280
+ # Public: Create email
281
+ #
282
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/customers/emails/create/
283
+ def create_customer_email(customer_id, data)
284
+ post("customers/#{customer_id}/emails", { body: data })
285
+ end
286
+
287
+ # Public: Delete email
288
+ #
289
+ # More info: https://developer.helpscout.com/mailbox-api/endpoints/customers/emails/delete/
290
+ def delete_customer_email(customer_id, email_id)
291
+ delete("customers/#{customer_id}/emails/#{email_id}")
292
+ end
293
+
195
294
  protected
196
295
 
197
296
  def post(path, options = {})
@@ -214,43 +313,59 @@ class HelpScout
214
313
  request(:delete, path, options)
215
314
  end
216
315
 
316
+ def patch(path, options = {})
317
+ options[:body] = options[:body].to_json if options[:body]
318
+
319
+ request(:patch, path, options)
320
+ end
321
+
217
322
  def search(path, query, page_id = 1, items = [])
218
323
  options = { query: { page: page_id, query: "(#{query})" } }
219
324
 
220
325
  result = get(path, options)
221
- if !result.empty?
222
- next_page_id = page_id + 1
223
- result["items"] += items
224
- if next_page_id > result["pages"]
225
- return result["items"]
226
- else
227
- search(path, query, next_page_id, result["items"])
228
- end
326
+ next_page_id = page_id + 1
327
+
328
+ if result.key?("_embedded")
329
+ items += result["_embedded"]["conversations"]
330
+ end
331
+
332
+ if next_page_id > result["page"]["totalPages"]
333
+ return items
334
+ else
335
+ search(path, query, next_page_id, items)
229
336
  end
230
337
  end
231
338
 
232
339
  def request(method, path, options)
233
- uri = URI("https://api.helpscout.net/v1/#{path}.json")
340
+ uri = URI("https://api.helpscout.net/v2/#{path}")
341
+
342
+ token = @token_storage.token
343
+ if token.nil?
344
+ token = generate_oauth_token
345
+ @token_storage.store_token(token)
346
+ end
234
347
 
235
- # The password can be anything, it's not used, see:
236
- # http://developer.helpscout.net/help-desk-api/
237
348
  options = {
238
- basic_auth: {
239
- username: @api_key, password: 'X'
240
- },
241
- headers: {}
349
+ headers: {
350
+ "Content-Type": "application/json",
351
+ "Authorization": "Bearer #{token}",
352
+ }
242
353
  }.merge(options)
243
354
 
244
- if options.key?(:body)
245
- options[:headers]['Content-Type'] ||= 'application/json'
246
- end
247
-
248
355
  @last_response = HTTParty.send(method, uri, options)
356
+
249
357
  case last_response.code
358
+ when HTTP_UNAUTHORIZED
359
+ # Unauthorized means our token is expired. We will fetch a new one, and
360
+ # retry the original request
361
+ new_token = generate_oauth_token
362
+ @token_storage.store_token(new_token)
363
+ options.delete(:headers)
364
+ request(method, path, options)
250
365
  when HTTP_OK, HTTP_CREATED, HTTP_NO_CONTENT
251
366
  last_response.parsed_response
252
367
  when HTTP_BAD_REQUEST
253
- raise ValidationError, last_response.parsed_response["validationErrors"]
368
+ raise ValidationError, JSON.parse(last_response.body)["_embedded"]["errors"]
254
369
  when HTTP_FORBIDDEN
255
370
  raise ForbiddenError
256
371
  when HTTP_NOT_FOUND
@@ -261,7 +376,7 @@ class HelpScout
261
376
  when HTTP_SERVICE_UNAVAILABLE
262
377
  raise ServiceUnavailable
263
378
  when HTTP_TOO_MANY_REQUESTS
264
- retry_after = last_response.headers["Retry-After"]
379
+ retry_after = last_response.headers["X-RateLimit-Retry-After"]
265
380
  message = "Rate limit of 200 RPM or 12 POST/PUT/DELETE requests per 5 " +
266
381
  "seconds reached. Next request possible in #{retry_after} seconds."
267
382
  raise TooManyRequestsError, message
@@ -0,0 +1,4 @@
1
+ class HelpScout
2
+ module TokenStorage
3
+ end
4
+ end
@@ -0,0 +1,13 @@
1
+ class HelpScout
2
+ module TokenStorage
3
+ class Memory
4
+ def token
5
+ @token
6
+ end
7
+
8
+ def store_token(token)
9
+ @token = token
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ class HelpScout
2
+ module TokenStorage
3
+ class Redis
4
+ IDENTIFIER = "helpscout-client-token"
5
+
6
+ def initialize(db)
7
+ @db = db
8
+ end
9
+
10
+ def token
11
+ @db.get(IDENTIFIER)
12
+ end
13
+
14
+ def store_token(new_token)
15
+ @db.set(IDENTIFIER, new_token)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,3 +1,3 @@
1
1
  class HelpScout
2
- VERSION = "0.12.1"
2
+ VERSION = "1.0.0.beta1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: help_scout
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 1.0.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dennis Paagman
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: exe
13
13
  cert_chain: []
14
- date: 2018-06-21 00:00:00.000000000 Z
14
+ date: 2019-08-15 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: httparty
@@ -31,16 +31,16 @@ dependencies:
31
31
  name: bundler
32
32
  requirement: !ruby/object:Gem::Requirement
33
33
  requirements:
34
- - - "~>"
34
+ - - ">="
35
35
  - !ruby/object:Gem::Version
36
- version: '1.12'
36
+ version: '0'
37
37
  type: :development
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
40
40
  requirements:
41
- - - "~>"
41
+ - - ">="
42
42
  - !ruby/object:Gem::Version
43
- version: '1.12'
43
+ version: '0'
44
44
  - !ruby/object:Gem::Dependency
45
45
  name: rake
46
46
  requirement: !ruby/object:Gem::Requirement
@@ -118,6 +118,9 @@ files:
118
118
  - bin/setup
119
119
  - help_scout.gemspec
120
120
  - lib/help_scout.rb
121
+ - lib/help_scout/token_storage.rb
122
+ - lib/help_scout/token_storage/memory.rb
123
+ - lib/help_scout/token_storage/redis.rb
121
124
  - lib/help_scout/version.rb
122
125
  homepage: https://github.com/Springest/help_scout
123
126
  licenses:
@@ -134,12 +137,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
134
137
  version: '0'
135
138
  required_rubygems_version: !ruby/object:Gem::Requirement
136
139
  requirements:
137
- - - ">="
140
+ - - ">"
138
141
  - !ruby/object:Gem::Version
139
- version: '0'
142
+ version: 1.3.1
140
143
  requirements: []
141
144
  rubyforge_project:
142
- rubygems_version: 2.7.4
145
+ rubygems_version: 2.6.14.3
143
146
  signing_key:
144
147
  specification_version: 4
145
148
  summary: HelpScout is an API client for Help Scout