help_scout 0.12.1 → 1.0.0.beta1

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
- 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