ably 1.0.7 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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