ably 1.0.7 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.editorconfig +14 -0
- data/.travis.yml +4 -4
- data/CHANGELOG.md +26 -3
- data/Rakefile +32 -0
- data/SPEC.md +920 -565
- data/ably.gemspec +9 -4
- data/lib/ably/auth.rb +28 -2
- data/lib/ably/exceptions.rb +8 -2
- data/lib/ably/models/channel_state_change.rb +1 -1
- data/lib/ably/models/connection_state_change.rb +1 -1
- data/lib/ably/models/device_details.rb +87 -0
- data/lib/ably/models/device_push_details.rb +86 -0
- data/lib/ably/models/error_info.rb +23 -2
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -4
- data/lib/ably/models/protocol_message.rb +32 -2
- data/lib/ably/models/push_channel_subscription.rb +89 -0
- data/lib/ably/modules/conversions.rb +1 -1
- data/lib/ably/modules/encodeable.rb +1 -1
- data/lib/ably/modules/exception_codes.rb +128 -0
- data/lib/ably/modules/model_common.rb +15 -2
- data/lib/ably/modules/state_machine.rb +1 -1
- data/lib/ably/realtime.rb +1 -0
- data/lib/ably/realtime/auth.rb +1 -1
- data/lib/ably/realtime/channel.rb +24 -102
- data/lib/ably/realtime/channel/channel_manager.rb +2 -6
- data/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
- data/lib/ably/realtime/channel/publisher.rb +74 -0
- data/lib/ably/realtime/channel/push_channel.rb +62 -0
- data/lib/ably/realtime/client.rb +87 -0
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +6 -2
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
- data/lib/ably/realtime/connection.rb +8 -5
- data/lib/ably/realtime/connection/connection_manager.rb +7 -7
- data/lib/ably/realtime/connection/websocket_transport.rb +1 -1
- data/lib/ably/realtime/presence.rb +4 -4
- data/lib/ably/realtime/presence/members_map.rb +3 -3
- data/lib/ably/realtime/push.rb +40 -0
- data/lib/ably/realtime/push/admin.rb +61 -0
- data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
- data/lib/ably/realtime/push/device_registrations.rb +105 -0
- data/lib/ably/rest.rb +1 -0
- data/lib/ably/rest/channel.rb +33 -5
- data/lib/ably/rest/channel/push_channel.rb +62 -0
- data/lib/ably/rest/client.rb +137 -28
- data/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
- data/lib/ably/rest/presence.rb +1 -0
- data/lib/ably/rest/push.rb +42 -0
- data/lib/ably/rest/push/admin.rb +54 -0
- data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
- data/lib/ably/rest/push/device_registrations.rb +103 -0
- data/lib/ably/version.rb +7 -2
- data/spec/acceptance/realtime/auth_spec.rb +6 -8
- data/spec/acceptance/realtime/channel_spec.rb +166 -51
- data/spec/acceptance/realtime/client_spec.rb +149 -0
- data/spec/acceptance/realtime/connection_failures_spec.rb +1 -1
- data/spec/acceptance/realtime/connection_spec.rb +4 -4
- data/spec/acceptance/realtime/message_spec.rb +19 -17
- data/spec/acceptance/realtime/presence_spec.rb +5 -5
- data/spec/acceptance/realtime/push_admin_spec.rb +696 -0
- data/spec/acceptance/realtime/push_spec.rb +27 -0
- data/spec/acceptance/rest/auth_spec.rb +4 -3
- data/spec/acceptance/rest/base_spec.rb +2 -2
- data/spec/acceptance/rest/client_spec.rb +129 -10
- data/spec/acceptance/rest/message_spec.rb +175 -4
- data/spec/acceptance/rest/push_admin_spec.rb +896 -0
- data/spec/acceptance/rest/push_spec.rb +25 -0
- data/spec/acceptance/rest/time_spec.rb +1 -1
- data/spec/run_parallel_tests +33 -0
- data/spec/unit/logger_spec.rb +10 -3
- data/spec/unit/models/device_details_spec.rb +102 -0
- data/spec/unit/models/device_push_details_spec.rb +101 -0
- data/spec/unit/models/error_info_spec.rb +51 -3
- data/spec/unit/models/message_spec.rb +17 -2
- data/spec/unit/models/presence_message_spec.rb +1 -1
- data/spec/unit/models/push_channel_subscription_spec.rb +86 -0
- data/spec/unit/realtime/client_spec.rb +12 -0
- data/spec/unit/realtime/push_channel_spec.rb +36 -0
- data/spec/unit/rest/channel_spec.rb +8 -1
- data/spec/unit/rest/client_spec.rb +30 -0
- data/spec/unit/rest/push_channel_spec.rb +36 -0
- metadata +71 -8
data/lib/ably/rest/client.rb
CHANGED
@@ -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
|
-
#
|
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
|
144
|
-
@tls
|
145
|
-
@environment
|
146
|
-
@environment
|
147
|
-
@protocol
|
148
|
-
@debug_http
|
149
|
-
@log_level
|
150
|
-
@custom_logger
|
151
|
-
@custom_host
|
152
|
-
@custom_port
|
153
|
-
@custom_tls_port
|
154
|
-
@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
|
195
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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,
|
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,
|
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
|
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, "
|
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
|
data/lib/ably/rest/presence.rb
CHANGED
@@ -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
|