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.
- 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
|