ably 1.0.7 → 1.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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +14 -0
  3. data/.travis.yml +4 -4
  4. data/CHANGELOG.md +26 -3
  5. data/Rakefile +32 -0
  6. data/SPEC.md +920 -565
  7. data/ably.gemspec +9 -4
  8. data/lib/ably/auth.rb +28 -2
  9. data/lib/ably/exceptions.rb +8 -2
  10. data/lib/ably/models/channel_state_change.rb +1 -1
  11. data/lib/ably/models/connection_state_change.rb +1 -1
  12. data/lib/ably/models/device_details.rb +87 -0
  13. data/lib/ably/models/device_push_details.rb +86 -0
  14. data/lib/ably/models/error_info.rb +23 -2
  15. data/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -4
  16. data/lib/ably/models/protocol_message.rb +32 -2
  17. data/lib/ably/models/push_channel_subscription.rb +89 -0
  18. data/lib/ably/modules/conversions.rb +1 -1
  19. data/lib/ably/modules/encodeable.rb +1 -1
  20. data/lib/ably/modules/exception_codes.rb +128 -0
  21. data/lib/ably/modules/model_common.rb +15 -2
  22. data/lib/ably/modules/state_machine.rb +1 -1
  23. data/lib/ably/realtime.rb +1 -0
  24. data/lib/ably/realtime/auth.rb +1 -1
  25. data/lib/ably/realtime/channel.rb +24 -102
  26. data/lib/ably/realtime/channel/channel_manager.rb +2 -6
  27. data/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
  28. data/lib/ably/realtime/channel/publisher.rb +74 -0
  29. data/lib/ably/realtime/channel/push_channel.rb +62 -0
  30. data/lib/ably/realtime/client.rb +87 -0
  31. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +6 -2
  32. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  33. data/lib/ably/realtime/connection.rb +8 -5
  34. data/lib/ably/realtime/connection/connection_manager.rb +7 -7
  35. data/lib/ably/realtime/connection/websocket_transport.rb +1 -1
  36. data/lib/ably/realtime/presence.rb +4 -4
  37. data/lib/ably/realtime/presence/members_map.rb +3 -3
  38. data/lib/ably/realtime/push.rb +40 -0
  39. data/lib/ably/realtime/push/admin.rb +61 -0
  40. data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  41. data/lib/ably/realtime/push/device_registrations.rb +105 -0
  42. data/lib/ably/rest.rb +1 -0
  43. data/lib/ably/rest/channel.rb +33 -5
  44. data/lib/ably/rest/channel/push_channel.rb +62 -0
  45. data/lib/ably/rest/client.rb +137 -28
  46. data/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
  47. data/lib/ably/rest/presence.rb +1 -0
  48. data/lib/ably/rest/push.rb +42 -0
  49. data/lib/ably/rest/push/admin.rb +54 -0
  50. data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  51. data/lib/ably/rest/push/device_registrations.rb +103 -0
  52. data/lib/ably/version.rb +7 -2
  53. data/spec/acceptance/realtime/auth_spec.rb +6 -8
  54. data/spec/acceptance/realtime/channel_spec.rb +166 -51
  55. data/spec/acceptance/realtime/client_spec.rb +149 -0
  56. data/spec/acceptance/realtime/connection_failures_spec.rb +1 -1
  57. data/spec/acceptance/realtime/connection_spec.rb +4 -4
  58. data/spec/acceptance/realtime/message_spec.rb +19 -17
  59. data/spec/acceptance/realtime/presence_spec.rb +5 -5
  60. data/spec/acceptance/realtime/push_admin_spec.rb +696 -0
  61. data/spec/acceptance/realtime/push_spec.rb +27 -0
  62. data/spec/acceptance/rest/auth_spec.rb +4 -3
  63. data/spec/acceptance/rest/base_spec.rb +2 -2
  64. data/spec/acceptance/rest/client_spec.rb +129 -10
  65. data/spec/acceptance/rest/message_spec.rb +175 -4
  66. data/spec/acceptance/rest/push_admin_spec.rb +896 -0
  67. data/spec/acceptance/rest/push_spec.rb +25 -0
  68. data/spec/acceptance/rest/time_spec.rb +1 -1
  69. data/spec/run_parallel_tests +33 -0
  70. data/spec/unit/logger_spec.rb +10 -3
  71. data/spec/unit/models/device_details_spec.rb +102 -0
  72. data/spec/unit/models/device_push_details_spec.rb +101 -0
  73. data/spec/unit/models/error_info_spec.rb +51 -3
  74. data/spec/unit/models/message_spec.rb +17 -2
  75. data/spec/unit/models/presence_message_spec.rb +1 -1
  76. data/spec/unit/models/push_channel_subscription_spec.rb +86 -0
  77. data/spec/unit/realtime/client_spec.rb +12 -0
  78. data/spec/unit/realtime/push_channel_spec.rb +36 -0
  79. data/spec/unit/rest/channel_spec.rb +8 -1
  80. data/spec/unit/rest/client_spec.rb +30 -0
  81. data/spec/unit/rest/push_channel_spec.rb +36 -0
  82. metadata +71 -8
