pingpp 2.0.15 → 2.1.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.
data/lib/pingpp.rb CHANGED
@@ -1,11 +1,13 @@
1
1
  # Pingpp Ruby bindings
2
- # API spec at https://pingxx.com/document/api
2
+ # API spec at https://www.pingxx.com/api
3
3
  require 'cgi'
4
4
  require 'set'
5
5
  require 'openssl'
6
6
  require 'rest-client'
7
7
  require 'json'
8
8
  require 'base64'
9
+ require 'socket'
10
+ require 'rbconfig'
9
11
 
10
12
  # Version
11
13
  require 'pingpp/version'
@@ -15,6 +17,7 @@ require 'pingpp/api_operations/create'
15
17
  require 'pingpp/api_operations/update'
16
18
  require 'pingpp/api_operations/delete'
17
19
  require 'pingpp/api_operations/list'
20
+ require 'pingpp/api_operations/request'
18
21
 
19
22
  # Resources
20
23
  require 'pingpp/util'
@@ -29,6 +32,9 @@ require 'pingpp/event'
29
32
  require 'pingpp/transfer'
30
33
  require 'pingpp/webhook'
31
34
  require 'pingpp/identification'
35
+ require 'pingpp/customs'
36
+ require 'pingpp/batch_refund'
37
+ require 'pingpp/batch_transfer'
32
38
 
33
39
  # Errors
34
40
  require 'pingpp/errors/pingpp_error'
@@ -37,27 +43,45 @@ require 'pingpp/errors/api_connection_error'
37
43
  require 'pingpp/errors/invalid_request_error'
38
44
  require 'pingpp/errors/authentication_error'
39
45
  require 'pingpp/errors/channel_error'
46
+ require 'pingpp/errors/rate_limit_error'
40
47
 
41
48
  # WxPubOauth
42
49
  require 'pingpp/wx_pub_oauth'
43
50
 
44
51
  module Pingpp
45
52
  DEFAULT_CA_BUNDLE_PATH = File.dirname(__FILE__) + '/data/ca-certificates.crt'
46
- @api_base = 'https://api.pingxx.com'
47
53
 
48
- @api_version = '2015-10-10'
54
+ RETRY_EXCEPTIONS = [
55
+ Errno::ECONNREFUSED,
56
+ Errno::ECONNRESET,
57
+ Errno::ETIMEDOUT,
58
+ RestClient::Conflict,
59
+ RestClient::RequestTimeout,
60
+ ].freeze
61
+
62
+ @api_base = 'https://api.pingxx.com'
49
63
 
50
- @ssl_bundle_path = DEFAULT_CA_BUNDLE_PATH
64
+ @ca_bundle_path = DEFAULT_CA_BUNDLE_PATH
51
65
  @verify_ssl_certs = true
52
66
 
67
+ @open_timeout = 30
68
+ @timeout = 80
69
+
70
+ @max_network_retries = 0
71
+ @max_network_retry_delay = 2
72
+ @initial_network_retry_delay = 0.5
73
+
53
74
  HEADERS_TO_PARSE = [:pingpp_one_version, :pingpp_sdk_version]
54
75
 
55
76
  class << self
56
- attr_accessor :api_key, :api_base, :verify_ssl_certs, :api_version, :parsed_headers, :private_key, :pub_key
77
+ attr_accessor :api_key, :api_base, :verify_ssl_certs, :api_version,
78
+ :parsed_headers, :private_key, :pub_key, :app_id, :timeout,
79
+ :open_timeout
80
+ attr_reader :max_network_retry_delay, :initial_network_retry_delay
57
81
  end
58
82
 
59
- def self.api_url(url='')
60
- @api_base + url
83
+ def self.api_url(url='', api_base_url=nil)
84
+ (api_base_url || @api_base) + url
61
85
  end
62
86
 
63
87
  def self.parse_headers(headers)
@@ -77,7 +101,9 @@ module Pingpp
77
101
  end
78
102
  end
79
103
 
80
- def self.request(method, url, api_key, params={}, headers={})
104
+ def self.request(method, url, api_key, params={}, headers={}, api_base_url=nil)
105
+ api_base_url = api_base_url || @api_base
106
+
81
107
  unless api_key ||= @api_key
