apns_kit 0.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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +126 -0
- data/Rakefile +6 -0
- data/apns_kit.gemspec +28 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/apns_kit.rb +36 -0
- data/lib/apns_kit/certificate.rb +33 -0
- data/lib/apns_kit/client.rb +79 -0
- data/lib/apns_kit/connection.rb +110 -0
- data/lib/apns_kit/notification.rb +69 -0
- data/lib/apns_kit/request.rb +59 -0
- data/lib/apns_kit/response.rb +70 -0
- data/lib/apns_kit/version.rb +3 -0
- metadata +146 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d2a8a7b77cfad1dc15ed5ecd77a1f94c3e573d26
|
4
|
+
data.tar.gz: e1f79c125380960ff087382fcdf258b3752bac36
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7e4efdda5e415dc9921b75ddd4133debb854c7fae033a2e796473894cee9652b195c608a80429e6cdcb92cfc201d1a601b0adf31b475c514e682116cd412ef4f
|
7
|
+
data.tar.gz: 7a8c8b7fad2b2cebfbbe6ea9e296bee7e39732d71ac81581887b24f37cb399c76b30f32014a78647c6c9f363ef6039074f4f71bab89a18dc67659ad114bf1903
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Chris Recalis
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# ApnsKit
|
2
|
+
|
3
|
+
**NOTE!** this gem is currently under development and no tests have been written yet.
|
4
|
+
|
5
|
+
A simple to use gem that interfaces with Apple's new HTTP/2 APNs Service
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'apns_kit', '~> 0.1.0'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install apns_kit
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
require 'apns_kit'
|
26
|
+
|
27
|
+
certificate = ApnsKit::Certificate.new(File.read("path_to_certificate.pem"), "password_or_nil")
|
28
|
+
|
29
|
+
# create a production client (you can also call ApnsKit::Client.development with the same options)
|
30
|
+
# pool_size is the number of open connections defaults to 1 (advisable to keep the default value)
|
31
|
+
# heartbeat_interval sends a ping to APNs servers to check if the connection is still alive defaults to 60 seconds
|
32
|
+
client = ApnsKit::Client.production(certificate, pool_size: 1, heartbeat_interval: 30)
|
33
|
+
|
34
|
+
# Build the notification
|
35
|
+
notification = ApnsKit::Notification.new (
|
36
|
+
token: "a1ee474316e40f6cfb028c6c508dd0c4e49a2855e55765586789896d0fd03e22",
|
37
|
+
alert: "Hello!",
|
38
|
+
badge: 1,
|
39
|
+
sound: "mysound.caf",
|
40
|
+
content_available: true,
|
41
|
+
data: { event_id: 1 } # data can be named to anything. Supports multiple custom keys as well
|
42
|
+
)
|
43
|
+
```
|
44
|
+
### Blocking send
|
45
|
+
This will block the calling thread until all notifications have been sent and we get a response for all
|
46
|
+
```ruby
|
47
|
+
# Can send an individual notifications or an array of them
|
48
|
+
responses = client.send(notification)
|
49
|
+
# [#<ApnsKit::Response:0x007fc0bc065520 200 (Success) notification=#<ApnsKit::Notification:0x007fc0bc0b68d0>>]
|
50
|
+
```
|
51
|
+
### Non Blocking send
|
52
|
+
This will not block the calling thread but instead use a callback for individual responses
|
53
|
+
```ruby
|
54
|
+
client.send_async(notification) do |response|
|
55
|
+
if response.success?
|
56
|
+
puts "Awesome!"
|
57
|
+
else
|
58
|
+
puts "Failed: #{response.message} reason: #{response.reason}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
### Fire and forget
|
64
|
+
You can also skip passing the block
|
65
|
+
```ruby
|
66
|
+
client.send_async(notification)
|
67
|
+
```
|
68
|
+
|
69
|
+
### Client considerations
|
70
|
+
If you do not provide a topic for a notification the client will use the app bundle id in your certificate as the topic.
|
71
|
+
|
72
|
+
Do not setup and forget about clients. If you are using short term connections you need to call `client.shutdown` to terminate the connection and the threads that it creates. If however you are using the client as a long running connection you can leave them open. If for some reason the connection is dropped the client will reinitiate the connection on your behalf.
|
73
|
+
|
74
|
+
## Logger
|
75
|
+
ApnsKit will use the Rails logger if its present. If not it creates its own logger to `STDOUT`. You can change and modify the logger however you like
|
76
|
+
```ruby
|
77
|
+
new_logger = Logger.new("some_path.log")
|
78
|
+
ApnsKit.logger = new_logger
|
79
|
+
```
|
80
|
+
|
81
|
+
# Classes
|
82
|
+
### ApnsKit::Response
|
83
|
+
```ruby
|
84
|
+
# response = <ApnsKit::Response:0x007fc0bc065520 200 (Success) notification=#<ApnsKit::Notification:0x007fc0bc0b68d0>>
|
85
|
+
response.id # returns the id of the notification
|
86
|
+
response.status # returns the http status
|
87
|
+
response.message # converts the status to a meaningful message
|
88
|
+
response.success? # convenience method checking if the status was 200
|
89
|
+
response.body # the json body of the response
|
90
|
+
response.failure_reason # convenience method to pull out the failure reason from the body
|
91
|
+
response.invalid_token? # returns true if the token was invalid
|
92
|
+
response.unregistered? # returns true if the token wasn't registered
|
93
|
+
response.bad_device_token? # returns true if the token wasn't properly formatted
|
94
|
+
response.device_token_not_for_topic? # The device token does not match the specified topic
|
95
|
+
response.notification # the ApnsKit::Notification for this response
|
96
|
+
```
|
97
|
+
|
98
|
+
### ApnsKit::Notification
|
99
|
+
```ruby
|
100
|
+
notification = ApnsKit::Notification.new (
|
101
|
+
token: "a1ee474316e40f6cfb028c6c508dd0c4e49a2855e55765586789896d0fd03e22",
|
102
|
+
alert: "Hello!",
|
103
|
+
badge: 1,
|
104
|
+
sound: "",
|
105
|
+
category: "",
|
106
|
+
expiry: 1460992609 # A UNIX epoch date expressed in seconds (UTC),
|
107
|
+
priority: 5,
|
108
|
+
content_available: true,
|
109
|
+
data: { event_id: 1 } # data can be named to anything. Supports multiple custom keys as well
|
110
|
+
)
|
111
|
+
```
|
112
|
+
### ApnsKit::Certificate
|
113
|
+
```ruby
|
114
|
+
certificate = ApnsKit::Certificate.new(File.read("path_to_certificate.pem"), "password_or_nil")
|
115
|
+
|
116
|
+
certificate.production? # returns true if the certificate can be used to connect to APNs production environment
|
117
|
+
certificate.development? # returns true if the certificate can be used to connect to APNs development environment
|
118
|
+
certificate.universal? # returns true if the certificate can be used to connect to APNs production and development environment
|
119
|
+
certificate.app_bundle_id # the app bundle id this certificate was issued for
|
120
|
+
```
|
121
|
+
## Development
|
122
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
123
|
+
|
124
|
+
## License
|
125
|
+
|
126
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/apns_kit.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'apns_kit/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "apns_kit"
|
8
|
+
spec.version = ApnsKit::VERSION
|
9
|
+
spec.authors = ["Chris Recalis"]
|
10
|
+
spec.email = ["chris@rover.io"]
|
11
|
+
|
12
|
+
spec.summary = %q{Send push notifications using Apple's new http/2 APNs service}
|
13
|
+
spec.homepage = "https://github.com/RoverPlatform/apns-kit"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "http-2", "~> 0.8.1"
|
22
|
+
spec.add_dependency "concurrent-ruby", ">= 1.0.1"
|
23
|
+
spec.add_dependency "json", "> 0"
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
28
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "apns_kit"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/apns_kit.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require "apns_kit/version"
|
2
|
+
require "apns_kit/certificate"
|
3
|
+
require "apns_kit/connection"
|
4
|
+
require "apns_kit/request"
|
5
|
+
require "apns_kit/response"
|
6
|
+
require "apns_kit/notification"
|
7
|
+
require "apns_kit/client"
|
8
|
+
require "logger"
|
9
|
+
|
10
|
+
module ApnsKit
|
11
|
+
class << self
|
12
|
+
|
13
|
+
def logger
|
14
|
+
return @logger if defined?(@logger)
|
15
|
+
@logger = rails_logger || default_logger
|
16
|
+
end
|
17
|
+
|
18
|
+
def logger=(logger)
|
19
|
+
@logger = logger
|
20
|
+
end
|
21
|
+
|
22
|
+
def default_logger
|
23
|
+
logger = Logger.new($stdout)
|
24
|
+
logger.level = Logger::INFO
|
25
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
26
|
+
"[#{datetime} ##{$$}] #{severity} -- : APNs Kit | #{msg}\n"
|
27
|
+
end
|
28
|
+
logger
|
29
|
+
end
|
30
|
+
|
31
|
+
def rails_logger
|
32
|
+
defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ApnsKit
|
2
|
+
class Certificate
|
3
|
+
|
4
|
+
def initialize(cert_data, passphrase = nil)
|
5
|
+
@key = OpenSSL::PKey::RSA.new(cert_data, passphrase)
|
6
|
+
@certificate = OpenSSL::X509::Certificate.new(cert_data)
|
7
|
+
end
|
8
|
+
|
9
|
+
def ssl_context
|
10
|
+
@ssl_context ||= OpenSSL::SSL::SSLContext.new.tap do |context|
|
11
|
+
context.key = @key
|
12
|
+
context.cert = @certificate
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def production?
|
17
|
+
extension(PRODUCTION_ENV_EXTENSION).present?
|
18
|
+
end
|
19
|
+
|
20
|
+
def development?
|
21
|
+
extension(DEVELOPMENT_ENV_EXTENSION).present?
|
22
|
+
end
|
23
|
+
|
24
|
+
def universal?
|
25
|
+
extension(UNIVERSAL_CERTIFICATE_EXTENSION).present?
|
26
|
+
end
|
27
|
+
|
28
|
+
def app_bundle_id
|
29
|
+
@app_bundle_id ||= @certificate.subject.to_a.find { |key, *_| key == "UID" }[1]
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'concurrent'
|
3
|
+
|
4
|
+
module ApnsKit
|
5
|
+
|
6
|
+
APPLE_PRODUCTION_API_URI = URI.parse("https://api.push.apple.com:443").freeze
|
7
|
+
APPLE_DEVELOPMENT_API_URI = URI.parse("https://api.development.push.apple.com:443").freeze
|
8
|
+
|
9
|
+
class Client
|
10
|
+
|
11
|
+
attr_reader :pool_size
|
12
|
+
|
13
|
+
attr_reader :connection_pool
|
14
|
+
|
15
|
+
attr_reader :default_topic
|
16
|
+
|
17
|
+
class << self
|
18
|
+
|
19
|
+
def production(certificate, pool_size: 1, heartbeat_interval: 60)
|
20
|
+
client = self.new(APPLE_PRODUCTION_API_URI, certificate, pool_size: pool_size, heartbeat_interval: heartbeat_interval)
|
21
|
+
client
|
22
|
+
end
|
23
|
+
|
24
|
+
def development(certificate, pool_size: 1, heartbeat_interval: 60)
|
25
|
+
client = self.new(APPLE_DEVELOPMENT_API_URI, certificate, pool_size: pool_size, heartbeat_interval: heartbeat_interval)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(uri, certificate, pool_size: 1, heartbeat_interval: 60)
|
31
|
+
@pool_size = pool_size
|
32
|
+
@connection_pool = @pool_size.times.map { ApnsKit::Connection.new(uri, certificate) }.freeze
|
33
|
+
@default_topic = certificate.app_bundle_id
|
34
|
+
if heartbeat_interval > 0
|
35
|
+
ApnsKit.logger.info("Setting up heartbeat checker")
|
36
|
+
@heartbeat_checker = Concurrent::TimerTask.new { @connection_pool.each(&:ping) }
|
37
|
+
@heartbeat_checker.execution_interval = heartbeat_interval
|
38
|
+
@heartbeat_checker.execute
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def shutdown
|
43
|
+
@heartbeat_checker.shutdown if @heartbeat_checker
|
44
|
+
@connection_pool.each(&:close)
|
45
|
+
return true
|
46
|
+
end
|
47
|
+
|
48
|
+
def send_async(*notifications, &block)
|
49
|
+
notifications.flatten!
|
50
|
+
notifications.each { |notification| notification.topic = default_topic if notification.topic.nil? }
|
51
|
+
request = ApnsKit::Request.new(notifications)
|
52
|
+
|
53
|
+
if block
|
54
|
+
Concurrent::Future.execute{ request.perform_nonblocking_send(connection_pool.sample, &block) }
|
55
|
+
else
|
56
|
+
Concurrent::Future.execute{ request.perform_nonblocking_send(connection_pool.sample) }
|
57
|
+
end
|
58
|
+
|
59
|
+
return true
|
60
|
+
end
|
61
|
+
|
62
|
+
def send(*notifications)
|
63
|
+
return [] if notifications.empty?
|
64
|
+
notifications.flatten!
|
65
|
+
notifications.each { |notification| notification.topic = default_topic if notification.topic.nil? }
|
66
|
+
request = ApnsKit::Request.new(notifications)
|
67
|
+
return Concurrent::Future.execute{ request.perform_blocking_send(connection_pool.sample) }.value
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
"uri=#{connection_pool.first.uri} connected=#{connection_pool.map(&:connected)} pool_size=#{pool_size}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def inspect
|
75
|
+
"#<ApnsKit::Client:#{"0x00%x" % (object_id << 1)} #{to_s}>"
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'openssl'
|
3
|
+
require 'http/2'
|
4
|
+
require 'thread'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
module ApnsKit
|
8
|
+
class Connection
|
9
|
+
|
10
|
+
attr_reader :connected
|
11
|
+
|
12
|
+
attr_reader :uri
|
13
|
+
|
14
|
+
attr_reader :http
|
15
|
+
|
16
|
+
def initialize(uri, certificate)
|
17
|
+
@uri = uri
|
18
|
+
@certificate = certificate
|
19
|
+
@connected = false
|
20
|
+
@mutex = Mutex.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def open
|
24
|
+
if !connected && (@thread.nil? || @thread.stop?)
|
25
|
+
start
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def close
|
30
|
+
shutdown if !@thread.nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
def ping
|
34
|
+
if @http
|
35
|
+
ApnsKit.logger.debug("Sending ping")
|
36
|
+
@http.ping("whatever")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def setup_connection!
|
43
|
+
@mutex.synchronize do
|
44
|
+
ApnsKit.logger.info("Setting up connection")
|
45
|
+
ctx = @certificate.ssl_context
|
46
|
+
tcp = TCPSocket.new(@uri.host, @uri.port)
|
47
|
+
|
48
|
+
@socket = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
|
49
|
+
|
50
|
+
@socket.sync_close = true
|
51
|
+
@socket.hostname = @uri.hostname
|
52
|
+
@socket.connect
|
53
|
+
|
54
|
+
@connected = true
|
55
|
+
|
56
|
+
@http = HTTP2::Client.new
|
57
|
+
@http.on(:frame) do |bytes|
|
58
|
+
ApnsKit.logger.debug("Sending bytes: #{bytes.unpack("H*").first}")
|
59
|
+
@socket.print bytes
|
60
|
+
@socket.flush
|
61
|
+
end
|
62
|
+
|
63
|
+
ping
|
64
|
+
ApnsKit.logger.info("Connection established")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def start
|
69
|
+
setup_connection!
|
70
|
+
@thread = Thread.new {
|
71
|
+
loop do
|
72
|
+
begin
|
73
|
+
if @socket.closed?
|
74
|
+
close_connection!
|
75
|
+
ApnsKit.logger.warn("Socket was closed")
|
76
|
+
break
|
77
|
+
elsif !@socket.eof?
|
78
|
+
data = @socket.readpartial(1024)
|
79
|
+
ApnsKit.logger.debug("Received bytes: #{data.unpack("H*").first}")
|
80
|
+
@http << data
|
81
|
+
end
|
82
|
+
rescue => e
|
83
|
+
close_connection!
|
84
|
+
ApnsKit.logger.warn("#{e.class} exception: #{e.message} - closing socket")
|
85
|
+
e.backtrace.each { |l| ApnsKit.logger.debug(l) }
|
86
|
+
raise e
|
87
|
+
end
|
88
|
+
end
|
89
|
+
}
|
90
|
+
return true
|
91
|
+
end
|
92
|
+
|
93
|
+
def shutdown
|
94
|
+
@thread.exit
|
95
|
+
@thread.join
|
96
|
+
close_connection!
|
97
|
+
end
|
98
|
+
|
99
|
+
def close_connection!
|
100
|
+
@mutex.synchronize do
|
101
|
+
ApnsKit.logger.info("Closing connection")
|
102
|
+
@socket.close if @socket
|
103
|
+
@connected = false
|
104
|
+
@http = nil
|
105
|
+
ApnsKit.logger.info("Connection closed")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module ApnsKit
|
4
|
+
class Notification
|
5
|
+
|
6
|
+
MAXIMUM_PAYLOAD_SIZE = 4096
|
7
|
+
|
8
|
+
attr_accessor :token
|
9
|
+
|
10
|
+
attr_accessor :id
|
11
|
+
|
12
|
+
attr_accessor :alert
|
13
|
+
|
14
|
+
attr_accessor :topic
|
15
|
+
|
16
|
+
attr_accessor :custom_data
|
17
|
+
|
18
|
+
def initialize(options)
|
19
|
+
@token = options.delete(:token) || options.delete(:device)
|
20
|
+
@alert = options.delete(:alert)
|
21
|
+
@badge = options.delete(:badge)
|
22
|
+
@sound = options.delete(:sound)
|
23
|
+
@category = options.delete(:category)
|
24
|
+
@expiry = options.delete(:expiry)
|
25
|
+
@id = options.delete(:id)
|
26
|
+
@priority = options.delete(:priority)
|
27
|
+
@content_available = options.delete(:content_available)
|
28
|
+
@topic = options.delete(:topic)
|
29
|
+
|
30
|
+
@custom_data = options
|
31
|
+
end
|
32
|
+
|
33
|
+
def id
|
34
|
+
@id ||= SecureRandom.uuid.upcase
|
35
|
+
end
|
36
|
+
|
37
|
+
def valid?
|
38
|
+
payload.bytesize <= MAXIMUM_PAYLOAD_SIZE
|
39
|
+
end
|
40
|
+
|
41
|
+
def header
|
42
|
+
json = {
|
43
|
+
':scheme' => 'https',
|
44
|
+
':method' => 'POST',
|
45
|
+
':path' => "/3/device/#{token}",
|
46
|
+
'apns-id' => id,
|
47
|
+
'content-length' => payload.bytesize.to_s,
|
48
|
+
'apns-topic' => topic
|
49
|
+
}
|
50
|
+
|
51
|
+
json.merge!({ "apns-expiry" => @expiry }) if @expiry
|
52
|
+
json.merge!({ "apns-priority" => @priority }) if @priority
|
53
|
+
return json
|
54
|
+
end
|
55
|
+
|
56
|
+
def payload
|
57
|
+
json = {}.merge(@custom_data || {}).inject({}){|h,(k,v)| h[k.to_s] = v; h}
|
58
|
+
|
59
|
+
json['aps'] ||= {}
|
60
|
+
json['aps']['alert'] = @alert if @alert
|
61
|
+
json['aps']['badge'] = @badge.to_i rescue 0 if @badge
|
62
|
+
json['aps']['sound'] = @sound if @sound
|
63
|
+
json['aps']['category'] = @category if @category
|
64
|
+
json['aps']['content-available'] = 1 if @content_available
|
65
|
+
|
66
|
+
JSON.dump(json)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'concurrent'
|
2
|
+
|
3
|
+
module ApnsKit
|
4
|
+
class Request
|
5
|
+
|
6
|
+
|
7
|
+
def initialize(notifications)
|
8
|
+
@notifications = notifications
|
9
|
+
end
|
10
|
+
|
11
|
+
def perform_blocking_send(connection)
|
12
|
+
connection.open
|
13
|
+
|
14
|
+
responses = Concurrent::Array.new
|
15
|
+
latch = Concurrent::CountDownLatch.new(@notifications.size)
|
16
|
+
|
17
|
+
perform_nonblocking_send(connection) do |response|
|
18
|
+
responses.push(response)
|
19
|
+
latch.count_down
|
20
|
+
end
|
21
|
+
|
22
|
+
latch.wait
|
23
|
+
return responses
|
24
|
+
end
|
25
|
+
|
26
|
+
def perform_nonblocking_send(connection)
|
27
|
+
connection.open
|
28
|
+
|
29
|
+
ApnsKit.logger.info("Sending #{@notifications.size} notifications")
|
30
|
+
@notifications.each do |notification|
|
31
|
+
stream = connection.http.new_stream
|
32
|
+
|
33
|
+
response = ApnsKit::Response.new
|
34
|
+
response.notification = notification
|
35
|
+
|
36
|
+
stream.on(:headers) do |headers|
|
37
|
+
headers = Hash[*headers.flatten]
|
38
|
+
response.headers = headers
|
39
|
+
ApnsKit.logger.debug("Received headers #{headers}")
|
40
|
+
if response.success?
|
41
|
+
yield response if block_given?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
stream.on(:data) do |data|
|
46
|
+
response.raw_body ||= ""
|
47
|
+
response.raw_body << data
|
48
|
+
ApnsKit.logger.debug("Received data #{data}")
|
49
|
+
yield response if block_given?
|
50
|
+
end
|
51
|
+
|
52
|
+
stream.headers(notification.header, end_stream: false)
|
53
|
+
stream.data(notification.payload)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module ApnsKit
|
4
|
+
class Response
|
5
|
+
|
6
|
+
STATUS_CODES = {
|
7
|
+
200 => "Success",
|
8
|
+
400 => "Bad request",
|
9
|
+
403 => "There was an error with the certificate",
|
10
|
+
405 => "The request used a bad :method value. Only POST requests are supported",
|
11
|
+
410 => "The device token is no longer active for the topic",
|
12
|
+
413 => "The notification payload was too large",
|
13
|
+
429 => "The server received too many requests for the same device token",
|
14
|
+
500 => "Internal server error",
|
15
|
+
503 => "The server is shutting down and unavailable",
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
INVALID_TOKEN_REASONS = Set.new(["Unregistered", "BadDeviceToken", "DeviceTokenNotForTopic"]).freeze
|
19
|
+
|
20
|
+
attr_accessor :headers, :raw_body, :notification
|
21
|
+
|
22
|
+
def id
|
23
|
+
headers["apns-id"]
|
24
|
+
end
|
25
|
+
|
26
|
+
def status
|
27
|
+
headers[":status"].to_i
|
28
|
+
end
|
29
|
+
|
30
|
+
def message
|
31
|
+
STATUS_CODES[status]
|
32
|
+
end
|
33
|
+
|
34
|
+
def success?
|
35
|
+
status == 200
|
36
|
+
end
|
37
|
+
|
38
|
+
def body
|
39
|
+
@body ||= raw_body.nil? ? {} : JSON.load(raw_body)
|
40
|
+
end
|
41
|
+
|
42
|
+
def failure_reason
|
43
|
+
body["reason"]
|
44
|
+
end
|
45
|
+
|
46
|
+
def invalid_token?
|
47
|
+
!success? && INVALID_TOKEN_REASONS.include?(failure_reason)
|
48
|
+
end
|
49
|
+
|
50
|
+
def unregistered?
|
51
|
+
!success? && failure_reason == "Unregistered"
|
52
|
+
end
|
53
|
+
|
54
|
+
def bad_device_token?
|
55
|
+
!success? && failure_reason == "BadDeviceToken"
|
56
|
+
end
|
57
|
+
|
58
|
+
def device_token_not_for_topic?
|
59
|
+
!success? && failure_reason == "DeviceTokenNotForTopic"
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_s
|
63
|
+
"#{status} (#{message}) notification=#{notification}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def inspect
|
67
|
+
"#<ApnsKit::Response:#{"0x00%x" % (object_id << 1)} #{to_s}>"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
metadata
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: apns_kit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Recalis
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-04-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: http-2
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.8.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.8.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: concurrent-ruby
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.0.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.0.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: json
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.11'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.11'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '10.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
description:
|
98
|
+
email:
|
99
|
+
- chris@rover.io
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rspec"
|
106
|
+
- ".travis.yml"
|
107
|
+
- Gemfile
|
108
|
+
- LICENSE.txt
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- apns_kit.gemspec
|
112
|
+
- bin/console
|
113
|
+
- bin/setup
|
114
|
+
- lib/apns_kit.rb
|
115
|
+
- lib/apns_kit/certificate.rb
|
116
|
+
- lib/apns_kit/client.rb
|
117
|
+
- lib/apns_kit/connection.rb
|
118
|
+
- lib/apns_kit/notification.rb
|
119
|
+
- lib/apns_kit/request.rb
|
120
|
+
- lib/apns_kit/response.rb
|
121
|
+
- lib/apns_kit/version.rb
|
122
|
+
homepage: https://github.com/RoverPlatform/apns-kit
|
123
|
+
licenses:
|
124
|
+
- MIT
|
125
|
+
metadata: {}
|
126
|
+
post_install_message:
|
127
|
+
rdoc_options: []
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
requirements: []
|
141
|
+
rubyforge_project:
|
142
|
+
rubygems_version: 2.4.8
|
143
|
+
signing_key:
|
144
|
+
specification_version: 4
|
145
|
+
summary: Send push notifications using Apple's new http/2 APNs service
|
146
|
+
test_files: []
|