Permalink
Browse files

Core: refactor + feedback host & port (Thanks to Will Prater for insp…

…iration). Payload: added truncation of alert or custom field + strip of message strings (Also thanks to Will Prater for inspiration). Specs: updated for the new features. README: Updated
  • Loading branch information...
1 parent 9554bfd commit 99141b534997ea662b5631b21f28d7943decb119 @Orion98MC committed Feb 23, 2011
Showing with 269 additions and 46 deletions.
  1. +2 −1 MIT-LICENSE
  2. +48 −2 README.textile
  3. +42 −33 lib/apns/core.rb
  4. +85 −8 lib/apns/payload.rb
  5. +20 −2 spec/apns/core_spec.rb
  6. +72 −0 spec/apns/payload_spec.rb
View
@@ -1,4 +1,5 @@
Copyright (c) 2009 James Pozdena
+Copyright (c) 2010-2011 Thierry Passeron
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
@@ -19,4 +20,4 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
+OTHER DEALINGS IN THE SOFTWARE.
View
@@ -66,9 +66,11 @@ You can also send multiple payloads at once
p1 = APNS::Payload.new(device_token, 'Hello iPhone!' )
- n2 = APNS::Payload.new(device_token, :alert => 'Hello iPhone!', :badge => 1, :sound => 'default')
+ p2 = APNS::Payload.new(device_token, :alert => 'Hello iPhone!', :badge => 1, :sound => 'default')
+
+ p3 = APNS::Payload.new(device_token).alert("Hello from APNS").badge(2).sound("bipbip.aiff")
- APNS.send_payloads([p1, p2])
+ APNS.send_payloads([p1, p2, p3])
</code>
</pre>
@@ -91,6 +93,50 @@ This will add the :sent key to the same level as the "aps" key:
</code>
</pre>
+h2. Truncating payload informations
+
+Only valid Payloads will be sent to Apple. A valid payload has a size lesser or equal to 256 bytes.
+You can check whether a payload is valid with the Payload#valid? method.
+In case you know a payload message field is too large and wish to have it truncated you can either use Payload#payload_with_truncated_alert or a more generic Payload#payload_with_truncated_string_at_keypath. These two method will try to truncate the value of the alert field or any custom field at a keypath.
+
+Truncate the alert field:
+
+<pre>
+ <code>
+ p = APNS::Payload.new("a-device-token", "A very long message "*15)
+ => #<APNS::Payload:0x103192ba8 @device=#<APNS::Device:0x103192298 @token="a-device-token">, @message={:alert=>"A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message"}>
+ p.valid?
+ => false
+ p.size
+ => 331
+ p.payload_with_truncated_alert
+ => #<APNS::Payload:0x1031d9468 @device=#<APNS::Device:0x1031d91e8 @token="a-device-token">, @message={:alert=>"A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A..."}>
+ p.payload_with_truncated_alert.size
+ => 256
+ p.payload_with_truncated_alert.valid?
+ => true
+ </code>
+</pre>
+
+Truncate a custom field:
+
+<pre>
+ <code>
+ p = APNS::Payload.new("a-device-token", :alert => "Hello from APNS", :custom => {:foo => "Bar "*80})
+ => #<APNS::Payload:0x10331cb90 @device=#<APNS::Device:0x103481530 @token="a-device-token">, @message={:alert=>"Hello from APNS", :custom=>{:foo=>"Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar "}}>
+ p.valid?
+ => false
+ p.size
+ => 387
+ p.payload_with_truncated_string_at_keypath "custom.foo"
+ => #<APNS::Payload:0x1031cbb88 @device=#<APNS::Device:0x1031cb908 @token="a-device-token">, @message={:alert=>"Hello from APNS", :custom=>{:foo=>"Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Ba..."}}>
+ p.payload_with_truncated_string_at_keypath("custom.foo").size
+ => 256
+ p.payload_with_truncated_string_at_keypath("custom.foo").valid?
+ => true
+ </code>
+</pre>
+
h2. Getting your iPhone's device token
View
@@ -6,18 +6,36 @@ module APNS
class PemPathError < RuntimeError;end
class PemFileError < RuntimeError;end
+ ## Host for push notification service
+ #
+ # production: gateway.push.apple.com
+ # development: gateway.sandbox.apple.com
+ #
+ # You may set the correct host with:
+ # APNS.host = <host> or use the default one
@host = 'gateway.sandbox.push.apple.com'
@port = 2195
+
+ ## Host for feedback service
+ #
+ # production: feedback.push.apple.com
+ # development: feedback.sandbox.apple.com
+ #
+ # You may set the correct feedback host with:
+ # APNS.feedback_host = <host> or use the default one
+ @feedback_host = @host.gsub('gateway','feedback')
+ @feedback_port = 2196
+
# openssl pkcs12 -in mycert.p12 -out client-cert.pem -nodes -clcerts
- @pem = nil # this should be the path of the pem file not the contentes
+ @pem = nil # this should be the path of the pem file not the contents
@pass = nil
# Persistent connection
@@ssl = nil
@@sock = nil
class << self
- attr_accessor :host, :pem, :port, :pass
+ attr_accessor :host, :port, :feedback_host, :feedback_port, :pem, :pass
end
# send one or many payloads
@@ -36,32 +54,29 @@ class << self
#
# or with multiple payloads
# APNS.send_payloads([payload1, payload2])
-
+
def self.send_payloads(payloads)
# accept Array or single payload
payloads = payloads.is_a?(Array) ? payloads : [payloads]
- return if (payloads.nil? || payloads.count == 0)
-
- # try to connect
- if @@ssl.nil?
- @@sock, @@ssl = self.push_connection
- end
-
# retain valid payloads only
payloads.reject!{ |p| !(p.is_a?(APNS::Payload) && p.valid?) }
-
- # loop
+
+ return if (payloads.nil? || payloads.count < 1)
+
+ # loop through each payloads
payloads.each do |payload|
retry_delay = 2
begin
if @@ssl.nil?
@@sock, @@ssl = self.push_connection
end
@@ssl.write(payload.to_ssl); @@ssl.flush
+ rescue PemPathError, PemFileError => e
+ raise e
rescue
@@ssl.close; @@sock.close
- @@ssl = nil; @@sock = nil
+ @@ssl = nil; @@sock = nil # cleanup
retry_delay *= 2
if retry_delay <= 8
@@ -95,38 +110,32 @@ def self.feedback
protected
-
- def self.push_connection
+
+ def self.ssl_context
raise PemPathError, "The path to your pem file is not set. (APNS.pem = /path/to/cert.pem)" unless self.pem
raise PemFileError, "The path to your pem file does not exist!" unless File.exist?(self.pem)
context = OpenSSL::SSL::SSLContext.new
context.cert = OpenSSL::X509::Certificate.new(File.read(self.pem))
context.key = OpenSSL::PKey::RSA.new(File.read(self.pem), self.pass)
-
- sock = TCPSocket.new(self.host, self.port)
- ssl = OpenSSL::SSL::SSLSocket.new(sock,context)
+ context
+ end
+
+ def self.connect_to(aps_host, aps_port)
+ context = self.ssl_context
+ sock = TCPSocket.new(aps_host, aps_port)
+ ssl = OpenSSL::SSL::SSLSocket.new(sock, context)
ssl.connect
return sock, ssl
end
+
+ def self.push_connection
+ self.connect_to(self.host, self.port)
+ end
def self.feedback_connection
- raise "The path to your pem file is not set. (APNS.pem = /path/to/cert.pem)" unless self.pem
- raise "The path to your pem file does not exist!" unless File.exist?(self.pem)
-
- context = OpenSSL::SSL::SSLContext.new
- context.cert = OpenSSL::X509::Certificate.new(File.read(self.pem))
- context.key = OpenSSL::PKey::RSA.new(File.read(self.pem), self.pass)
-
- fhost = self.host.gsub!('gateway','feedback')
- puts fhost
-
- sock = TCPSocket.new(fhost, 2196)
- ssl = OpenSSL::SSL::SSLSocket.new(sock,context)
- ssl.connect
-
- return sock, ssl
+ self.connect_to(self.feedback_host, self.feedback_port)
end
end
View
@@ -1,22 +1,47 @@
module APNS
-
+
class Payload
attr_accessor :device, :message
APS_ROOT = :aps
APS_KEYS = [:alert, :badge, :sound]
+
+ PAYLOAD_MAX_SIZE = 256
- def initialize(device_token, message_string_or_hash)
+ def initialize(device_token, message_string_or_hash = {})
self.device = APNS::Device.new(device_token)
if message_string_or_hash.is_a?(String)
- self.message = {:alert => message_string_or_hash}
+ self.message = {:alert => message_string_or_hash.strip}
elsif message_string_or_hash.is_a?(Hash)
- self.message = message_string_or_hash
+ self.message = message_string_or_hash.each_value { |val| val.strip! if val.respond_to? :strip! }
else
- raise "Payload message needs to be either a hash or string"
+ raise "Payload message argument needs to be either a hash or a string"
end
+ self
end
-
+
+ #
+ # Handy chainable setters
+ #
+ # Ex: APNS::Payload.new(token).badge(3).sound("bipbip").alert("Roadrunner!")
+ #
+ def badge(number)
+ message[:badge] = number
+ self
+ end
+
+ def sound(filename)
+ message[:sound] = filename
+ self
+ end
+
+ def alert(string)
+ message[:alert] = string
+ self
+ end
+
+
+ #
def to_ssl
pm = self.apn_message.to_json
[0, 0, 32, self.device.to_payload, 0, pm.size, pm].pack("ccca*cca*")
@@ -27,8 +52,8 @@ def size
end
def valid?
- self.size <= 256
- end
+ self.size <= PAYLOAD_MAX_SIZE
+ end
def apn_message
message_hash = message.dup
@@ -40,5 +65,57 @@ def apn_message
apnm
end
+ # Returns a new payload with the alert truncated to fit in the payload size requirement (PAYLOAD_MAX_SIZE)
+ # Rem: It's a best effort since the alert may not be the one string responsible for the oversized payload
+ def payload_with_truncated_alert
+ payload_with_truncated_string_at_keypath([:alert])
+ end
+
+
+ # payload_with_truncated_string_at_keypath("alert") or payload_with_truncated_string_at_keypath([:alert])
+ # or
+ # payload_with_truncated_string_at_keypath("custom1.custom2") or payload_with_truncated_string_at_keypath([:custom1, :custom2])
+ # Rem: Truncation only works on String values...
+ def payload_with_truncated_string_at_keypath(array_or_dotted_string)
+ return self if valid? # You can safely call it on a valid payload
+
+ # Rem: I'm using Marshall to make a deep copy of the message hash. Of course this would only work with "standard" values like Hash/String/Array
+ payload_with_empty_string = APNS::Payload.new(device.token, Marshal.load(Marshal.dump(message)).at_key_path(array_or_dotted_string){|obj, key| obj[key] = ""})
+ wanted_length = PAYLOAD_MAX_SIZE - payload_with_empty_string.size
+
+ # Return a new payload with truncated value
+ APNS::Payload.new(device.token, Marshal.load(Marshal.dump(message)).at_key_path(array_or_dotted_string) {|obj, key| obj[key] = obj[key].truncate(wanted_length) })
+ end
+
+ end #Payload
+
+end #module
+
+class Hash
+ def at_key_path(array_or_dotted_string, &block)
+ keypath = array_or_dotted_string.is_a?(Array) ? array_or_dotted_string.dup : array_or_dotted_string.split('.')
+ obj = self
+ while (keypath.count > 0) do
+ key = keypath.shift.to_s
+ key = key.to_sym if !obj.has_key?(key) && obj.has_key?(key.to_sym)
+ next unless keypath.count > 0 # exit the while loop
+ obj = obj.has_key?(key) ? obj[key] : raise("No key #{key} in Object (#{obj.inspect})")
+ end
+
+ raise("No key #{key} in Object (#{obj.inspect})") unless obj.has_key?(key)
+ if block_given?
+ block.call(obj, key)
+ return self
+ else
+ return obj[key]
+ end
+ end
+end
+
+class String
+ if !String.new.respond_to? :truncate
+ def truncate(len)
+ (len > 4 && length > 5) ? self[0..(len - 1) - 3] + '...' : self
+ end
end
end
@@ -2,14 +2,32 @@
describe APNS do
+ def valid_payload
+ APNS::Payload.new("a-device-token", "my message")
+ end
+
describe "send_payload" do
it "should complain about no pem file path" do
- lambda{APNS.send_payloads("payload")}.should raise_error(APNS::PemPathError)
+ lambda{APNS.send_payloads(valid_payload)}.should raise_error(APNS::PemPathError)
+ end
+
+ it "should complain about pem file inexistence" do
+ APNS.pem = "test.pem"
+ lambda{APNS.send_payloads(valid_payload)}.should raise_error(APNS::PemFileError)
+ APNS.pem = nil # cleanup for next tests
+ end
+
+ end
+
+ describe "feedback" do
+ it "should complain about no pem file path" do
+ lambda{APNS.feedback()}.should raise_error(APNS::PemPathError)
end
it "should complain about pem file inexistence" do
APNS.pem = "test.pem"
- lambda{APNS.send_payloads("payload")}.should raise_error(APNS::PemFileError)
+ lambda{APNS.feedback()}.should raise_error(APNS::PemFileError)
+ APNS.pem = nil # cleanup for next tests
end
end
Oops, something went wrong.

0 comments on commit 99141b5

Please sign in to comment.