banjo-apn_sender 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Kali Donovan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,124 @@
1
+ == Synopsis
2
+
3
+ Based on Kali Donovan's APN sender 1.x.
4
+
5
+ In 2.0 we keep things lean - we removed the resque layer, and make APN connection pluggable to multithreaded background worker (like SideKiq) to send Apple Push Notifications over a persistent TCP socket.
6
+
7
+ == The Story
8
+
9
+ So you're building the server component of an iPhone application in Ruby. And you want to send background notifications through the Apple Push Notification servers, which doesn't seem too bad at first. But then you read in the {Apple Documentation}[https://developer.apple.com/iphone/library/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/WhatAreRemoteNotif/WhatAreRemoteNotif.html#//apple_ref/doc/uid/TP40008194-CH102-SW7] that Apple's servers may treat non-persistent connections as a Denial of Service attack, and you realize that Rails has no easy way to maintain a persistent connection internally, and things start looking more complicated.
10
+
11
+ The apn_sender 2.0 gem is pluggable to run on delayed job workers, and processes background messages from your application and sends them along to Apple <em>over a single, persistent socket</em>.
12
+
13
+ Same as 1.x, it also includes the ability to query the Feedback service, helper methods for enqueueing your jobs.
14
+
15
+ == Yet another ApplePushNotification interface?
16
+
17
+ Yup. There's some great code out there already, but we didn't like the idea of getting banned from the APN gateway for establishing a new connection each time we needed to send a batch of messages, and none of the libraries I found handled maintaining a persistent connection.
18
+
19
+ == Usage
20
+
21
+ === 0. Setting up the pem certs
22
+
23
+ Put your <code>apn_production.pem</code> and <code>apn_production_sandbox.pem</code> certificates from Apple in your <code>RAILS_ROOT/config/certs</code> directory.
24
+
25
+ === 1. Sending Messages From Your Application
26
+
27
+ To send a message through Apple's Push Notification service from your Rails application:
28
+
29
+ APN::Connection.send(token, options, sandbox)
30
+
31
+ where,
32
+ +token+ is the unique identifier of the iPhone to receive the notification
33
+ +sandbox+ is a boolean to indicate if you are using the APN sandbox push server
34
+ +options+ can have any of the following keys:
35
+
36
+ # :alert #=> The alert to send
37
+ # :badge #=> The badge number to send
38
+ # :sound #=> The sound file to play on receipt, or true to play the default sound installed with your app
39
+ # :custom #=> Anything else that goes to the root of the message
40
+
41
+ If any other keys are present they'll be added to the root of the aps hash
42
+
43
+ === 2. Sending as Queued Message using delayed processes, ie: SideKiq as the example here
44
+
45
+ class ApnSideKiqWorker
46
+ sidekiq_options queue: :apn
47
+
48
+ def perform(token, options, sandbox = false)
49
+ APN::Connection.send(token, options, sandbox)
50
+ end
51
+ end
52
+
53
+ options = { alert: 'Hello from APN', sound: 'default.wav', badge: 10, extra: { id: 1234 } }
54
+ ApnSideKiqWorker.perform_async(device.token, options, device.build == 'development')
55
+
56
+ Once this is done, you can fire off the SideKiq background worker with
57
+
58
+ $ sidekiq -c 5 -q apn
59
+
60
+ === 3. Checking Apple's Feedback Service
61
+
62
+ Since push notifications are a fire-and-forget sorta deal, where you get no indication if your message was received (or if the specified recipient even exists), Apple needed to come up with some other way to ensure their network isn't clogged with thousands of bogus messages (e.g. from developers sending messages to phones where their application <em>used</em> to be installed, but where the user has since removed it). Hence, the Feedback Service.
63
+
64
+ It's actually really simple - you connect to them periodically and they give you a big dump of tokens you shouldn't send to anymore. The gem wraps this up nicely -- just call:
65
+
66
+ # APN::Feedback accepts the same optional :environment and :cert_path / :full_cert_path options as APN::Sender
67
+ feedback = APN::Feedback.new()
68
+
69
+ tokens = feedback.tokens # => Array of device tokens
70
+ tokens.each do |token|
71
+ # ... custom logic here to stop you app from
72
+ # sending further notifications to this token
73
+ end
74
+
75
+ If you're interested in knowing exactly <em>when</em> Apple determined each token was expired (which can be useful in determining if the application re-registered with your service since it first appeared in the expired queue):
76
+
77
+ items = feedback.data # => Array of APN::FeedbackItem elements
78
+ items.each do |item|
79
+ item.token
80
+ item.timestamp
81
+ # ... custom logic here
82
+ end
83
+
84
+ The Feedback Service works as a big queue. When you connect it pops off all its data and sends it over the wire at once, which means connecting a second time will return an empty array, so for ease of use a call to either +tokens+ or +data+ will connect once and cache the data. If you call either one again it'll continue to use its cached version (rather than connecting to Apple a second time to retrieve an empty array, which is probably not what you want).
85
+
86
+ Forcing a reconnect is as easy as calling either method with the single parameter +true+, but be sure you've already used the existing data because you'll never get it back.
87
+
88
+
89
+ ==== Warning: No really, check Apple's Feedback Service occasionally
90
+
91
+ If you're sending notifications, you should definitely call one of the <code>receive</code> methods periodically, as Apple's policies require it and they apparently monitor providers for compliance. I'd definitely recommend throwing together a quick rake task to take care of this for you (the {whenever library}[http://github.com/javan/whenever] provides a nice wrapper around scheduling tasks to run at certain times (for systems with cron enabled)).
92
+
93
+ Just for the record, this is essentially what you want to have whenever run periodically for you:
94
+
95
+ def self.clear_uninstalled_applications
96
+ feedback_data = APN::Feedback.new(:environment => :production).data
97
+
98
+ feedback_data.each do |item|
99
+ user = User.find_by_iphone_token( item.token )
100
+
101
+ if user.iphone_token_updated_at && user.iphone_token_updated_at > item.timestamp
102
+ return true # App has been reregistered since Apple determined it'd been uninstalled
103
+ else
104
+ user.update_attributes(:iphone_token => nil, :iphone_token_updated_at => Time.now)
105
+ end
106
+ end
107
+ end
108
+
109
+
110
+ == Installation
111
+
112
+ Add this to your Gemfile:
113
+
114
+ gem 'apn_sender', :require => 'apn'
115
+
116
+ Then bunndle,
117
+
118
+ $ bundle install
119
+
120
+
121
+ == Copyright
122
+
123
+ Copyright (c) 2010 Kali Donovan. See LICENSE for details.
124
+
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task default: :spec
6
+
7
+ begin
8
+ require 'jeweler'
9
+ Jeweler::Tasks.new do |gem|
10
+ gem.name = "apn_sender"
11
+ gem.summary = %Q{APN connection pluggable to multithreaded background worker (like SideKiq) to send Apple Push Notifications over a persistent TCP socket.}
12
+ gem.description = %Q{Based on Kali Donovan's APN sender 1.x. 2.0 keep things lean - we removed the resque layer, and make APN connection pluggable to multithreaded background worker (like SideKiq) to send Apple Push Notifications over a persistent TCP socket.}
13
+ gem.email = "kali.donovan@gmail.com justin@teambanjo.com"
14
+ gem.homepage = "http://github.com/BanjoInc/apn_sender"
15
+ gem.authors = ["Kali Donovan", "KW Justin Leung"]
16
+ gem.add_dependency 'yajl-ruby'
17
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.0.0
@@ -0,0 +1,50 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{banjo-apn_sender}
8
+ s.version = "2.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = [%q{Kali Donovan}, %q{KW Justin Leung}]
12
+ s.date = %q{2012-06-19}
13
+ s.description = %q{Based on Kali Donovan's APN sender 1.x. 2.0 keep things lean - we removed the resque layer, and make APN connection pluggable to multithreaded background worker (like SideKiq) to send Apple Push Notifications over a persistent TCP socket.}
14
+ s.email = %q{kali.donovan@gmail.com justin@teambanjo.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ "LICENSE",
21
+ "README.rdoc",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "apn_sender.gemspec",
25
+ "lib/apn.rb",
26
+ "lib/apn/base.rb",
27
+ "lib/apn/feedback.rb",
28
+ "lib/apn/notification.rb"
29
+ ]
30
+ s.homepage = %q{http://github.com/BanjoInc/apn_sender}
31
+ s.require_paths = [%q{lib}]
32
+ s.rubygems_version = %q{1.8.6}
33
+ s.summary = %q{APN connection pluggable to multithreaded background worker (like SideKiq) to send Apple Push Notifications over a persistent TCP socket.}
34
+
35
+ if s.respond_to? :specification_version then
36
+ s.specification_version = 3
37
+
38
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
39
+ s.add_runtime_dependency(%q<apn_sender>, [">= 0"])
40
+ s.add_runtime_dependency(%q<yajl-ruby>, [">= 0"])
41
+ else
42
+ s.add_dependency(%q<apn_sender>, [">= 0"])
43
+ s.add_dependency(%q<yajl-ruby>, [">= 0"])
44
+ end
45
+ else
46
+ s.add_dependency(%q<apn_sender>, [">= 0"])
47
+ s.add_dependency(%q<yajl-ruby>, [">= 0"])
48
+ end
49
+ end
50
+
data/lib/apn/base.rb ADDED
@@ -0,0 +1,121 @@
1
+ require 'socket'
2
+ require 'openssl'
3
+
4
+ module APN
5
+ # APN::Base takes care of all the boring certificate loading, socket creating, and logging
6
+ # responsibilities so APN::Sender and APN::Feedback and focus on their respective specialties.
7
+ module Base
8
+ attr_accessor :opts, :logger
9
+
10
+ def initialize(opts = {})
11
+ @opts = opts
12
+
13
+ setup_logger
14
+ setup_paths
15
+ log(:info, "APN::Sender with opts #{@opts}")
16
+ end
17
+
18
+ # Lazy-connect the socket once we try to access it in some way
19
+ def socket
20
+ setup_connection unless @socket
21
+ return @socket
22
+ end
23
+
24
+ protected
25
+ # Default to Rails logger, if available
26
+ def setup_logger
27
+ @logger = defined?(::Rails.logger) ? ::Rails.logger : Logger.new(STDOUT)
28
+ end
29
+
30
+ # Log message to any logger provided by the user (e.g. the Rails logger).
31
+ # Accepts +log_level+, +message+, since that seems to make the most sense,
32
+ # and just +message+, to be compatible with Resque's log method and to enable
33
+ # sending verbose and very_verbose worker messages to e.g. the rails logger.
34
+ #
35
+ # Perhaps a method definition of +message, +level+ would make more sense, but
36
+ # that's also the complete opposite of what anyone comming from rails would expect.
37
+ def log(level, message = nil)
38
+ level, message = 'info', level if message.nil? # Handle only one argument if called from Resque, which expects only message
39
+
40
+ return false unless self.logger && self.logger.respond_to?(level)
41
+ self.logger.send(level, "[#{apn_environment}] #{Time.now}: #{message}")
42
+ end
43
+
44
+ # Log the message first, to ensure it reports what went wrong if in daemon mode.
45
+ # Then die, because something went horribly wrong.
46
+ def log_and_die(msg)
47
+ log(:fatal, msg)
48
+ raise msg
49
+ end
50
+
51
+ def apn_enterprise?
52
+ @apn_enterprise ||= @opts[:enterprise].present?
53
+ end
54
+
55
+ def apn_sandbox?
56
+ @apn_sandbox ||= @opts[:sandbox].present?
57
+ end
58
+
59
+ def apn_enterprise?
60
+ @apn_enterprise ||= @opts[:enterprise].present?
61
+ end
62
+
63
+ def apn_environment
64
+ @apn_envoironment ||= (apn_sandbox? ? 'sandbox' : 'production') + (apn_enterprise? ? '_enterprise' : '')
65
+ end
66
+
67
+ # Get a fix on the .pem certificate we'll be using for SSL
68
+ def setup_paths
69
+ @opts[:environment] ||= ::Rails.env if defined?(::Rails.env)
70
+
71
+ # Accept a complete :full_cert_path allowing arbitrary certificate names, or create a default from the Rails env
72
+ cert_path = @opts[:full_cert_path] || begin
73
+ # Note that RAILS_ROOT is still here not from Rails, but to handle passing in root from sender_daemon
74
+ @opts[:root_path] ||= defined?(::Rails.root) ? ::Rails.root.to_s : (defined?(RAILS_ROOT) ? RAILS_ROOT : '/')
75
+ @opts[:cert_path] ||= File.join(File.expand_path(@opts[:root_path]), "config", "certs")
76
+ @opts[:cert_name] ||= 'apn_' + ::Rails.env + (apn_sandbox? ? '_sandbox' : '') + (apn_enterprise? ? '_enterprise' : '') + '.pem'
77
+
78
+ File.join(@opts[:cert_path], @opts[:cert_name])
79
+ end
80
+
81
+ log(:info, "APN environment=#{apn_environment}, Rails environment=#{::Rails.env}, using cert #{cert_path}")
82
+ @apn_cert = File.read(cert_path) if File.exists?(cert_path)
83
+ log_and_die("Please specify correct :full_cert_path. No apple push notification certificate found in: #{cert_path}") unless @apn_cert
84
+ end
85
+
86
+ # Open socket to Apple's servers
87
+ def setup_connection
88
+ log_and_die("Missing apple push notification certificate") unless @apn_cert
89
+ return true if @socket && @socket_tcp && !@socket.closed? && !@socket_tcp.closed?
90
+ log_and_die("Trying to open half-open connection") if (@socket && !@socket.closed?) || (@socket_tcp && !@socket_tcp.closed?)
91
+
92
+ ctx = OpenSSL::SSL::SSLContext.new
93
+ ctx.cert = OpenSSL::X509::Certificate.new(@apn_cert)
94
+ ctx.key = OpenSSL::PKey::RSA.new(@apn_cert)
95
+
96
+ @socket_tcp = TCPSocket.new(apn_host, apn_port)
97
+ @socket = OpenSSL::SSL::SSLSocket.new(@socket_tcp, ctx)
98
+ @socket.sync = true
99
+ @socket.connect
100
+ rescue SocketError => error
101
+ log_and_die("Error with connection to #{apn_host}: #{error}")
102
+ end
103
+
104
+ # Close open sockets
105
+ def teardown_connection
106
+ log(:info, "Closing connections...") if @opts[:verbose]
107
+
108
+ begin
109
+ @socket.close if @socket
110
+ rescue Exception => e
111
+ log(:error, "Error closing SSL Socket: #{e}")
112
+ end
113
+
114
+ begin
115
+ @socket_tcp.close if @socket_tcp && !@socket_tcp.closed?
116
+ rescue Exception => e
117
+ log(:error, "Error closing TCP Socket: #{e}")
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,91 @@
1
+ require 'apn/base'
2
+
3
+ module APN
4
+ # Encapsulates data returned from the {APN Feedback Service}[http://developer.apple.com/iphone/library/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3].
5
+ # Possesses +timestamp+ and +token+ attributes.
6
+ class FeedbackItem
7
+ attr_accessor :timestamp, :token
8
+
9
+ def initialize(time, token)
10
+ @timestamp = time
11
+ @token = token
12
+ end
13
+
14
+ # For convenience, return the token on to_s
15
+ def to_s
16
+ token
17
+ end
18
+ end
19
+
20
+ # When supplied with the certificate path and the desired environment, connects to the {APN Feedback Service}[http://developer.apple.com/iphone/library/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3]
21
+ # and returns any response as an array of APN::FeedbackItem elements.
22
+ #
23
+ # See README for usage and details.
24
+ class Feedback
25
+ include APN::Base
26
+
27
+ # Returns array of APN::FeedbackItem elements read from Apple. Connects to Apple once and caches the
28
+ # data, continues to returns cached data unless called with <code>data(true)</code>, which clears the
29
+ # existing feedback array. Note that once you force resetting the cache you loose all previous feedback,
30
+ # so be sure you've already processed it.
31
+ def data(force = nil)
32
+ @feedback = nil if force
33
+ @feedback ||= receive
34
+ end
35
+
36
+ # Wrapper around +data+ returning just an array of token strings.
37
+ def tokens(force = nil)
38
+ data(force).map(&:token)
39
+ end
40
+
41
+ # Prettify to return meaningful status information when printed. Can't add these directly to connection/base, because Resque depends on decoding to_s
42
+ def inspect
43
+ "#<#{self.class.name}: #{to_s}>"
44
+ end
45
+
46
+ # Prettify to return meaningful status information when printed. Can't add these directly to connection/base, because Resque depends on decoding to_s
47
+ def to_s
48
+ "#{@socket ? 'Connected' : 'Connection not currently established'} to #{apn_host} on #{apn_port}"
49
+ end
50
+
51
+ protected
52
+
53
+ # Connects to Apple's Feedback Service and checks if there's anything there for us.
54
+ # Returns an array of APN::FeedbackItem pairs
55
+ def receive
56
+ feedback = []
57
+
58
+ # Hi Apple
59
+ setup_connection
60
+
61
+ # Unpacking code borrowed from http://github.com/jpoz/APNS/blob/master/lib/apns/core.rb
62
+ while bunch = socket.read(38) # Read data from the socket
63
+ f = bunch.strip.unpack('N1n1H140')
64
+ feedback << APN::FeedbackItem.new(Time.at(f[0]), f[2])
65
+ end
66
+
67
+ # Bye Apple
68
+ teardown_connection
69
+
70
+ return feedback
71
+ end
72
+
73
+
74
+ def apn_host
75
+ @apn_host ||= apn_sandbox? ? "feedback.sandbox.push.apple.com" : "feedback.push.apple.com"
76
+ end
77
+
78
+ def apn_port
79
+ 2196
80
+ end
81
+
82
+ end
83
+ end
84
+
85
+
86
+
87
+ __END__
88
+ # Testing from irb
89
+ irb -r 'lib/apn/feedback'
90
+
91
+ a=APN::Feedback.new(:cert_path => '/Users/kali/Code/insurrection/certs/', :environment => :production)
@@ -0,0 +1,95 @@
1
+ module APN
2
+ # Encapsulates the logic necessary to convert an iPhone token and an array of options into a string of the format required
3
+ # by Apple's servers to send the notification. Much of the processing code here copied with many thanks from
4
+ # http://github.com/samsoffes/apple_push_notification/blob/master/lib/apple_push_notification.rb
5
+ #
6
+ # APN::Notification.new's first argument is the token of the iPhone which should receive the notification. The second argument
7
+ # is a hash with any of :alert, :badge, and :sound keys. All three accept string arguments, while :sound can also be set to +true+
8
+ # to play the default sound installed with the application. At least one of these keys must exist. Any other keys are merged into
9
+ # the root of the hash payload ultimately sent to the iPhone:
10
+ #
11
+ # APN::Notification.new(token, {:alert => 'Stuff', :custom => {:code => 23}})
12
+ # # Writes this JSON to servers: {"aps" => {"alert" => "Stuff"}, "custom" => {"code" => 23}}
13
+ #
14
+ # As a shortcut, APN::Notification.new also accepts a string as the second argument, which it converts into the alert to send. The
15
+ # following two lines are equivalent:
16
+ #
17
+ # APN::Notification.new(token, 'Some Alert')
18
+ # APN::Notification.new(token, {:alert => 'Some Alert'})
19
+ #
20
+ class Notification
21
+ # Available to help clients determine before they create the notification if their message will be too large.
22
+ # Each iPhone Notification payload must be 256 or fewer characters. Encoding a null message has a 57
23
+ # character overhead, so there are 199 characters available for the alert string.
24
+ MAX_ALERT_LENGTH = 199
25
+
26
+ attr_accessor :options, :token
27
+ def initialize(token, opts)
28
+ @options = hash_as_symbols(opts.is_a?(Hash) ? opts : {:alert => opts})
29
+ @token = token
30
+ payload_size = packaged_message.bytesize.to_i
31
+
32
+ raise "Payload bytesize of #{payload_size} is > the maximum allowed size of 255." if payload_size > 255
33
+ end
34
+
35
+ def to_s
36
+ packaged_notification
37
+ end
38
+
39
+ # Ensures at least one of <code>%w(alert badge sound)</code> is present
40
+ def valid?
41
+ return true if %w(alert badge sound).any?{|key| options.keys.include?(key.to_sym) }
42
+ false
43
+ end
44
+
45
+ # Converts the supplied options into the JSON needed for Apple's push notification servers.
46
+ # Extracts :alert, :badge, :sound, 'aps' hash, merges 'custom' hash data
47
+ # into the root of the hash to encode and send to apple.
48
+ def packaged_message
49
+ self.class.packaged_message(@options)
50
+ end
51
+
52
+ def self.packaged_message(options)
53
+ raise "Message #{options} is missing the alert or badge keys." unless options[:badge] || options[:alert]
54
+
55
+ opts = options.clone # Don't destroy our pristine copy
56
+ aps_hash = {}
57
+
58
+ if sound = opts.delete(:sound)
59
+ aps_hash['sound'] = sound.is_a?(TrueClass) ? 'default' : sound.to_s
60
+ end
61
+
62
+ hsh = { 'aps' => aps_hash }
63
+ hsh.merge!(opts.delete(:custom) || {})
64
+ hsh['aps'].merge!(opts)
65
+
66
+ Yajl::Encoder.encode(hsh)
67
+ end
68
+
69
+ protected
70
+ # Completed encoded notification, ready to send down the wire to Apple
71
+ def packaged_notification
72
+ pt = packaged_token
73
+ pm = packaged_message
74
+ [0, 0, 32, pt, 0, pm.bytesize, pm].pack("ccca*cca*")
75
+ end
76
+
77
+ # Device token, compressed and hex-ified
78
+ def packaged_token
79
+ [@token.gsub(/[\s|<|>]/,'')].pack('H*')
80
+ end
81
+
82
+ # Symbolize keys, using ActiveSupport if available
83
+ def hash_as_symbols(hash)
84
+ if hash.respond_to?(:symbolize_keys)
85
+ return hash.symbolize_keys
86
+ else
87
+ hash.inject({}) do |opt, (key, value)|
88
+ opt[(key.to_sym rescue key) || key] = value
89
+ opt
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
data/lib/apn.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'yajl'
2
+ require 'json'
3
+ require 'active_support'
4
+ require 'apn/notification'
5
+ require 'apn/base'
6
+ require 'apn/connection'
7
+ require 'apn/feedback'
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: banjo-apn_sender
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kali Donovan
9
+ - KW Justin Leung
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-06-19 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: apn_sender
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: yajl-ruby
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ description: Based on Kali Donovan's APN sender 1.x. 2.0 keep things lean - we removed
48
+ the resque layer, and make APN connection pluggable to multithreaded background
49
+ worker (like SideKiq) to send Apple Push Notifications over a persistent TCP socket.
50
+ email: kali.donovan@gmail.com justin@teambanjo.com
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files:
54
+ - LICENSE
55
+ - README.rdoc
56
+ files:
57
+ - LICENSE
58
+ - README.rdoc
59
+ - Rakefile
60
+ - VERSION
61
+ - apn_sender.gemspec
62
+ - lib/apn.rb
63
+ - lib/apn/base.rb
64
+ - lib/apn/feedback.rb
65
+ - lib/apn/notification.rb
66
+ homepage: http://github.com/BanjoInc/apn_sender
67
+ licenses: []
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 1.8.23
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: APN connection pluggable to multithreaded background worker (like SideKiq)
90
+ to send Apple Push Notifications over a persistent TCP socket.
91
+ test_files: []