lowdown 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +121 -0
- data/Gemfile +11 -3
- data/README.md +145 -14
- data/Rakefile +13 -6
- data/bin/lowdown +38 -19
- data/examples/long-running.rb +63 -0
- data/examples/simple.rb +37 -0
- data/lib/lowdown.rb +2 -21
- data/lib/lowdown/certificate.rb +21 -1
- data/lib/lowdown/client.rb +156 -60
- data/lib/lowdown/client/request_group.rb +70 -0
- data/lib/lowdown/connection.rb +257 -182
- data/lib/lowdown/connection/monitor.rb +84 -0
- data/lib/lowdown/mock.rb +57 -49
- data/lib/lowdown/notification.rb +24 -6
- data/lib/lowdown/response.rb +9 -20
- data/lib/lowdown/version.rb +4 -1
- data/lowdown.gemspec +5 -3
- metadata +22 -4
- data/lib/lowdown/threading.rb +0 -188
@@ -0,0 +1,63 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
|
2
|
+
require "lowdown"
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
cert_file, environment, device_token = ARGV.first(3)
|
6
|
+
unless cert_file && environment && device_token
|
7
|
+
puts "Usage: #{$PROGRAM_NAME} path/to/cert.pem [production|development] device-token"
|
8
|
+
exit 1
|
9
|
+
end
|
10
|
+
|
11
|
+
# $CELLULOID_DEBUG = true
|
12
|
+
# Celluloid.logger.level = Logger::DEBUG
|
13
|
+
# Celluloid.logger.level = Logger::INFO
|
14
|
+
Celluloid.logger.level = Logger::ERROR
|
15
|
+
|
16
|
+
logger = Logger.new(STDOUT)
|
17
|
+
|
18
|
+
client = Lowdown::Client.production(environment == "production",
|
19
|
+
certificate: File.read(cert_file),
|
20
|
+
pool_size: 3,
|
21
|
+
keep_alive: true) # This option is the key to long running connections
|
22
|
+
client.connect
|
23
|
+
|
24
|
+
loop do
|
25
|
+
begin
|
26
|
+
logger.info "Perform burst"
|
27
|
+
# Perform a burst of notifications from multiple concurrent threads to demonstrate thread safety.
|
28
|
+
#
|
29
|
+
Array.new(3) do
|
30
|
+
Thread.new do
|
31
|
+
client.group do |group|
|
32
|
+
10.times do
|
33
|
+
notification = Lowdown::Notification.new(:token => device_token)
|
34
|
+
notification.payload = { :alert => "Hello HTTP/2! ID=#{notification.id}" }
|
35
|
+
group.send_notification(notification) do |response|
|
36
|
+
if response.success?
|
37
|
+
logger.debug "Sent notification with ID: #{notification.id}"
|
38
|
+
else
|
39
|
+
logger.error "[!] (##{response.id}): #{response}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end.each(&:join)
|
46
|
+
|
47
|
+
logger.info "Sleep for 5 seconds"
|
48
|
+
sleep(5)
|
49
|
+
|
50
|
+
rescue Interrupt
|
51
|
+
logger.info "[!] Interrupt, exiting"
|
52
|
+
break
|
53
|
+
|
54
|
+
rescue Exception => e
|
55
|
+
logger.error "[!] Exception occurred, re-trying in 1 second: #{e.inspect}\n\t#{e.backtrace.join("\n\t")}"
|
56
|
+
sleep 1
|
57
|
+
redo
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
client.disconnect
|
62
|
+
puts "Finished!"
|
63
|
+
|
data/examples/simple.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
|
2
|
+
require "lowdown"
|
3
|
+
|
4
|
+
cert_file, environment, device_token = ARGV.first(3)
|
5
|
+
unless cert_file && environment && device_token
|
6
|
+
puts "Usage: #{$PROGRAM_NAME} path/to/cert.pem [production|development] device-token"
|
7
|
+
exit 1
|
8
|
+
end
|
9
|
+
|
10
|
+
production = environment == "production"
|
11
|
+
|
12
|
+
# $CELLULOID_DEBUG = true
|
13
|
+
# Celluloid.logger.level = Logger::DEBUG
|
14
|
+
# Celluloid.logger.level = Logger::INFO
|
15
|
+
Celluloid.logger.level = Logger::ERROR
|
16
|
+
|
17
|
+
# Connection time can take a while, just count the time it takes to connect.
|
18
|
+
start = nil
|
19
|
+
|
20
|
+
# The block form of Client#connect flushes and closes the connection at the end of the block.
|
21
|
+
Lowdown::Client.production(production, certificate: File.read(cert_file), pool_size: 2).connect do |group|
|
22
|
+
start = Time.now
|
23
|
+
600.times do
|
24
|
+
notification = Lowdown::Notification.new(:token => device_token)
|
25
|
+
notification.payload = { :alert => "Hello HTTP/2! ID=#{notification.id}" }
|
26
|
+
group.send_notification(notification) do |response|
|
27
|
+
if response.success?
|
28
|
+
puts "Sent notification with ID: #{notification.id}"
|
29
|
+
else
|
30
|
+
puts "[!] (##{response.id}): #{response}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
puts "Finished in #{Time.now - start} seconds"
|
37
|
+
|
data/lib/lowdown.rb
CHANGED
@@ -3,27 +3,8 @@ require "lowdown/version"
|
|
3
3
|
|
4
4
|
# Lowdown is a Ruby client for the HTTP/2 version of the Apple Push Notification Service.
|
5
5
|
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
# Note that it is thus _your_ responsibility to take the threading issue into account. E.g. if you are planning to
|
9
|
-
# update a DB model with the status of a notification delivery, be sure to respect the treading rules of your DB client.
|
10
|
-
#
|
11
|
-
# The main classes you will interact with are {Lowdown::Client} and {Lowdown::Notification}. For testing purposes there
|
12
|
-
# are some helpers available in {Lowdown::Mock}.
|
13
|
-
#
|
14
|
-
# @example At its simplest, you can send a notification like so:
|
15
|
-
#
|
16
|
-
# notification = Lowdown::Notification.new(:token => "device-token", :payload => { :alert => "Hello World!" })
|
17
|
-
#
|
18
|
-
# Lowdown::Client.production(true, File.read("path/to/certificate.pem")).connect do |client|
|
19
|
-
# client.send_notification(notification) do |response|
|
20
|
-
# if response.success?
|
21
|
-
# puts "Notification sent"
|
22
|
-
# else
|
23
|
-
# puts "Notification failed: #{response}"
|
24
|
-
# end
|
25
|
-
# end
|
26
|
-
# end
|
6
|
+
# Refer to the README for usage instructions.
|
27
7
|
#
|
28
8
|
module Lowdown
|
29
9
|
end
|
10
|
+
|
data/lib/lowdown/certificate.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "openssl"
|
2
3
|
|
3
4
|
module Lowdown
|
@@ -36,6 +37,17 @@ module Lowdown
|
|
36
37
|
new(certificate, key)
|
37
38
|
end
|
38
39
|
|
40
|
+
# A convenience method that initializes a Certificate with the certificate and key from a SSL context object.
|
41
|
+
#
|
42
|
+
# @param [OpenSSL::SSL::SSLContext] context
|
43
|
+
# the context from which to initialize a Certificate.
|
44
|
+
#
|
45
|
+
# @return (see Certificate#initialize)
|
46
|
+
#
|
47
|
+
def self.from_ssl_context(context)
|
48
|
+
new(context.cert, context.key)
|
49
|
+
end
|
50
|
+
|
39
51
|
# @param [OpenSSL::X509::Certificate] certificate
|
40
52
|
# the Apple Push Notification certificate.
|
41
53
|
#
|
@@ -67,6 +79,13 @@ module Lowdown
|
|
67
79
|
[@key, @certificate].compact.map(&:to_pem).join("\n")
|
68
80
|
end
|
69
81
|
|
82
|
+
# @return [Boolean]
|
83
|
+
# whether or not this Certificate is equal in contents to another Certificate.
|
84
|
+
#
|
85
|
+
def ==(other)
|
86
|
+
other.is_a?(Certificate) && other.to_pem == to_pem
|
87
|
+
end
|
88
|
+
|
70
89
|
# @return [OpenSSL::SSL::SSLContext]
|
71
90
|
# a SSL context, configured with the certificate/key pair, which is used to connect to the APN service.
|
72
91
|
#
|
@@ -118,7 +137,7 @@ module Lowdown
|
|
118
137
|
# the App ID / app’s Bundle ID that this certificate is for.
|
119
138
|
#
|
120
139
|
def app_bundle_id
|
121
|
-
@certificate.subject.to_a.find { |key, *_| key ==
|
140
|
+
@certificate.subject.to_a.find { |key, *_| key == "UID" }[1]
|
122
141
|
end
|
123
142
|
|
124
143
|
private
|
@@ -133,3 +152,4 @@ module Lowdown
|
|
133
152
|
end
|
134
153
|
end
|
135
154
|
end
|
155
|
+
|
data/lib/lowdown/client.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "lowdown/client/request_group"
|
1
4
|
require "lowdown/certificate"
|
2
5
|
require "lowdown/connection"
|
6
|
+
require "lowdown/connection/monitor"
|
3
7
|
require "lowdown/notification"
|
4
8
|
|
5
9
|
require "uri"
|
@@ -8,6 +12,9 @@ require "json"
|
|
8
12
|
module Lowdown
|
9
13
|
# The main class to use for interactions with the Apple Push Notification HTTP/2 service.
|
10
14
|
#
|
15
|
+
# Important connection configuration options are `pool_size` and `keep_alive`. The former specifies the number of
|
16
|
+
# simultaneous connections the client should make and the latter is key for long running processes.
|
17
|
+
#
|
11
18
|
class Client
|
12
19
|
# The details to connect to the development (sandbox) environment version of the APN service.
|
13
20
|
#
|
@@ -17,25 +24,37 @@ module Lowdown
|
|
17
24
|
#
|
18
25
|
PRODUCTION_URI = URI.parse("https://api.push.apple.com:443")
|
19
26
|
|
27
|
+
# The default timeout for {#group}.
|
28
|
+
#
|
29
|
+
DEFAULT_GROUP_TIMEOUT = 3600
|
30
|
+
|
20
31
|
# @!group Constructor Summary
|
21
32
|
|
22
33
|
# This is the most convenient constructor for regular use.
|
23
34
|
#
|
24
|
-
# It then calls {Client.client}.
|
25
|
-
#
|
26
35
|
# @param [Boolean] production
|
27
36
|
# whether to use the production or the development environment.
|
28
37
|
#
|
29
|
-
# @param [Certificate, String]
|
38
|
+
# @param [Certificate, String] certificate
|
30
39
|
# a configured Certificate or PEM data to construct a Certificate from.
|
31
40
|
#
|
41
|
+
# @param [Fixnum] pool_size
|
42
|
+
# the number of connections to make.
|
43
|
+
#
|
44
|
+
# @param [Boolean] keep_alive
|
45
|
+
# when `true` this will make connections, new and restarted, immediately connect to the remote service. Use
|
46
|
+
# this if you want to keep connections open indefinitely.
|
47
|
+
#
|
48
|
+
# @param [Class] connection_class
|
49
|
+
# the connection class to instantiate, this can for instan be {Mock::Connection} during testing.
|
50
|
+
#
|
32
51
|
# @raise [ArgumentError]
|
33
52
|
# raised if the provided Certificate does not support the requested environment.
|
34
53
|
#
|
35
54
|
# @return (see Client#initialize)
|
36
55
|
#
|
37
|
-
def self.production(production,
|
38
|
-
certificate = Certificate.certificate(
|
56
|
+
def self.production(production, certificate:, pool_size: 1, keep_alive: false, connection_class: Connection)
|
57
|
+
certificate = Certificate.certificate(certificate)
|
39
58
|
if production
|
40
59
|
unless certificate.production?
|
41
60
|
raise ArgumentError, "The specified certificate is not usable with the production environment."
|
@@ -45,45 +64,59 @@ module Lowdown
|
|
45
64
|
raise ArgumentError, "The specified certificate is not usable with the development environment."
|
46
65
|
end
|
47
66
|
end
|
48
|
-
client(production ? PRODUCTION_URI : DEVELOPMENT_URI,
|
67
|
+
client(uri: production ? PRODUCTION_URI : DEVELOPMENT_URI,
|
68
|
+
certificate: certificate,
|
69
|
+
pool_size: pool_size,
|
70
|
+
keep_alive: keep_alive,
|
71
|
+
connection_class: connection_class)
|
49
72
|
end
|
50
73
|
|
51
|
-
# Creates a connection that connects to the specified `uri`.
|
52
|
-
#
|
53
|
-
# It then calls {Client.client_with_connection}.
|
74
|
+
# Creates a connection pool that connects to the specified `uri`.
|
54
75
|
#
|
55
76
|
# @param [URI] uri
|
56
77
|
# the endpoint details of the service to connect to.
|
57
78
|
#
|
58
|
-
# @param [Certificate, String]
|
79
|
+
# @param [Certificate, String] certificate
|
59
80
|
# a configured Certificate or PEM data to construct a Certificate from.
|
60
81
|
#
|
82
|
+
# @param [Fixnum] pool_size
|
83
|
+
# the number of connections to make.
|
84
|
+
#
|
85
|
+
# @param [Boolean] keep_alive
|
86
|
+
# when `true` this will make connections, new and restarted, immediately connect to the remote service. Use
|
87
|
+
# this if you want to keep connections open indefinitely.
|
88
|
+
#
|
89
|
+
# @param [Class] connection_class
|
90
|
+
# the connection class to instantiate, this can for instan be {Mock::Connection} during testing.
|
91
|
+
#
|
61
92
|
# @return (see Client#initialize)
|
62
93
|
#
|
63
|
-
def self.client(uri,
|
64
|
-
certificate = Certificate.certificate(
|
65
|
-
|
94
|
+
def self.client(uri:, certificate:, pool_size: 1, keep_alive: false, connection_class: Connection)
|
95
|
+
certificate = Certificate.certificate(certificate)
|
96
|
+
connection_class ||= Connection
|
97
|
+
connection_pool = connection_class.pool(size: pool_size, args: [uri, certificate.ssl_context, keep_alive])
|
98
|
+
client_with_connection(connection_pool, certificate: certificate)
|
66
99
|
end
|
67
100
|
|
68
|
-
# Creates a Client configured with the `app_bundle_id` as its `default_topic`, in case the Certificate represents
|
69
|
-
# Universal Certificate.
|
101
|
+
# Creates a Client configured with the `app_bundle_id` as its `default_topic`, in case the Certificate represents
|
102
|
+
# a Universal Certificate.
|
70
103
|
#
|
71
|
-
# @param [Connection] connection
|
72
|
-
# a Connection configured to connect to the remote service.
|
104
|
+
# @param [Connection, Celluloid::Supervision::Container::Pool<Connection>] connection
|
105
|
+
# a single Connection or a pool of Connection actors configured to connect to the remote service.
|
73
106
|
#
|
74
107
|
# @param [Certificate] certificate
|
75
108
|
# a configured Certificate.
|
76
109
|
#
|
77
110
|
# @return (see Client#initialize)
|
78
111
|
#
|
79
|
-
def self.client_with_connection(connection, certificate)
|
80
|
-
new(connection, certificate.universal? ? certificate.topics.first : nil)
|
112
|
+
def self.client_with_connection(connection, certificate:)
|
113
|
+
new(connection: connection, default_topic: certificate.universal? ? certificate.topics.first : nil)
|
81
114
|
end
|
82
115
|
|
83
116
|
# You should normally use any of the other constructors to create a Client object.
|
84
117
|
#
|
85
|
-
# @param [Connection] connection
|
86
|
-
# a Connection configured to connect to the remote service.
|
118
|
+
# @param [Connection, Celluloid::Supervision::Container::Pool<Connection>] connection
|
119
|
+
# a single Connection or a pool of Connection actors configured to connect to the remote service.
|
87
120
|
#
|
88
121
|
# @param [String] default_topic
|
89
122
|
# the ‘topic’ to use if the Certificate is a Universal Certificate and a Notification doesn’t explicitely
|
@@ -92,14 +125,14 @@ module Lowdown
|
|
92
125
|
# @return [Client]
|
93
126
|
# a new instance of Client.
|
94
127
|
#
|
95
|
-
def initialize(connection
|
128
|
+
def initialize(connection:, default_topic: nil)
|
96
129
|
@connection, @default_topic = connection, default_topic
|
97
130
|
end
|
98
131
|
|
99
132
|
# @!group Instance Attribute Summary
|
100
133
|
|
101
|
-
# @return [Connection]
|
102
|
-
# a Connection configured to connect to the remote service.
|
134
|
+
# @return [Connection, Celluloid::Supervision::Container::Pool<Connection>]
|
135
|
+
# a single Connection or a pool of Connection actors configured to connect to the remote service.
|
103
136
|
#
|
104
137
|
attr_reader :connection
|
105
138
|
|
@@ -111,85 +144,148 @@ module Lowdown
|
|
111
144
|
|
112
145
|
# @!group Instance Method Summary
|
113
146
|
|
114
|
-
# Opens the connection to the service
|
147
|
+
# Opens the connection to the service, yields a request group, and automatically closes the connection by the end of
|
148
|
+
# the block.
|
149
|
+
#
|
150
|
+
# @note Don’t use this if you opted to keep a pool of connections alive.
|
115
151
|
#
|
116
|
-
# @see
|
152
|
+
# @see Connection#connect
|
153
|
+
# @see Client#group
|
117
154
|
#
|
118
|
-
# @
|
119
|
-
#
|
155
|
+
# @param [Numeric] group_timeout
|
156
|
+
# the maximum amount of time to wait for a request group to halt the caller thread. Defaults to 1 hour.
|
120
157
|
#
|
121
|
-
# @
|
158
|
+
# @yieldparam (see Client#group)
|
122
159
|
#
|
123
|
-
|
124
|
-
|
125
|
-
|
160
|
+
# @return [void]
|
161
|
+
#
|
162
|
+
def connect(group_timeout: nil, &block)
|
163
|
+
if @connection.respond_to?(:actors)
|
164
|
+
@connection.actors.each { |connection| connection.async.connect }
|
165
|
+
else
|
166
|
+
@connection.async.connect
|
167
|
+
end
|
168
|
+
if block
|
126
169
|
begin
|
127
|
-
|
170
|
+
group(timeout: group_timeout, &block)
|
128
171
|
ensure
|
129
|
-
|
172
|
+
disconnect
|
130
173
|
end
|
131
174
|
end
|
132
175
|
end
|
133
176
|
|
134
|
-
#
|
177
|
+
# Closes the connection to the service.
|
135
178
|
#
|
136
|
-
# @see
|
179
|
+
# @see Connection#disconnect
|
137
180
|
#
|
138
|
-
# @return
|
181
|
+
# @return [void]
|
139
182
|
#
|
140
|
-
def
|
141
|
-
@connection.
|
183
|
+
def disconnect
|
184
|
+
@connection.disconnect
|
185
|
+
rescue Celluloid::DeadActorError
|
186
|
+
# Rescue this exception instead of calling #alive? as that only works on an actor, not a pool.
|
142
187
|
end
|
143
188
|
|
144
|
-
#
|
189
|
+
# Use this to group a batch of requests and halt the caller thread until all of the requests in the group have been
|
190
|
+
# performed.
|
191
|
+
#
|
192
|
+
# It proxies {RequestGroup#send_notification} to {Client#send_notification}, but, unlike the latter, the request
|
193
|
+
# callbacks are provided in the form of a block.
|
194
|
+
#
|
195
|
+
# @note Do **not** share the yielded group across threads.
|
196
|
+
#
|
197
|
+
# @see RequestGroup#send_notification
|
198
|
+
# @see Connection::Monitor
|
145
199
|
#
|
146
|
-
# @
|
200
|
+
# @param [Numeric] timeout
|
201
|
+
# the maximum amount of time to wait for a request group to halt the caller thread. Defaults to 1 hour.
|
147
202
|
#
|
148
|
-
# @
|
203
|
+
# @yieldparam [RequestGroup] group
|
204
|
+
# the request group object.
|
149
205
|
#
|
150
|
-
|
151
|
-
|
206
|
+
# @raise [Exception]
|
207
|
+
# if a connection in the pool has died during the execution of this group, the reason for its death will be
|
208
|
+
# raised.
|
209
|
+
#
|
210
|
+
# @return [void]
|
211
|
+
#
|
212
|
+
def group(timeout: nil)
|
213
|
+
group = nil
|
214
|
+
monitor do |condition|
|
215
|
+
group = RequestGroup.new(self, condition)
|
216
|
+
yield group
|
217
|
+
if !group.empty? && exception = condition.wait(timeout || DEFAULT_GROUP_TIMEOUT)
|
218
|
+
raise exception
|
219
|
+
end
|
220
|
+
end
|
221
|
+
ensure
|
222
|
+
group.terminate
|
223
|
+
end
|
224
|
+
|
225
|
+
# Registers a condition object with the connection pool, for the duration of the given block. It either returns an
|
226
|
+
# exception that caused a connection to die, or whatever value you signal to it.
|
227
|
+
#
|
228
|
+
# This is automatically used by {#group}.
|
229
|
+
#
|
230
|
+
# @yieldparam [Connection::Monitor::Condition] condition
|
231
|
+
# the monitor condition object.
|
232
|
+
#
|
233
|
+
# @return [void]
|
234
|
+
#
|
235
|
+
def monitor
|
236
|
+
condition = Connection::Monitor::Condition.new
|
237
|
+
if defined?(Mock::Connection) && @connection.class == Mock::Connection
|
238
|
+
yield condition
|
239
|
+
else
|
240
|
+
begin
|
241
|
+
@connection.__register_lowdown_crash_condition__(condition)
|
242
|
+
yield condition
|
243
|
+
ensure
|
244
|
+
@connection.__deregister_lowdown_crash_condition__(condition)
|
245
|
+
end
|
246
|
+
end
|
152
247
|
end
|
153
248
|
|
154
|
-
# Verifies the `notification` is valid and sends it to the remote service.
|
249
|
+
# Verifies the `notification` is valid and then sends it to the remote service. Response feedback is provided via
|
250
|
+
# a delegate mechanism.
|
155
251
|
#
|
156
|
-
# @
|
252
|
+
# @note In general, you will probably want to use {#group} to be able to use {RequestGroup#send_notification},
|
253
|
+
# which takes a traditional blocks-based callback approach.
|
157
254
|
#
|
158
|
-
# @
|
255
|
+
# @see Connection#post
|
159
256
|
#
|
160
257
|
# @param [Notification] notification
|
161
258
|
# the notification object whose data to send to the service.
|
162
259
|
#
|
163
|
-
# @
|
164
|
-
#
|
165
|
-
#
|
166
|
-
# @yieldparam [Response] response
|
167
|
-
# the Response that holds the status data that came back from the service.
|
260
|
+
# @param [Connection::DelegateProtocol] delegate
|
261
|
+
# an object that implements the connection delegate protocol.
|
168
262
|
#
|
169
|
-
# @
|
170
|
-
#
|
263
|
+
# @param [Object, nil] context
|
264
|
+
# any object that you want to be passed to the delegate once the response is back.
|
171
265
|
#
|
172
266
|
# @raise [ArgumentError]
|
173
267
|
# raised if the Notification is not {Notification#valid?}.
|
174
268
|
#
|
175
269
|
# @return [void]
|
176
270
|
#
|
177
|
-
def send_notification(notification,
|
271
|
+
def send_notification(notification, delegate:, context: nil)
|
178
272
|
raise ArgumentError, "Invalid notification: #{notification.inspect}" unless notification.valid?
|
179
273
|
|
180
274
|
topic = notification.topic || @default_topic
|
181
275
|
headers = {}
|
182
276
|
headers["apns-expiration"] = (notification.expiration || 0).to_i
|
183
|
-
headers["apns-id"] = notification.formatted_id
|
277
|
+
headers["apns-id"] = notification.formatted_id
|
184
278
|
headers["apns-priority"] = notification.priority if notification.priority
|
185
279
|
headers["apns-topic"] = topic if topic
|
186
280
|
|
187
281
|
body = notification.formatted_payload.to_json
|
188
282
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
283
|
+
@connection.async.post(path: "/3/device/#{notification.token}",
|
284
|
+
headers: headers,
|
285
|
+
body: body,
|
286
|
+
delegate: delegate,
|
287
|
+
context: context)
|
193
288
|
end
|
194
289
|
end
|
195
290
|
end
|
291
|
+
|