ably-rest 1.0.6 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -14
  3. data/lib/submodules/ably-ruby/.editorconfig +14 -0
  4. data/lib/submodules/ably-ruby/.travis.yml +4 -4
  5. data/lib/submodules/ably-ruby/CHANGELOG.md +43 -2
  6. data/lib/submodules/ably-ruby/README.md +3 -2
  7. data/lib/submodules/ably-ruby/Rakefile +32 -0
  8. data/lib/submodules/ably-ruby/SPEC.md +1277 -835
  9. data/lib/submodules/ably-ruby/ably.gemspec +9 -4
  10. data/lib/submodules/ably-ruby/lib/ably/auth.rb +30 -4
  11. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +8 -2
  12. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +1 -1
  13. data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +1 -1
  14. data/lib/submodules/ably-ruby/lib/ably/models/device_details.rb +87 -0
  15. data/lib/submodules/ably-ruby/lib/ably/models/device_push_details.rb +86 -0
  16. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +23 -2
  17. data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -4
  18. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +32 -2
  19. data/lib/submodules/ably-ruby/lib/ably/models/push_channel_subscription.rb +89 -0
  20. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +1 -1
  21. data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +1 -1
  22. data/lib/submodules/ably-ruby/lib/ably/modules/exception_codes.rb +128 -0
  23. data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +15 -2
  24. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +1 -1
  25. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +1 -0
  26. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +1 -1
  27. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +24 -102
  28. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +2 -6
  29. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/publisher.rb +74 -0
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/push_channel.rb +62 -0
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +87 -0
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +6 -2
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  35. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +8 -5
  36. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +7 -7
  37. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +1 -1
  38. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +4 -4
  39. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +3 -3
  40. data/lib/submodules/ably-ruby/lib/ably/realtime/push.rb +40 -0
  41. data/lib/submodules/ably-ruby/lib/ably/realtime/push/admin.rb +61 -0
  42. data/lib/submodules/ably-ruby/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  43. data/lib/submodules/ably-ruby/lib/ably/realtime/push/device_registrations.rb +105 -0
  44. data/lib/submodules/ably-ruby/lib/ably/rest.rb +1 -0
  45. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +33 -5
  46. data/lib/submodules/ably-ruby/lib/ably/rest/channel/push_channel.rb +62 -0
  47. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +138 -28
  48. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
  49. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -0
  50. data/lib/submodules/ably-ruby/lib/ably/rest/push.rb +42 -0
  51. data/lib/submodules/ably-ruby/lib/ably/rest/push/admin.rb +54 -0
  52. data/lib/submodules/ably-ruby/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  53. data/lib/submodules/ably-ruby/lib/ably/rest/push/device_registrations.rb +103 -0
  54. data/lib/submodules/ably-ruby/lib/ably/version.rb +7 -2
  55. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +233 -8
  56. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +166 -51
  57. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +149 -0
  58. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +1 -1
  59. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +4 -4
  60. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +19 -17
  61. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +5 -5
  62. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_admin_spec.rb +696 -0
  63. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_spec.rb +27 -0
  64. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +41 -3
  65. data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +2 -2
  66. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +129 -10
  67. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +175 -4
  68. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_admin_spec.rb +896 -0
  69. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_spec.rb +25 -0
  70. data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +1 -1
  71. data/lib/submodules/ably-ruby/spec/run_parallel_tests +33 -0
  72. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +10 -3
  73. data/lib/submodules/ably-ruby/spec/unit/models/device_details_spec.rb +102 -0
  74. data/lib/submodules/ably-ruby/spec/unit/models/device_push_details_spec.rb +101 -0
  75. data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +51 -3
  76. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +17 -2
  77. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +1 -1
  78. data/lib/submodules/ably-ruby/spec/unit/models/push_channel_subscription_spec.rb +86 -0
  79. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +12 -0
  80. data/lib/submodules/ably-ruby/spec/unit/realtime/push_channel_spec.rb +36 -0
  81. data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +8 -1
  82. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +30 -0
  83. data/lib/submodules/ably-ruby/spec/unit/rest/push_channel_spec.rb +36 -0
  84. metadata +29 -4