82
108
  raise AuthenticationError.new('No API key provided. ' +
83
109
  'Set your API key using "Pingpp.api_key = <API-KEY>". ' +
@@ -93,7 +119,7 @@ module Pingpp
93
119
 
94
120
  if verify_ssl_certs
95
121
  request_opts = {:verify_ssl => OpenSSL::SSL::VERIFY_PEER,
96
- :ssl_ca_file => @ssl_bundle_path,
122
+ :ssl_ca_file => @ca_bundle_path,
97
123
  :ssl_version => 'TLSv1'}
98
124
  else
99
125
  request_opts = {:verify_ssl => false,
@@ -107,46 +133,35 @@ module Pingpp
107
133
  end
108
134
 
109
135
  params = Util.objects_to_ids(params)
110
- url = api_url(url)
136
+ url = api_url(url, api_base_url)
111
137
 
112
- case method.to_s.downcase.to_sym
138
+ method_sym = method.to_s.downcase.to_sym
139
+ case method_sym
113
140
  when :get, :head, :delete
114
141
  # Make params into GET parameters
115
- url += "#{URI.parse(url).query ? '&' : '?'}#{uri_encode(params)}" if params && params.any?
142
+ url += "#{URI.parse(url).query ? '&' : '?'}#{Util.encode_parameters(params)}" if params && params.any?
116
143
  payload = nil
117
144
  else
118
145
  payload = JSON.generate(params)
119
146
  end
120
147
 
121
- request_opts.update(:headers => request_headers(api_key, method.to_s.downcase.to_sym, payload).update(headers),
122
- :method => method, :open_timeout => 30,
123
- :payload => payload, :url => url, :timeout => 80)
148
+ request_opts.update(:headers => request_headers(api_key, method_sym, payload, url).update(headers),
149
+ :method => method, :open_timeout => open_timeout,
150
+ :payload => payload, :url => url, :timeout => timeout)
124
151
 
125
- begin
126
- response = execute_request(request_opts)
127
- rescue SocketError => e
128
- handle_restclient_error(e)
129
- rescue NoMethodError => e
130
- # Work around RestClient bug
131
- if e.message =~ /\WRequestFailed\W/
132
- e = APIConnectionError.new('Unexpected HTTP response code')
133
- handle_restclient_error(e)
134
- else
135
- raise
136
- end
137
- rescue RestClient::ExceptionWithResponse => e
138
- if rcode = e.http_code and rbody = e.http_body
139
- handle_api_error(rcode, rbody)
140
- else
141
- handle_restclient_error(e)
142
- end
143
- rescue RestClient::Exception, Errno::ECONNREFUSED => e
144
- handle_restclient_error(e)
145
- end
152
+ response = execute_request_with_rescues(request_opts, api_base_url)
146
153
 
147
154
  [parse(response), api_key]
148
155
  end
149
156
 
157
+ def self.max_network_retries
158
+ @max_network_retries
159
+ end
160
+
161
+ def self.max_network_retries=(val)
162
+ @max_network_retries = val.to_i
163
+ end
164
+
150
165
  private
151
166
 
152
167
  def self.user_agent
@@ -159,23 +174,75 @@ module Pingpp
159
174
  :lang_version => lang_version,
160
175
  :platform => RUBY_PLATFORM,
161
176
  :publisher => 'pingpp',
162
- :uname => @uname
177
+ :uname => @uname,
178
+ :hostname => Socket.gethostname,
179
+ :engine => defined?(RUBY_ENGINE) ? RUBY_ENGINE : '',
180
+ :openssl_version => OpenSSL::OPENSSL_VERSION
163
181
  }
164
182
 
165
183
  end
166
184
 
167
185
  def self.get_uname
168
- `uname -a 2>/dev/null`.strip if RUBY_PLATFORM =~ /linux|darwin/i
169
- rescue Errno::ENOMEM => ex # couldn't create subprocess
186
+ if File.exist?('/proc/version')
187
+ File.read('/proc/version').strip
188
+ else
189
+ case RbConfig::CONFIG['host_os']
190
+ when /linux|darwin|bsd|sunos|solaris|cygwin/i
191
+ _uname_uname
192
+ when /mswin|mingw/i
193
+ _uname_ver
194
+ else
195
+ "unknown platform"
196
+ end
197
+ end
198
+ end
199
+
200
+ def self._uname_uname
201
+ (`uname -a 2>/dev/null` || '').strip
202
+ rescue Errno::ENOMEM
170
203
  "uname lookup failed"
171
204
  end
172
205
 
173
- def self.uri_encode(params)
174
- Util.flatten_params(params).
175
- map { |k,v| "#{k}=#{Util.url_encode(v)}" }.join('&')
206
+ def self._uname_ver
207
+ (`ver` || '').strip
208
+ rescue Errno::ENOMEM
209
+ "uname lookup failed"
176
210
  end
177
211
 
178
- def self.request_headers(api_key, method_sym, data=nil)
212
+ def self.execute_request_with_rescues(request_opts, api_base_url, retry_count = 0)
213
+ begin
214
+ response = execute_request(request_opts)
215
+
216
+ rescue => e
217
+ if should_retry?(e, retry_count)
218
+ retry_count = retry_count + 1
219
+ sleep sleep_time(retry_count)
220
+ retry
221
+ end
222
+
223
+ case e
224
+ when SocketError
225
+ response = handle_restclient_error(e, request_opts, retry_count, api_base_url)
226
+
227
+ when RestClient::ExceptionWithResponse
228
+ if e.response
229
+ handle_api_error(e.response)
230
+ else
231
+ response = handle_restclient_error(e, request_opts, retry_count, api_base_url)
232
+ end
233
+
234
+ when RestClient::Exception, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError
235
+ response = handle_restclient_error(e, request_opts, retry_count, api_base_url)
236
+
237
+ else
238
+ raise
239
+ end
240
+ end
241
+
242
+ response
243
+ end
244
+
245
+ def self.request_headers(api_key, method_sym, data, url)
179
246
  post_or_put = (method_sym == :post or method_sym == :put)
180
247
  headers = {
181
248
  :user_agent => "Pingpp/v1 RubyBindings/#{Pingpp::VERSION}",
@@ -193,8 +260,17 @@ module Pingpp
193
260
  :error => "#{e} (#{e.class})")
194
261
  end
195
262
 
196
- if post_or_put && private_key && data
197
- signature = sign_request(data, private_key)
263
+ data_to_be_signed = data || ''
264
+ uri = URI.parse(url)
265
+ data_to_be_signed += uri.path
266
+ (!uri.query.nil?) && data_to_be_signed += '?' + uri.query
267
+
268
+ request_time = Time.now.to_i.to_s
269
+ headers.update(:pingplusplus_request_timestamp => request_time)
270
+ data_to_be_signed += request_time
271
+
272
+ if private_key
273
+ signature = sign_request(data_to_be_signed, private_key)
198
274
  headers.update(:pingplusplus_signature => signature)
199
275
  end
200
276
 
@@ -235,63 +311,81 @@ module Pingpp
235
311
  "(HTTP response code was #{rcode})", rcode, rbody)
