├── .gitignore ├── .rspec ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── apns-persistent.gemspec ├── bin ├── console └── setup ├── exe ├── push_noti ├── push_noti_daemon └── push_noti_once ├── lib └── apns │ ├── persistent.rb │ └── persistent │ ├── client.rb │ ├── connection.rb │ ├── feedback_client.rb │ ├── push_client.rb │ └── version.rb └── spec ├── apns └── persistent_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.2 4 | before_install: gem install bundler -v 1.10.6 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in apns-persistent.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Koji Murata (https://github.com/malt03) 4 | Copyright (c) 2012–2015 Mattt Thompson (http://mattt.me/) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apns::Persistent 2 | 3 | apns-persisitent is referencing [houston](https://rubygems.org/gems/houston/) 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'apns-persistent' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install apns-persistent 20 | 21 | ## Push Usage 22 | [Examples](https://github.com/malt03/apns-persistent/tree/master/exe) 23 | 24 | ### Recommend 25 | ```ruby 26 | c = Apns::Persistent::PushClient.new(certificate: '/path/to/apple_push_notification.pem', sandbox: true) 27 | c.open 28 | 29 | thread = c.register_error_handle do |command, status, id| 30 | #error handle 31 | puts "Send Error! command:#{command} status:#{status} id:#{id}" 32 | end 33 | 34 | c.push(token: '88189fcf 62a1b2eb b7cb1435 597e734e a90da4ce 6196a9b3 309a5421 4c6259e', 35 | alert: 'foobar', 36 | sound: 'default', 37 | id: 1) 38 | 39 | begin 40 | thread.join 41 | rescue Interrupt 42 | c.close 43 | end 44 | ``` 45 | 46 | ### Without Thread 47 | ```ruby 48 | c = Apns::Persistent::PushClient.new(certificate: '/path/to/apple_push_notification.pem', sandbox: true) 49 | c.open 50 | c.push(token: '88189fcf 62a1b2eb b7cb1435 597e734e a90da4ce 6196a9b3 309a5421 4c6259e', 51 | alert: 'foobar', 52 | sound: 'default', 53 | id: 1) do |command, status, id| 54 | #error handle 55 | puts "Send Error! command:#{command} status:#{status} id:#{id}" 56 | end 57 | c.close 58 | ``` 59 | 60 | ### Without Persistent Connections 61 | ```ruby 62 | Apns::Persistent::PushClient.push_once(certificate: '/path/to/apple_push_notification.pem', 63 | token: '88189fcf 62a1b2eb b7cb1435 597e734e a90da4ce 6196a9b3 309a5421 4c6259e9', 64 | alert: 'foobar', 65 | sound: 'default', 66 | id: 1) do |command, status, id| 67 | #error handle 68 | puts "Send Error! command:#{command} status:#{status} id:#{id}" 69 | end 70 | ``` 71 | 72 | ## Feedback API Usage 73 | ### Get unregistered devices 74 | ```ruby 75 | c = Apns::Persistent::FeedbackClient.new(certificate: '/path/to/apple_push_notification.pem', sandbox: true) 76 | c.open 77 | devices = c.unregistered_devices 78 | c.close 79 | ``` 80 | 81 | ### Get unregistered device tokens 82 | ```ruby 83 | c = Apns::Persistent::FeedbackClient.new(certificate: '/path/to/apple_push_notification.pem', sandbox: true) 84 | c.open 85 | device_tokens = c.unregistered_device_tokens 86 | c.close 87 | ``` 88 | 89 | ### Get unregistered devices once 90 | ```ruby 91 | devices = Apns::Persistent::FeedbackClient.unregistered_devices_once(certificate: '/path/to/apple_push_notification.pem', sandbox: true) 92 | ``` 93 | 94 | ### Get unregistered device tokens once 95 | ```ruby 96 | device_tokens = Apns::Persistent::FeedbackClient.unregistered_device_tokens_once(certificate: '/path/to/apple_push_notification.pem', sandbox: true) 97 | ``` 98 | 99 | ## Command Line Tools 100 | ### Launch Daemon 101 | ```console 102 | $ push_daemon --pemfile [--sandbox] 103 | ``` 104 | ### Push after launch daemon 105 | ```console 106 | $ push --token --alert "Hello" --badge 2 --sound "default" 107 | ``` 108 | 109 | ### Push once 110 | ```console 111 | $ push_once --pemfile [--sandbox] --token --alert "Hello" --sound "default" --badge 2 112 | ``` 113 | 114 | ## Contributing 115 | 116 | Bug reports and pull requests are welcome on GitHub at https://github.com/malt03/apns-persistent. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct. 117 | 118 | 119 | ## License 120 | 121 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 122 | 123 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /apns-persistent.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'apns/persistent/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "apns-persistent" 8 | spec.version = Apns::Persistent::VERSION 9 | spec.authors = ["malt03"] 10 | spec.email = ["malt.koji@gmail.com"] 11 | 12 | spec.summary = "Send Apple Push Notifications" 13 | spec.description = "apns-persistent is a gem for sending APNs and easy to manage connections. " 14 | spec.homepage = "https://github.com/malt03/apns-persistent" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.required_ruby_version = '>= 2.1.0' 23 | 24 | spec.add_development_dependency "bundler" 25 | spec.add_development_dependency "rake" 26 | spec.add_development_dependency "rspec" 27 | end 28 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "apns/persistent" 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 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /exe/push_noti: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'yaml' 5 | require 'socket' 6 | require 'optparse' 7 | 8 | require 'apns/persistent' 9 | 10 | params = Hash[ARGV.getopts('', 11 | 'help', 12 | 'host:', 13 | 'port:', 14 | 'token:', 15 | 'alert:', 16 | 'badge:', 17 | 'sound:', 18 | 'category:', 19 | 'content_available', 20 | 'payload_yaml:', 21 | 'id:', 22 | 'expiry:', 23 | 'priority:').map { |k, v| [k.to_sym, v] }] 24 | 25 | if params.delete(:help) 26 | puts 'Usage: push [--host host] [--port port] [--token string] [--alert string] [--badge num] [--sound string] [--category string] [--content_available] [--payload_yaml path] [--id num] [--expiry time] [--priority num]' 27 | exit 28 | end 29 | 30 | params[:id] = params[:id].to_i if params[:id] 31 | params[:priority] = params[:priority].to_i if params[:priority] 32 | params.delete(:payload_yaml).tap do |file| 33 | params[:custom_payload] = YAML.load(File.read(file)) if file 34 | end 35 | 36 | sock = TCPSocket.open(params.delete(:host) || 'localhost', params.delete(:port) || 20000) 37 | yml = params.to_yaml 38 | sock.write([yml.length].pack('I')) 39 | sock.write(yml) 40 | puts sock.gets 41 | sock.close 42 | -------------------------------------------------------------------------------- /exe/push_noti_daemon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'yaml' 5 | require 'socket' 6 | require 'optparse' 7 | 8 | require 'apns/persistent' 9 | 10 | def ready(port) 11 | puts "\e[32mReady! port=#{port}\e[0m" 12 | end 13 | 14 | params = Hash[ARGV.getopts('', 'help', 'port:', 'pemfile:', 'passphrase:', 'sandbox').map { |k, v| [k.to_sym, v] }] 15 | 16 | if params[:help] 17 | puts 'Usage: push_daemon [--port port] --pemfile path [--passphrase string] [--sandbox]' 18 | exit 19 | end 20 | 21 | unless params[:pemfile] 22 | puts 'Missing filename (" --help" for help)' 23 | exit 24 | end 25 | unless File.file?(params[:pemfile]) 26 | puts "#{params[:pemfile]}: No such file or directory" 27 | exit 28 | end 29 | 30 | port = params[:port] || 20000 31 | 32 | client = Apns::Persistent::PushClient.new(certificate: params[:pemfile], passphrase: params[:passphrase], sandbox: params[:sandbox]) 33 | client.open 34 | thread = client.register_error_handle do |command, status, id| 35 | puts "\e[31mSend Error! command:#{command} status:#{status} id:#{id}\e[0m" 36 | ready(port) 37 | end 38 | 39 | gate = TCPServer.open port 40 | 41 | begin 42 | loop do 43 | ready(port) 44 | sock = gate.accept 45 | 46 | length = sock.read(4).unpack('I')[0] 47 | data = YAML.load(sock.read(length)) 48 | 49 | client.push(data) 50 | puts "Sent! id:#{data[:id] || 0}" 51 | sock.write("OK\n") 52 | sock.close 53 | end 54 | rescue Interrupt 55 | client.close 56 | gate.close 57 | end 58 | -------------------------------------------------------------------------------- /exe/push_noti_once: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'yaml' 5 | require 'optparse' 6 | 7 | require 'apns/persistent' 8 | 9 | params = Hash[ARGV.getopts('', 10 | 'help', 11 | 'pemfile:', 12 | 'passphrase:', 13 | 'sandbox', 14 | 'token:', 15 | 'alert:', 16 | 'badge:', 17 | 'sound:', 18 | 'category:', 19 | 'content_available', 20 | 'payload_yaml:', 21 | 'id:', 22 | 'expiry:', 23 | 'priority:').map { |k, v| [k.to_sym, v] }] 24 | 25 | if params.delete(:help) 26 | puts 'Usage: push_once [--pemfile path] [--passphrase string] [--sandbox] [--token string] [--alert string] [--badge num] [--sound string] [--category string] [--content_available] [--payload_yaml path] [--id num] [--expiry time] [--priority num]' 27 | exit 28 | end 29 | 30 | unless params[:pemfile] 31 | puts 'Missing filename (" --help" for help)' 32 | exit 33 | end 34 | unless File.file?(params[:pemfile]) 35 | puts "#{params[:pemfile]}: No such file or directory" 36 | exit 37 | end 38 | 39 | params[:certificate] = params.delete(:pemfile) 40 | params[:id] = params[:id].to_i if params[:id] 41 | params[:priority] = params[:priority].to_i if params[:priority] 42 | params.delete(:payload_yaml).tap do |file| 43 | params[:custom_payload] = YAML.load(File.read(file)) if file 44 | end 45 | 46 | Apns::Persistent::PushClient.push_once(params) do |command, status, id| 47 | puts "\e[31mSend Error! command:#{command} status:#{status} id:#{id}\e[0m" 48 | end 49 | -------------------------------------------------------------------------------- /lib/apns/persistent.rb: -------------------------------------------------------------------------------- 1 | require "apns/persistent/client" 2 | require "apns/persistent/push_client" 3 | require "apns/persistent/feedback_client" 4 | require "apns/persistent/connection" 5 | require "apns/persistent/version" 6 | 7 | # module Apns 8 | # module Persistent 9 | # # Your code goes here... 10 | # end 11 | # end 12 | -------------------------------------------------------------------------------- /lib/apns/persistent/client.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | require 'json' 3 | 4 | module Apns 5 | module Persistent 6 | class Client 7 | extend Forwardable 8 | def_delegators :@connection, :open, :close, :opened?, :closed? 9 | 10 | def initialize(certificate: , passphrase: nil, sandbox: false) 11 | cer = File.read(certificate) 12 | @connection = Connection.new(self.class.gateway_uri(sandbox), cer, passphrase) 13 | end 14 | 15 | def self.gateway_uri(sandbox) 16 | raise 'please inherit' 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/apns/persistent/connection.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'socket' 3 | require 'openssl' 4 | require 'forwardable' 5 | 6 | module Apns 7 | module Persistent 8 | class Connection 9 | extend Forwardable 10 | def_delegators :@ssl, :read, :write 11 | 12 | def initialize(uri, certificate, passphrase) 13 | @uri = URI(uri) 14 | @certificate = certificate 15 | @passphrase = passphrase 16 | end 17 | 18 | def open 19 | @socket = TCPSocket.new(@uri.host, @uri.port) 20 | context = OpenSSL::SSL::SSLContext.new 21 | context.key = OpenSSL::PKey::RSA.new(@certificate, @passphrase) 22 | context.cert = OpenSSL::X509::Certificate.new(@certificate) 23 | @ssl = OpenSSL::SSL::SSLSocket.new(@socket, context) 24 | @ssl.sync 25 | @ssl.connect 26 | end 27 | 28 | def close 29 | @ssl.close 30 | @socket.close 31 | @ssl = nil 32 | @socket = nil 33 | end 34 | 35 | def reopen 36 | close 37 | open 38 | end 39 | 40 | def opened? 41 | !(@ssl.nil? || @socket.nil?) 42 | end 43 | 44 | def closed? 45 | !opened? 46 | end 47 | 48 | def readable? 49 | r, w = IO.select([@ssl], [], [@ssl], 1) 50 | (r && r[0]) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/apns/persistent/feedback_client.rb: -------------------------------------------------------------------------------- 1 | module Apns 2 | module Persistent 3 | class FeedbackClient < Client 4 | def self.unregistered_devices_once(certificate: , sandbox: true) 5 | client = Apns::Persistent::FeedbackClient.new(certificate: certificate, sandbox: sandbox) 6 | client.open 7 | devices = client.unregistered_devices 8 | client.close 9 | devices 10 | end 11 | 12 | def self.unregistered_device_tokens_once(certificate: , sandbox: true) 13 | FeedbackClient.unregistered_devices_once(certificate: certificate, sandbox: sandbox).collect { |device| device[:token] } 14 | end 15 | 16 | def unregistered_devices 17 | raise 'please open' if closed? 18 | 19 | devices = [] 20 | while line = @connection.read(38) 21 | feedback = line.unpack('N1n1H140') 22 | timestamp = feedback[0] 23 | token = feedback[2].scan(/.{0,8}/).join(' ').strip 24 | devices << {token: token, timestamp: timestamp} if token && timestamp 25 | end 26 | devices 27 | end 28 | 29 | def unregistered_device_tokens 30 | unregistered_devices.collect { |device| device[:token] } 31 | end 32 | 33 | def self.gateway_uri(sandbox) 34 | sandbox ? "apn://feedback.sandbox.push.apple.com:2196" : "apn://feedback.push.apple.com:2196" 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/apns/persistent/push_client.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module Apns 4 | module Persistent 5 | class PushClient < Client 6 | def self.push_once(certificate: , 7 | passphrase: nil, 8 | sandbox: true, 9 | token: , 10 | alert: nil, 11 | badge: nil, 12 | sound: nil, 13 | category: nil, 14 | content_available: true, 15 | mutable_content: false, 16 | custom_payload: nil, 17 | id: nil, 18 | expiry: nil, 19 | priority: nil) 20 | 21 | client = PushClient.new(certificate: certificate, passphrase: passphrase, sandbox: sandbox) 22 | client.open 23 | 24 | client.push(token: token, 25 | alert: alert, 26 | badge: badge, 27 | sound: sound, 28 | category: category, 29 | content_available: content_available, 30 | mutable_content: mutable_content, 31 | custom_payload: custom_payload, 32 | id: id, 33 | expiry: expiry, 34 | priority: priority) do |command, status, return_id| 35 | if block_given? 36 | yield(command, status, return_id) 37 | end 38 | end 39 | 40 | client.close 41 | end 42 | 43 | def push(token: , 44 | alert: nil, 45 | badge: nil, 46 | sound: nil, 47 | category: nil, 48 | content_available: true, 49 | mutable_content: false, 50 | custom_payload: nil, 51 | id: nil, 52 | expiry: nil, 53 | priority: nil) 54 | 55 | raise 'please open' if closed? 56 | 57 | m = PushClient.message(token, alert, badge, sound, category, content_available, mutable_content, custom_payload, id, expiry, priority) 58 | @connection.write(m) 59 | 60 | if block_given? && @connection.readable? 61 | if error = @connection.read(6) 62 | command, status, return_id = error.unpack('ccN') 63 | yield(command, status, return_id) 64 | @connection.reopen 65 | end 66 | end 67 | end 68 | 69 | def register_error_handle 70 | Thread.new do 71 | while error = @connection.read(6) 72 | command, status, id = error.unpack('ccN') 73 | yield(command, status, id) 74 | @connection.reopen 75 | end 76 | end 77 | end 78 | 79 | class << self 80 | def gateway_uri(sandbox) 81 | sandbox ? "apn://gateway.sandbox.push.apple.com:2195" : "apn://gateway.push.apple.com:2195" 82 | end 83 | 84 | def message(token, alert, badge, sound, category, content_available, mutable_content, custom_payload, id, expiry, priority) 85 | data = [token_data(token), 86 | payload_data(custom_payload, alert, badge, sound, category, content_available, mutable_content), 87 | id_data(id), 88 | expiration_data(expiry), 89 | priority_data(priority)].compact.join 90 | [2, data.bytes.count, data].pack('cNa*') 91 | end 92 | 93 | private 94 | 95 | def token_data(token) 96 | [1, 32, token.gsub(/[<\s>]/, '')].pack('cnH64') 97 | end 98 | 99 | def payload_data(custom_payload, alert, badge, sound, category, content_available, mutable_content) 100 | payload = {}.merge(custom_payload || {}).inject({}){|h,(k,v)| h[k.to_s] = v; h} 101 | 102 | payload['aps'] ||= {} 103 | payload['aps']['alert'] = alert if alert 104 | payload['aps']['badge'] = badge.to_i rescue 0 if badge 105 | payload['aps']['sound'] = sound if sound 106 | payload['aps']['category'] = category if category 107 | payload['aps']['content-available'] = 1 if content_available 108 | payload['aps']['mutable-content'] = 1 if mutable_content 109 | 110 | json = payload.to_json 111 | [2, json.bytes.count, json].pack('cna*') 112 | end 113 | 114 | def id_data(id) 115 | [3, 4, id].pack('cnN') unless id.nil? 116 | end 117 | 118 | def expiration_data(expiry) 119 | [4, 4, expiry.to_i].pack('cnN') unless expiry.nil? 120 | end 121 | 122 | def priority_data(priority) 123 | [5, 1, priority].pack('cnc') unless priority.nil? 124 | end 125 | end 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/apns/persistent/version.rb: -------------------------------------------------------------------------------- 1 | module Apns 2 | module Persistent 3 | VERSION = "1.0.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/apns/persistent_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Apns::Persistent do 4 | it 'has a version number' do 5 | expect(Apns::Persistent::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'apns/persistent' 3 | --------------------------------------------------------------------------------