@@ -0,0 +1,62 @@
1
+ module Ably::Rest
2
+ class Channel
3
+ # A push channel used for push notifications
4
+ # Each PushChannel maps to exactly one Rest Channel
5
+ #
6
+ # @!attribute [r] channel
7
+ # @return [Ably::Rest::Channel] Underlying channel object
8
+ #
9
+ class PushChannel
10
+ attr_reader :channel
11
+
12
+ def initialize(channel)
13
+ raise ArgumentError, "Unsupported channel type '#{channel.class}'" unless channel.kind_of?(Ably::Rest::Channel)
14
+ @channel = channel
15
+ end
16
+
17
+ def to_s
18
+ "<PushChannel: name=#{channel.name}>"
19
+ end
20
+
21
+ # Subscribe local device for push notifications on this channel
22
+ #
23
+ # @note This is unsupported in the Ruby library
24
+ def subscribe_device(*args)
25
+ raise_unsupported
26
+ end
27
+
28
+ # Subscribe all devices registered to this client's authenticated client_id for push notifications on this channel
29
+ #
30
+ # @note This is unsupported in the Ruby library
31
+ def subscribe_client_id(*args)
32
+ raise_unsupported
33
+ end
34
+
35
+ # Unsubscribe local device for push notifications on this channel
36
+ #
37
+ # @note This is unsupported in the Ruby library
38
+ def unsubscribe_device(*args)
39
+ raise_unsupported
40
+ end
41
+
42
+ # Unsubscribe all devices registered to this client's authenticated client_id for push notifications on this channel
43
+ #
44
+ # @note This is unsupported in the Ruby library
45
+ def unsubscribe_client_id(*args)
46
+ raise_unsupported
47
+ end
48
+
49
+ # Get list of subscriptions on this channel for this device or authenticate client_id
50
+ #
51
+ # @note This is unsupported in the Ruby library
52
+ def get_subscriptions(*args)
53
+ raise_unsupported
54
+ end
55
+
56
+ private
57
+ def raise_unsupported
58
+ raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. All PushChannel methods are unavailable'
59
+ end
60
+ end
61
+ end
62
+ end
@@ -30,6 +30,8 @@ module Ably
30
30
  max_retry_count: 3
31
31
  }.freeze
32
32
 
33
+ FALLBACK_RETRY_TIMEOUT = 10 * 60
34
+
33
35
  def_delegators :auth, :client_id, :auth_options
34
36
 
35
37
  # Custom environment to use such as 'sandbox' when testing the client library against an alternate Ably environment
@@ -83,10 +85,23 @@ module Ably
83
85
  # if empty or nil then fallback host functionality is disabled
84
86
  attr_reader :fallback_hosts
85
87
 
86
- # Whethere the {Client} has to add a random identifier to the path of a request
88
+ # Whether the {Client} has to add a random identifier to the path of a request
87
89
  # @return [Boolean]
88
90
  attr_reader :add_request_ids
89
91
 
92
+ # Retries are logged by default to warn and error. When true, retries are logged at info level
93
+ # @return [Boolean]
94
+ # @api private
95
+ attr_reader :log_retries_as_info
96
+
97
+ # True when idempotent publishing is enabled for all messages published via REST.
98
+ # When this feature is enabled, the client library will add a unique ID to every published message (without an ID)
99
+ # ensuring any failed published attempts (due to failures such as HTTP requests failing mid-flight) that are
100
+ # automatically retried will not result in duplicate messages being published to the Ably platform.
101
+ # Note: This is a beta unsupported feature!
102
+ # @return [Boolean]
103
+ attr_reader :idempotent_rest_publishing
104
+
90
105
  # Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
91
106
  #
92
107
  # @param [Hash,String] options an options Hash used to configure the client and the authentication, or String with an API key or Token ID
@@ -117,6 +132,10 @@ module Ably
117
132
  #
118
133
  # @option options [Boolean] :fallback_hosts_use_default (false) When true, forces the user of fallback hosts even if a non-default production endpoint is being used
119
134
  # @option options [Array<String>] :fallback_hosts When an array of fallback hosts are provided, these fallback hosts are always used if a request fails to the primary endpoint. If an empty array is provided, the fallback host functionality is disabled
135
+ # @option options [Integer] :fallback_retry_timeout (600 seconds) amount of time in seconds a REST client will continue to use a working fallback host when the primary fallback host has previously failed
136
+ #
137
+ # @option options [Boolean] :add_request_ids (false) When true, adds a unique request_id to each request sent to Ably servers. This is handy when reporting issues, because you can refer to a specific request.
138
+ # @option options [Boolean] :idempotent_rest_publishing (false if ver < 1.2) When true, idempotent publishing is enabled for all messages published via REST
120
139
  #
121
140
  # @return [Ably::Rest::Client]