236
312
  end
237
313
 
238
- def self.handle_api_error(rcode, rbody)
314
+ def self.handle_api_error(resp)
239
315
  begin
240
- error_obj = JSON.parse(rbody)
316
+ error_obj = JSON.parse(resp.body)
241
317
  error_obj = Util.symbolize_names(error_obj)
242
- error = error_obj[:error] or raise PingppError.new # escape from parsing
318
+ error = error_obj[:error]
319
+ raise PingppError.new unless error && error.is_a?(Hash)
243
320
 
244
321
  rescue JSON::ParserError, PingppError
245
- raise general_api_error(rcode, rbody)
322
+ raise general_api_error(resp.code, resp.body)
246
323
  end
247
324
 
248
- case rcode
325
+ case resp.code
249
326
  when 400, 404
250
- raise invalid_request_error error, rcode, rbody, error_obj
327
+ raise invalid_request_error(error, resp, error_obj)
251
328
  when 401
252
- raise authentication_error error, rcode, rbody, error_obj
329
+ raise authentication_error(error, resp, error_obj)
253
330
  when 402
254
- raise channel_error error, rcode, rbody, error_obj
331
+ raise channel_error(error, resp, error_obj)
332
+ when 403
333
+ raise rate_limit_error(error, resp, error_obj)
255
334
  else
256
- raise api_error error, rcode, rbody, error_obj
335
+ raise api_error(error, resp, error_obj)
257
336
  end
258
337
 
259
338
  end
260
339
 
