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 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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.0
4
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in apns_kit.gemspec
4
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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
@@ -0,0 +1,3 @@
1
+ module ApnsKit
2
+ VERSION = "0.1.0"
3
+ 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: []