122
141
  #
@@ -139,18 +158,21 @@ module Ably
139
158
  end
140
159
  end
141
160
 
142
- @realtime_client = options.delete(:realtime_client)
143
- @tls = options.delete(:tls) == false ? false : true
144
- @environment = options.delete(:environment) # nil is production
145
- @environment = nil if [:production, 'production'].include?(@environment)
146
- @protocol = options.delete(:protocol) || :msgpack
147
- @debug_http = options.delete(:debug_http)
148
- @log_level = options.delete(:log_level) || ::Logger::WARN
149
- @custom_logger = options.delete(:logger)
150
- @custom_host = options.delete(:rest_host)
151
- @custom_port = options.delete(:port)
152
- @custom_tls_port = options.delete(:tls_port)
153
- @add_request_ids = options.delete(:add_request_ids)
161
+ @realtime_client = options.delete(:realtime_client)
162
+ @tls = options.delete(:tls) == false ? false : true
163
+ @environment = options.delete(:environment) # nil is production
164
+ @environment = nil if [:production, 'production'].include?(@environment)
165
+ @protocol = options.delete(:protocol) || :msgpack
166
+ @debug_http = options.delete(:debug_http)
167
+ @log_level = options.delete(:log_level) || ::Logger::WARN
168
+ @custom_logger = options.delete(:logger)
169
+ @custom_host = options.delete(:rest_host)
170
+ @custom_port = options.delete(:port)
171
+ @custom_tls_port = options.delete(:tls_port)
172
+ @add_request_ids = options.delete(:add_request_ids)
173
+ @log_retries_as_info = options.delete(:log_retries_as_info)
174
+ @idempotent_rest_publishing = options.delete(:idempotent_rest_publishing) || Ably.major_minor_version_numeric > 1.1
175
+
154
176
 
155
177
  if options[:fallback_hosts_use_default] && options[:fallback_jhosts]
156
178
  raise ArgumentError, "fallback_hosts_use_default cannot be set to trye when fallback_jhosts is also provided"
@@ -166,6 +188,10 @@ module Ably
166
188
  Ably::FALLBACK_HOSTS
167
189
  end
168
190
 
191
+ options[:fallback_retry_timeout] ||= FALLBACK_RETRY_TIMEOUT
192
+
193
+ # Take option keys prefixed with `http_`, remove the http_ and
194
+ # check if the option exists in HTTP_DEFAULTS. If so, update http_defaults
169
195
  @http_defaults = HTTP_DEFAULTS.dup
170
196
  options.each do |key, val|
171
197
  if http_key = key[/^http_(.+)/, 1]
@@ -190,8 +216,12 @@ module Ably
190
216
  raise ArgumentError, 'Protocol is invalid. Must be either :msgpack or :json' unless [:msgpack, :json].include?(@protocol)
191
217
 
192
218
  token_params = options.delete(:default_token_params) || {}
193
- @options = options
194
- @auth = Auth.new(self, token_params, options)
219
+ @options = options
220
+ init_auth_options = options.select do |key, _|
221
+ Auth::AUTH_OPTIONS_KEYS.include?(key.to_s)
222
+ end
223
+
224
+ @auth = Auth.new(self, token_params, init_auth_options)
195
225
  @channels = Ably::Rest::Channels.new(self)
196
226
  @encoders = []
197
227
 
@@ -273,6 +303,24 @@ module Ably
273
303
  raw_request(:post, path, params, options)
274
304
  end
275
305
 
306
+ # Perform an HTTP PUT request to the API using configured authentication
307
+ #
308
+ # @return [Faraday::Response]
309
+ #
310
+ # @api private
311
+ def put(path, params, options = {})
312
+ raw_request(:put, path, params, options)
313
+ end
314
+
315
+ # Perform an HTTP DELETE request to the API using configured authentication
316
+ #
317
+ # @return [Faraday::Response]
318
+ #
319
+ # @api private
320
+ def delete(path, params, options = {})
321
+ raw_request(:delete, path, params, options)
322
+ end
323
+
276
324
  # Perform an HTTP request to the Ably API
277
325
  # This is a convenience for customers who wish to use bleeding edge REST API functionality
278
326
  # that is either not documented or is not included in the API for our client libraries.
@@ -292,14 +340,14 @@ module Ably
292
340
 
293
341
  response = case method.to_sym
294
342
  when :get
295
- reauthorize_on_authorisation_failure do
343
+ reauthorize_on_authorization_failure do
296
344
  send_request(method, path, params, headers: headers)