261
- def self.invalid_request_error(error, rcode, rbody, error_obj)
262
- InvalidRequestError.new(error[:message], error[:param], rcode,
263
- rbody, error_obj)
340
+ def self.invalid_request_error(error, resp, error_obj)
341
+ InvalidRequestError.new(error[:message], error[:param], resp.code,
342
+ resp.body, error_obj, resp.headers)
343
+ end
344
+
345
+ def self.authentication_error(error, resp, error_obj)
346
+ AuthenticationError.new(error[:message], resp.code, resp.body, error_obj,
347
+ resp.headers)
264
348
  end
265
349
 
266
- def self.authentication_error(error, rcode, rbody, error_obj)
267
- AuthenticationError.new(error[:message], rcode, rbody, error_obj)
350
+ def self.channel_error(error, resp, error_obj)
351
+ ChannelError.new(error[:message], error[:code], error[:param], resp.code,
352
+ resp.body, error_obj, resp.headers)
268
353
  end
269
354
 
270
- def self.channel_error(error, rcode, rbody, error_obj)
271
- ChannelError.new(error[:message], error[:code], error[:param], rcode, rbody, error_obj)
355
+ def self.rate_limit_error(error, resp, error_obj)
356
+ RateLimitError.new(error[:message], resp.code, resp.body, error_obj,
357
+ resp.headers)
272
358
  end
273
359
 
274
- def self.api_error(error, rcode, rbody, error_obj)
275
- APIError.new(error[:message], rcode, rbody, error_obj)
360
+ def self.api_error(error, resp, error_obj)
361
+ APIError.new(error[:message], resp.code, resp.body, error_obj, resp.headers)
276
362
  end
277
363
 
278
- def self.handle_restclient_error(e)
364
+ def self.handle_restclient_error(e, request_opts, retry_count, api_base_url=nil)
365
+ api_base_url = @api_base unless api_base_url
366
+
279
367
  connection_message = "Please check your internet connection and try again. " \
280
368
  "If this problem persists, you should check Pingpp's service status at " \
281
369
  "https://www.pingxx.com/status"
282
370
 
283
371
  case e
284
372
  when RestClient::RequestTimeout
285
- message = "Could not connect to Pingpp (#{@api_base}). #{connection_message}"
373
+ message = "Could not connect to Pingpp (#{api_base_url}). #{connection_message}"
286
374
 
287
375
  when RestClient::ServerBrokeConnection
288
- message = "The connection to the server (#{@api_base}) broke before the " \
376
+ message = "The connection to the server (#{api_base_url}) broke before the " \
289
377
  "request completed. #{connection_message}"
290
378
 
379
+ when OpenSSL::SSL::SSLError
380
+ message = "Could not establish a secure connection to Ping++, you may " \
381
+ "need to upgrade your OpenSSL version. To check, try running " \
382
+ "'openssl s_client -connect api.pingxx.com:443' from the " \
383
+ "command line."
384
+
291
385
  when RestClient::SSLCertificateNotVerified
292
386
  message = "Could not verify Pingpp's SSL certificate. " \
293
387
  "Please make sure that your network is not intercepting certificates. " \
294
- "(Try going to (#{@api_base}) in your browser.)"
388
+ "(Try going to (#{api_base_url}) in your browser.)"
295
389
 
296
390
  when SocketError
297
391
  message = "Unexpected error communicating when trying to connect to Pingpp. " \
@@ -303,6 +397,23 @@ module Pingpp
303
397
 
304
398
  end
305
399
 
400
+ if retry_count > 0
401
+ message += " Request was retried #{retry_count} times."
402
+ end
403
+
306
404
  raise APIConnectionError.new(message + "\n\n(Network error: #{e.message})")
307
405
  end
406
+
407
+ def self.should_retry?(e, retry_count)
408
+ retry_count < self.max_network_retries &&
409
+ RETRY_EXCEPTIONS.any? { |klass| e.is_a?(klass) }
410
+ end
411
+
412
+ def self.sleep_time(retry_count)
413
+ sleep_seconds = [initial_network_retry_delay * (2 ** (retry_count - 1)), max_network_retry_delay].min
414
+ sleep_seconds = sleep_seconds * (0.5 * (1 + rand()))
415
+ sleep_seconds = [initial_network_retry_delay, sleep_seconds].max
416
+
417
+ sleep_seconds
418
+ end
308
419
  end