@@ -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,7 +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
+ #
120
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
121
139
  #
122
140
  # @return [Ably::Rest::Client]
123
141
  #
@@ -140,18 +158,21 @@ module Ably
140
158
  end
141
159
  end
142
160
 
143
- @realtime_client = options.delete(:realtime_client)
144
- @tls = options.delete(:tls) == false ? false : true
145
- @environment = options.delete(:environment) # nil is production
146
- @environment = nil if [:production, 'production'].include?(@environment)
147
- @protocol = options.delete(:protocol) || :msgpack
148
- @debug_http = options.delete(:debug_http)
149
- @log_level = options.delete(:log_level) || ::Logger::WARN
150
- @custom_logger = options.delete(:logger)
151
- @custom_host = options.delete(:rest_host)
152
- @custom_port = options.delete(:port)
153
- @custom_tls_port = options.delete(:tls_port)
154
- @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
+
155
176
 
156
177
  if options[:fallback_hosts_use_default] && options[:fallback_jhosts]
157
178
  raise ArgumentError, "fallback_hosts_use_default cannot be set to trye when fallback_jhosts is also provided"
@@ -167,6 +188,10 @@ module Ably
167
188
  Ably::FALLBACK_HOSTS
168
189
  end
169
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
170
195
  @http_defaults = HTTP_DEFAULTS.dup
171
196
  options.each do |key, val|
172
197
  if http_key = key[/^http_(.+)/, 1]
@@ -191,8 +216,12 @@ module Ably
191
216
  raise ArgumentError, 'Protocol is invalid. Must be either :msgpack or :json' unless [:msgpack, :json].include?(@protocol)
192
217
 
193
218
  token_params = options.delete(:default_token_params) || {}
194
- @options = options
195
- @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)
196
225
  @channels = Ably::Rest::Channels.new(self)
197
226
  @encoders = []
198
227
 
@@ -274,6 +303,24 @@ module Ably
274
303
  raw_request(:post, path, params, options)
275
304
  end
276
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
+
277
324
  # Perform an HTTP request to the Ably API
278
325
  # This is a convenience for customers who wish to use bleeding edge REST API functionality
279
326
  # that is either not documented or is not included in the API for our client libraries.
@@ -293,14 +340,14 @@ module Ably
293
340
 
294
341
  response = case method.to_sym
295
342
  when :get
296
- reauthorize_on_authorisation_failure do
343
+ reauthorize_on_authorization_failure do
297
344
  send_request(method, path, params, headers: headers)
298
345
  end
299
346
  when :post
300
347
  path_with_params = Addressable::URI.new
301
348
  path_with_params.query_values = params || {}
302
349
  query = path_with_params.query