297
345
  end
298
346
  when :post
299
347
  path_with_params = Addressable::URI.new
300
348
  path_with_params.query_values = params || {}
301
349
  query = path_with_params.query
302
- reauthorize_on_authorisation_failure do
350
+ reauthorize_on_authorization_failure do
303
351
  send_request(method, "#{path}#{"?#{query}" unless query.nil? || query.empty?}", body, headers: headers)
304
352
  end
305
353
  end
@@ -321,6 +369,20 @@ module Ably
321
369
  Models::HttpPaginatedResponse.new(response, path, self)
322
370
  end
323
371
 
372
+ # The local device detilas
373
+ # @return [Ably::Models::LocalDevice]
374
+ #
375
+ # @note This is unsupported in the Ruby library
376
+ def device
377
+ raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. The local device object is not unavailable'
378
+ end
379
+
380
+ # Push notification object for publishing and managing push notifications
381
+ # @return [Ably::Rest::Push]
382
+ def push
383
+ @push ||= Push.new(self)
384
+ end
385
+
324
386
  # @!attribute [r] endpoint
325
387
  # @return [URI::Generic] Default Ably REST endpoint used for all requests
326
388
  def endpoint
@@ -389,7 +451,6 @@ module Ably
389
451
  def fallback_connection
390
452
  unless defined?(@fallback_connections) && @fallback_connections
391
453
  @fallback_connections = fallback_hosts.shuffle.map { |host| Faraday.new(endpoint_for_host(host).to_s, connection_options) }
392
- @fallback_connections << Faraday.new(endpoint.to_s, connection_options) # Try the original host last if all fallbacks have been used
393
454
  end
394
455
  @fallback_index ||= 0
395
456
 
@@ -420,13 +481,46 @@ module Ably
420
481
  end
421
482
  end
422
483
 
484
+ # If the primary host endpoint fails, and a subsequent fallback host succeeds, the fallback
485
+ # host that succeeded is used for +ClientOption+ +fallback_retry_timeout+ seconds to avoid
486
+ # retries to known failing hosts for a short period of time.
487
+ # See https://github.com/ably/docs/pull/554, spec id #RSC15f
488
+ #
489
+ # @return [nil, String] Returns nil (falsey) if the primary host is being used, or the currently used host if a fallback host is currently preferred
490
+ def using_preferred_fallback_host?
491
+ if preferred_fallback_connection && (preferred_fallback_connection.fetch(:expires_at) > Time.now)
492
+ preferred_fallback_connection.fetch(:connection_object).host
493
+ end
494
+ end
495
+
423
496
  private
497
+
498
+ attr_reader :preferred_fallback_connection
499
+
500
+ # See #using_preferred_fallback_host? for context
501
+ def set_preferred_fallback_connection(connection)
502
+ @preferred_fallback_connection = if connection == @connection
503
+ # If the succeeded connection is in fact the primary connection (tried after a failed fallback)
504
+ # then clear the preferred fallback connection
505
+ nil
506
+ else
507
+ {
508
+ expires_at: Time.now + options.fetch(:fallback_retry_timeout),
509
+ connection_object: connection,
510
+ }
511
+ end
512
+ end
513
+
514
+ def get_preferred_fallback_connection_object
515
+ preferred_fallback_connection.fetch(:connection_object) if using_preferred_fallback_host?
516
+ end
517
+
424
518
  def raw_request(method, path, params = {}, options = {})
425
519
  options = options.clone
426
520
  if options.delete(:disable_automatic_reauthorize) == true
427
521
  send_request(method, path, params, options)
428
522
  else
429
- reauthorize_on_authorisation_failure do
523
+ reauthorize_on_authorization_failure do
430
524
  send_request(method, path, params, options)
431
525
  end
432
526
  end
@@ -442,10 +536,22 @@ module Ably
442
536
  retry_sequence_id = nil
443
537
  request_id = SecureRandom.urlsafe_base64(10) if add_request_ids
444
538
 
539
+ preferred_fallback_connection_for_first_request = get_preferred_fallback_connection_object
540
+
445
541
  begin
446
- use_fallback = can_fallback_to_alternate_ably_host? && retry_count > 0
542
+ use_fallback = can_fallback_to_alternate_ably_host? && (retry_count > 0)
543
+
544
+ conn = if preferred_fallback_connection_for_first_request
545
+ case retry_count
546
+ when 0
547
+ preferred_fallback_connection_for_first_request
548
+ when 1
549
+ # Ensure the root host is used first if the preferred fallback fails, see #RSC15f
550
+ connection(use_fallback: false)
551
+ end
552
+ end || connection(use_fallback: use_fallback) # default to normal connection selection process if not preferred connection set
447
553
 
448
- connection(use_fallback: use_fallback).send(method, path, params) do |request|
554
+ conn.send(method, path, params) do |request|
449
555
  if add_request_ids
450
556
  request.params[:request_id] = request_id
451
557
  request.options.context = {} if request.options.context.nil?
@@ -461,10 +567,12 @@ module Ably
461
567
  end
462
568
  end.tap do
463
569
  if retry_count > 0
464
- logger.warn do
570
+ retry_log_severity = log_retries_as_info ? :info : :warn
571
+ logger.public_send(retry_log_severity) do
465
572
  "Ably::Rest::Client - Request SUCCEEDED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \
466
573
  " #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)"
467
574
  end
575
+ set_preferred_fallback_connection conn
468
576
  end
469
577
  end
470
578
 
@@ -472,30 +580,32 @@ module Ably
472
580
  retry_sequence_id ||= SecureRandom.urlsafe_base64(4)
473
581
  time_passed = Time.now - requested_at
474
582
 
475
- if can_fallback_to_alternate_ably_host? && retry_count < max_retry_count && time_passed <= max_retry_duration
583
+ if can_fallback_to_alternate_ably_host? && (retry_count < max_retry_count) && (time_passed <= max_retry_duration)
476
584
  retry_count += 1
477
- logger.warn { "Ably::Rest::Client - Retry #{retry_count} for #{method} #{path} #{params} as initial attempt failed (seq ##{retry_sequence_id}): #{error}" }
585
+ retry_log_severity = log_retries_as_info ? :info : :warn
586
+ logger.public_send(retry_log_severity) { "Ably::Rest::Client - Retry #{retry_count} for #{method} #{path} #{params} as initial attempt failed (seq ##{retry_sequence_id}): #{error}" }
478
587
  retry
479
588
  end
480
589
 
481
- logger.error do
590
+ retry_log_severity = log_retries_as_info ? :info : :error
591
+ logger.public_send(retry_log_severity) do
482
592
  "Ably::Rest::Client - Request FAILED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \
483
593
  " #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)"
484
594
  end
485
595
 
486
596
  case error
487
597
  when Faraday::TimeoutError
488
- raise Ably::Exceptions::ConnectionTimeout.new(error.message, nil, 80014, error, { request_id: request_id })
598
+ raise Ably::Exceptions::ConnectionTimeout.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_TIMED_OUT, error, { request_id: request_id })
489
599
  when Faraday::ClientError