metadata CHANGED
@@ -1,36 +1,22 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pingpp
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.15
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xufeng Weng
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-08-18 00:00:00.000000000 Z
11
+ date: 2016-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rest-client
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.4'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.4'
27
- - !ruby/object:Gem::Dependency
28
- name: mime-types
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - ">="
32
18
  - !ruby/object:Gem::Version
33
- version: '1.25'
19
+ version: '1.4'
34
20
  - - "<"
35
21
  - !ruby/object:Gem::Version
36
22
  version: '4.0'
@@ -40,64 +26,10 @@ dependencies:
40
26
  requirements:
41
27
  - - ">="
42
28
  - !ruby/object:Gem::Version
43
- version: '1.25'
29
+ version: '1.4'
44
30
  - - "<"
45
31
  - !ruby/object:Gem::Version
46
32
  version: '4.0'
47
- - !ruby/object:Gem::Dependency
48
- name: json
49
- requirement: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - "~>"
52
- - !ruby/object:Gem::Version
53
- version: '1.8'
54
- - - ">="
55
- - !ruby/object:Gem::Version
56
- version: 1.8.1
57
- type: :runtime
58
- prerelease: false
59
- version_requirements: !ruby/object:Gem::Requirement
60
- requirements:
61
- - - "~>"
62
- - !ruby/object:Gem::Version
63
- version: '1.8'
64
- - - ">="
65
- - !ruby/object:Gem::Version
66
- version: 1.8.1
67
- - !ruby/object:Gem::Dependency
68
- name: mocha
69
- requirement: !ruby/object:Gem::Requirement
70
- requirements:
71
- - - "~>"
72
- - !ruby/object:Gem::Version
73
- version: 0.13.2
74
- type: :development
75
- prerelease: false
76
- version_requirements: !ruby/object:Gem::Requirement
77
- requirements:
78
- - - "~>"
79
- - !ruby/object:Gem::Version
80
- version: 0.13.2
81
- - !ruby/object:Gem::Dependency
82
- name: shoulda
83
- requirement: !ruby/object:Gem::Requirement
84
- requirements:
85
- - - "~>"
86
- - !ruby/object:Gem::Version
87
- version: '3.4'
88
- - - ">="
89
- - !ruby/object:Gem::Version
90
- version: 3.4.0
91
- type: :development
92
- prerelease: false
93
- version_requirements: !ruby/object:Gem::Requirement
94
- requirements:
95
- - - "~>"
96
- - !ruby/object:Gem::Version
97
- version: '3.4'
98
- - - ">="
99
- - !ruby/object:Gem::Version
100
- version: 3.4.0
101
33
  description: PingPlusPlus is the easiest way to accept payments online. See https://pingxx.com
102
34
  for details.
103
35
  email:
@@ -111,15 +43,20 @@ files:
111
43
  - lib/pingpp/api_operations/create.rb
112
44
  - lib/pingpp/api_operations/delete.rb
113
45
  - lib/pingpp/api_operations/list.rb
46
+ - lib/pingpp/api_operations/request.rb
114
47
  - lib/pingpp/api_operations/update.rb
115
48
  - lib/pingpp/api_resource.rb
49
+ - lib/pingpp/batch_refund.rb
50
+ - lib/pingpp/batch_transfer.rb
116
51
  - lib/pingpp/charge.rb
52
+ - lib/pingpp/customs.rb
117
53
  - lib/pingpp/errors/api_connection_error.rb
118
54
  - lib/pingpp/errors/api_error.rb
119
55
  - lib/pingpp/errors/authentication_error.rb
120
56
  - lib/pingpp/errors/channel_error.rb
121
57
  - lib/pingpp/errors/invalid_request_error.rb
122
58
  - lib/pingpp/errors/pingpp_error.rb
59
+ - lib/pingpp/errors/rate_limit_error.rb
123
60
  - lib/pingpp/event.rb
124
61
  - lib/pingpp/identification.rb
125
62
  - lib/pingpp/list_object.rb
@@ -132,7 +69,7 @@ files:
132
69
  - lib/pingpp/version.rb
133
70
  - lib/pingpp/webhook.rb
134
71
  - lib/pingpp/wx_pub_oauth.rb
135
- homepage: https://pingxx.com/document/api
72
+ homepage: https://www.pingxx.com/api
136
73
  licenses:
137
74
  - MIT
138
75
  metadata: {}