lowdown 0.2.0 → 0.3.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/.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
|
+
|