303
- reauthorize_on_authorisation_failure do
350
+ reauthorize_on_authorization_failure do
304
351
  send_request(method, "#{path}#{"?#{query}" unless query.nil? || query.empty?}", body, headers: headers)
305
352
  end
306
353
  end
@@ -322,6 +369,20 @@ module Ably
322
369
  Models::HttpPaginatedResponse.new(response, path, self)
323
370
  end
324
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
+
325
386
  # @!attribute [r] endpoint
326
387
  # @return [URI::Generic] Default Ably REST endpoint used for all requests
327
388
  def endpoint
@@ -390,7 +451,6 @@ module Ably
390
451
  def fallback_connection
391
452
  unless defined?(@fallback_connections) && @fallback_connections
392
453
  @fallback_connections = fallback_hosts.shuffle.map { |host| Faraday.new(endpoint_for_host(host).to_s, connection_options) }
393
- @fallback_connections << Faraday.new(endpoint.to_s, connection_options) # Try the original host last if all fallbacks have been used
394
454
  end
395
455
  @fallback_index ||= 0
396
456
 
@@ -421,13 +481,46 @@ module Ably
421
481
  end
422
482
  end
423
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
+
424
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
+
425
518
  def raw_request(method, path, params = {}, options = {})
426
519
  options = options.clone
427
520
  if options.delete(:disable_automatic_reauthorize) == true
428
521
  send_request(method, path, params, options)
429
522
  else
430
- reauthorize_on_authorisation_failure do
523
+ reauthorize_on_authorization_failure do
431
524
  send_request(method, path, params, options)
432
525
  end
433
526
  end
@@ -443,10 +536,22 @@ module Ably
443
536
  retry_sequence_id = nil
444
537
  request_id = SecureRandom.urlsafe_base64(10) if add_request_ids
445
538
 
539
+ preferred_fallback_connection_for_first_request = get_preferred_fallback_connection_object
540
+
446
541
  begin
447
- 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
448
553
 
449
- connection(use_fallback: use_fallback).send(method, path, params) do |request|
554
+ conn.send(method, path, params) do |request|
450
555
  if add_request_ids
451
556
  request.params[:request_id] = request_id
452
557
  request.options.context = {} if request.options.context.nil?
@@ -462,10 +567,12 @@ module Ably
462
567
  end
463
568
  end.tap do
464
569
  if retry_count > 0
465
- logger.warn do
570
+ retry_log_severity = log_retries_as_info ? :info : :warn
571
+ logger.public_send(retry_log_severity) do
466
572
  "Ably::Rest::Client - Request SUCCEEDED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \
467
573
  " #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)"
468
574
  end
575
+ set_preferred_fallback_connection conn
469
576
  end
470
577
  end
471
578
 
@@ -473,30 +580,32 @@ module Ably
473
580
  retry_sequence_id ||= SecureRandom.urlsafe_base64(4)
474
581
  time_passed = Time.now - requested_at
475
582
 
476
- 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)
477
584
  retry_count += 1
478
- 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}" }
479
587
  retry
480
588
  end
481
589
 
482
- logger.error do
590
+ retry_log_severity = log_retries_as_info ? :info : :error
591
+ logger.public_send(retry_log_severity) do
483
592
  "Ably::Rest::Client - Request FAILED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \
484
593
  " #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)"
485
594
  end
486
595
 
487
596
  case error
488
597
  when Faraday::TimeoutError
489
- 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 })
490
599
  when Faraday::ClientError
491
600
  # request_id is also available in the request context
492
- 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 })
493
602
  else
494
603
  raise error
495
604
  end
496
605
  end
497
606
  end
498
607
 
499
- def reauthorize_on_authorisation_failure
608
+ def reauthorize_on_authorization_failure
500
609
  yield
501
610
  rescue Ably::Exceptions::TokenExpired => e