490
600
  # request_id is also available in the request context
491
- raise Ably::Exceptions::ConnectionError.new(error.message, nil, 80000, error, { request_id: request_id })
601
+ raise Ably::Exceptions::ConnectionError.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_FAILED, error, { request_id: request_id })
492
602
  else
493
603
  raise error
494
604
  end
495
605
  end
496
606
  end
497
607
 
498
- def reauthorize_on_authorisation_failure
608
+ def reauthorize_on_authorization_failure
499
609
  yield
500
610
  rescue Ably::Exceptions::TokenExpired => e
501
611
  if auth.token_renewable?
@@ -10,6 +10,14 @@ module Ably
10
10
  env.body = parse(env.body) unless env.response_headers['Ably-Middleware-Parsed'] == true
11
11
  env.response_headers['Ably-Middleware-Parsed'] = true
12
12
  end
13
+ rescue Ably::Exceptions::InvalidResponseBody => e
14
+ debug_info = {
15
+ method: env.method,
16
+ url: env.url,
17
+ base64_body: base64_body(env.body),
18
+ response_headers: env.response_headers
19
+ }
20
+ raise Ably::Exceptions::InvalidResponseBody, "#{e.message}\nRequest env: #{debug_info}"
13
21
  end
14
22
 
15
23
  def parse(body)
@@ -18,8 +26,16 @@ module Ably
18
26
  else
19
27
  body
20
28
  end
29
+ rescue MessagePack::UnknownExtTypeError => e
30
+ raise Ably::Exceptions::InvalidResponseBody, "MessagePack::UnknownExtTypeError body could not be decoded: #{e.message}. Got Base64:\n#{base64_body(body)}"
21
31
  rescue MessagePack::MalformedFormatError => e