502
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
@@ -0,0 +1,121 @@
1
+ module Ably::Rest
2
+ class Push
3
+ # Manage push notification channel subscriptions for devices or client identifiers
4
+ class ChannelSubscriptions
5
+ include Ably::Modules::Conversions
6
+
7
+ # @api private
8
+ attr_reader :client
9
+
10
+ # @api private
11
+ attr_reader :admin
12
+
13
+ def initialize(admin)
14
+ @admin = admin
15
+ @client = admin.client
16
+ end
17
+
18
+ # List channel subscriptions filtered by optional params
19
+ #
20
+ # @param [Hash] params the filter options for the list channel subscription request. At least one of channel, client_id or device_id is required
21
+ # @option params [String] :channel filter by realtime pub/sub channel name
22
+ # @option params [String] :client_id filter by devices registered to a client identifier. If provided with device_id param, a concat operation is used so that any device with this client_id or provided device_id is returned.
23
+ # @option params [String] :device_id filter by unique device ID. If provided with client_id param, a concat operation is used so that any device with this device_id or provided client_id is returned.
24
+ # @option params [Integer] :limit maximum number of subscriptions to retrieve up to 1,000, defaults to 100
25
+ #
26
+ # @return [Ably::Models::PaginatedResult<Ably::Models::PushChannelSubscription>] Paginated list of matching {Ably::Models::PushChannelSubscription}
27
+ #
28
+ def list(params)
29
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
30
+
31
+ if (IdiomaticRubyWrapper(params).keys & [:channel, :client_id, :device_id]).length == 0
32
+ raise ArgumentError, "at least one channel, client_id or device_id filter param must be provided"
33
+ end
34
+
35
+ params = params.clone
36
+
37
+ paginated_options = {
38
+ coerce_into: 'Ably::Models::PushChannelSubscription',
39
+ async_blocking_operations: params.delete(:async_blocking_operations),
40
+ }
41
+
42
+ response = client.get('/push/channelSubscriptions', IdiomaticRubyWrapper(params).as_json)
43
+
44
+ Ably::Models::PaginatedResult.new(response, '', client, paginated_options)
45
+ end
46
+
47
+ # List channels with at least one subscribed device
48
+ #
49
+ # @param [Hash] params the options for the list channels request
50
+ # @option params [Integer] :limit maximum number of channels to retrieve up to 1,000, defaults to 100
51
+ #
52
+ # @return [Ably::Models::PaginatedResult<String>] Paginated list of matching {Ably::Models::PushChannelSubscription}
53
+ #
54
+ def list_channels(params = {})
55
+ params = {} if params.nil?
56
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
57
+
58
+ params = params.clone
59
+
60
+ paginated_options = {
61
+ coerce_into: 'String',
62
+ async_blocking_operations: params.delete(:async_blocking_operations),
63
+ }
64
+
65
+ response = client.get('/push/channels', IdiomaticRubyWrapper(params).as_json)
66
+
67
+ Ably::Models::PaginatedResult.new(response, '', client, paginated_options)
68
+ end
69
+
70
+ # Save push channel subscription for a device or client ID
71
+ #
72
+ # @param [Ably::Models::PushChannelSubscription,Hash] push_channel_subscription the push channel subscription details to save
73
+ #
74
+ # @return [void]
75
+ #
76
+ def save(push_channel_subscription)
77
+ push_channel_subscription_object = PushChannelSubscription(push_channel_subscription)
78
+ raise ArgumentError, "Channel is required yet is empty" if push_channel_subscription_object.channel.to_s.empty?
79
+
80
+ client.post("/push/channelSubscriptions", push_channel_subscription_object.as_json)
81
+ end
82
+
83
+ # Remove a push channel subscription
84
+ #
85
+ # @param [Ably::Models::PushChannelSubscription,Hash] push_channel_subscription the push channel subscription details to remove
86
+ #
87
+ # @return [void]
88
+ #
89
+ def remove(push_channel_subscription)
90
+ push_channel_subscription_object = PushChannelSubscription(push_channel_subscription)
91
+ raise ArgumentError, "Channel is required yet is empty" if push_channel_subscription_object.channel.to_s.empty?
92
+ if push_channel_subscription_object.client_id.to_s.empty? && push_channel_subscription_object.device_id.to_s.empty?
93
+ raise ArgumentError, "Either client_id or device_id must be present"
94
+ end
95
+
96
+ client.delete("/push/channelSubscriptions", push_channel_subscription_object.as_json)
97
+ end
98
+
99
+ # Remove all matching push channel subscriptions
100
+ #
101
+ # @param [Hash] params the filter options for the list channel subscription request. At least one of channel, client_id or device_id is required
102
+ # @option params [String] :channel filter by realtime pub/sub channel name
103
+ # @option params [String] :client_id filter by devices registered to a client identifier. If provided with device_id param, a concat operation is used so that any device with this client_id or provided device_id is returned.
104
+ # @option params [String] :device_id filter by unique device ID. If provided with client_id param, a concat operation is used so that any device with this device_id or provided client_id is returned.
105
+ #
106
+ # @return [void]
107
+ #
108
+ def remove_where(params)
109
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
110
+
111
+ if (IdiomaticRubyWrapper(params).keys & [:channel, :client_id, :device_id]).length == 0
112
+ raise ArgumentError, "at least one channel, client_id or device_id filter param must be provided"
113
+ end
114
+
115
+ params = params.clone
116
+
117
+ client.delete("/push/channelSubscriptions", IdiomaticRubyWrapper(params).as_json)
118
+ end
119
+ end
120
+ end
121
+ end