22
- raise Ably::Exceptions::InvalidResponseBody, "Expected MessagePack response: #{e.message}"
32
+ raise Ably::Exceptions::InvalidResponseBody, "MessagePack::MalformedFormatError body could not be decoded: #{e.message}. Got Base64:\n#{base64_body(body)}"
33
+ end
34
+
35
+ def base64_body(body)
36
+ Base64.encode64(body)
37
+ rescue => err
38
+ "[#{err.message}! Could not base64 encode body: '#{body}']"
23
39
  end
24
40
  end
25
41
  end
@@ -5,6 +5,7 @@ module Ably
5
5
 
6
6
  # {Ably::Rest::Client} for this Presence object
7
7
  # @return {Ably::Rest::Client}
8
+ # @private
8
9
  attr_reader :client
9
10
 
10
11
  # {Ably::Rest::Channel} this Presence object is associated with
@@ -0,0 +1,42 @@
1
+ require 'ably/rest/push/admin'
2
+
3
+ module Ably
4
+ module Rest
5
+ # Class providing push notification functionality
6
+ class Push
7
+ include Ably::Modules::Conversions
8
+
9
+ # @private
10
+ attr_reader :client
11
+
12
+ def initialize(client)
13
+ @client = client
14
+ end
15
+
16
+ # Admin features for push notifications like managing devices and channel subscriptions
17
+ # @return [Ably::Rest::Push::Admin]
18
+ def admin
19
+ @admin ||= Admin.new(self)
20
+ end
21
+
22
+ # Activate this device for push notifications by registering with the push transport such as GCM/APNS
23
+ #
24
+ # @note This is unsupported in the Ruby library
25
+ def activate(*arg)
26
+ raise_unsupported
27
+ end
28
+
29
+ # Deactivate this device for push notifications by removing the registration with the push transport such as GCM/APNS
30
+ #
31
+ # @note This is unsupported in the Ruby library
32
+ def deactivate(*arg)
33
+ raise_unsupported
34
+ end
35
+
36
+ private
37
+ def raise_unsupported
38
+ raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. All PushChannel methods are unavailable'
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,54 @@
1
+ require 'ably/rest/push/device_registrations'
2
+ require 'ably/rest/push/channel_subscriptions'
3
+
4
+ module Ably::Rest
5
+ class Push
6
+ # Class providing push notification administrative functionality
7
+ # for registering devices and attaching to channels etc.
8
+ class Admin
9
+ include Ably::Modules::Conversions
10
+
11
+ # @api private
12
+ attr_reader :client
13
+
14
+ # @api private
15
+ attr_reader :push
16
+
17
+ def initialize(push)
18
+ @push = push
19
+ @client = push.client
20
+ end
21
+
22
+ # Publish a push message directly to a single recipient
23
+ #
24
+ # @param recipient [Hash] A recipient device, client_id or raw APNS/GCM target. Refer to push documentation
25
+ # @param data [Hash] The notification payload data and fields. Refer to push documentation
26
+ #
27
+ # @return [void]
28
+ #
29
+ def publish(recipient, data)
30
+ raise ArgumentError, "Expecting a Hash object for recipient, got #{recipient.class}" unless recipient.kind_of?(Hash)
31
+ raise ArgumentError, "Recipient data is empty. You must provide recipient details" if recipient.empty?
32
+ raise ArgumentError, "Expecting a Hash object for data, got #{data.class}" unless data.kind_of?(Hash)
33
+ raise ArgumentError, "Push data field is empty. You must provide attributes for the push notification" if data.empty?
34
+
35
+ publish_data = data.merge(recipient: IdiomaticRubyWrapper(recipient))
36
+ # Co-erce to camelCase for notitication fields which are always camelCase
37
+ publish_data[:notification] = IdiomaticRubyWrapper(data[:notification]) if publish_data[:notification].kind_of?(Hash)
38
+ client.post('/push/publish', publish_data)
39
+ end
40
+
41
+ # Manage device registrations
42
+ # @return [Ably::Rest::Push::DeviceRegistrations]
43
+ def device_registrations
44
+ @device_registrations ||= DeviceRegistrations.new(self)
45
+ end
46
+
47
+ # Manage channel subscriptions for devices or clients
48
+ # @return [Ably::Rest::Push::ChannelSubscriptions]
49
+ def channel_subscriptions
50
+ @channel_subscriptions ||= ChannelSubscriptions.new(self)
51
+ end
52
+ end
53
+ end
54
+ end