diff --git a/.gitignore b/.gitignore index 7fe46ee3..2e6d0026 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ profile spec/active_record/test.db .bundle **/.svn +.idea \ No newline at end of file diff --git a/README b/README index ff01eaa7..9d67cb58 100644 --- a/README +++ b/README @@ -123,21 +123,21 @@ APN on Rails has the following default configurations that you change as you see configatron.apn.passphrase # => '' configatron.apn.port # => 2195 configatron.apn.host # => 'gateway.sandbox.push.apple.com' - configatron.apn.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_development.pem') + configatron.apn.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_development.pem') # production (delivery): configatron.apn.host # => 'gateway.push.apple.com' - configatron.apn.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_production.pem') + configatron.apn.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_production.pem') # development (feedback): configatron.apn.feedback.passphrase # => '' configatron.apn.feedback.port # => 2196 configatron.apn.feedback.host # => 'feedback.sandbox.push.apple.com' - configatron.apn.feedback.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_development.pem') + configatron.apn.feedback.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_development.pem') # production (feedback): configatron.apn.feedback.host # => 'feedback.push.apple.com' - configatron.apn.feedback.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_production.pem') + configatron.apn.feedback.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_production.pem') That's it, now you're ready to start creating notifications. diff --git a/README.textile b/README.textile index 65d4c1b1..b02b549e 100644 --- a/README.textile +++ b/README.textile @@ -141,21 +141,21 @@ see fit: configatron.apn.passphrase # => '' configatron.apn.port # => 2195 configatron.apn.host # => 'gateway.sandbox.push.apple.com' - configatron.apn.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_development.pem') + configatron.apn.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_development.pem') # production (delivery): configatron.apn.host # => 'gateway.push.apple.com' - configatron.apn.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_production.pem') + configatron.apn.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_production.pem') # development (feedback): configatron.apn.feedback.passphrase # => '' configatron.apn.feedback.port # => 2196 configatron.apn.feedback.host # => 'feedback.sandbox.push.apple.com' - configatron.apn.feedback.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_development.pem') + configatron.apn.feedback.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_development.pem') # production (feedback): configatron.apn.feedback.host # => 'feedback.push.apple.com' - configatron.apn.feedback.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_production.pem') + configatron.apn.feedback.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_production.pem') That's it, now you're ready to start creating notifications. diff --git a/lib/apn_on_rails/apn_on_rails.rb b/lib/apn_on_rails/apn_on_rails.rb index 56caa6a1..1bc8bd55 100644 --- a/lib/apn_on_rails/apn_on_rails.rb +++ b/lib/apn_on_rails/apn_on_rails.rb @@ -3,13 +3,13 @@ require 'configatron' rails_root = File.join(FileUtils.pwd, 'rails_root') -if defined?(RAILS_ROOT) - rails_root = RAILS_ROOT +if defined?(::Rails.root) + rails_root = ::Rails.root.to_s end rails_env = 'development' -if defined?(RAILS_ENV) - rails_env = RAILS_ENV +if defined?(::Rails.env) + rails_env = ::Rails.env end configatron.apn.set_default(:passphrase, '') diff --git a/lib/apn_on_rails/app/models/apn/app.rb b/lib/apn_on_rails/app/models/apn/app.rb index a0927374..60537f90 100644 --- a/lib/apn_on_rails/app/models/apn/app.rb +++ b/lib/apn_on_rails/app/models/apn/app.rb @@ -1,16 +1,17 @@ +# encoding: utf-8 class APN::App < APN::Base - + has_many :groups, :class_name => 'APN::Group', :dependent => :destroy has_many :devices, :class_name => 'APN::Device', :dependent => :destroy has_many :notifications, :through => :devices, :dependent => :destroy has_many :unsent_notifications, :through => :devices has_many :group_notifications, :through => :groups has_many :unsent_group_notifications, :through => :groups - + def cert - (RAILS_ENV == 'production' ? apn_prod_cert : apn_dev_cert) + (::Rails.env.production? ? apn_prod_cert : apn_dev_cert) end - + # Opens a connection to the Apple APN server and attempts to batch deliver # an Array of group notifications. # @@ -25,9 +26,9 @@ def send_notifications end APN::App.send_notifications_for_cert(self.cert, self.id) end - + def self.send_notifications - apps = APN::App.all + apps = APN::App.all apps.each do |app| app.send_notifications end @@ -36,50 +37,56 @@ def self.send_notifications send_notifications_for_cert(global_cert, nil) end end - + def self.send_notifications_for_cert(the_cert, app_id) # unless self.unsent_notifications.nil? || self.unsent_notifications.empty? - if (app_id == nil) - conditions = "app_id is null" - else - conditions = ["app_id = ?", app_id] - end - begin - APN::Connection.open_for_delivery({:cert => the_cert}) do |conn, sock| - APN::Device.find_each(:conditions => conditions) do |dev| - dev.unsent_notifications.each do |noty| - conn.write(noty.message_for_sending) - noty.sent_at = Time.now - noty.save - end - end + if (app_id == nil) + conditions = "app_id is null" + else + conditions = ["app_id = ?", app_id] + end + APN::Connection.open_for_delivery({:cert => the_cert}) do |conn, sock| + APN::Device.find_each(:conditions => conditions) do |dev| + dev.unsent_notifications.each do |noty| + conn.write(noty.message_for_sending) + noty.sent_at = Time.now + noty.save end - rescue Exception => e - log_connection_exception(e) end - # end + end + # end end - + def send_group_notifications - if self.cert.nil? + if self.cert.nil? raise APN::Errors::MissingCertificateError.new return end - unless self.unsent_group_notifications.nil? || self.unsent_group_notifications.empty? - APN::Connection.open_for_delivery({:cert => self.cert}) do |conn, sock| - unsent_group_notifications.each do |gnoty| - gnoty.devices.find_each do |device| - conn.write(gnoty.message_for_sending(device)) + unless self.unsent_group_notifications.nil? || self.unsent_group_notifications.empty? + unsent_group_notifications.each do |gnoty| + failed = 0 + devices_to_send = gnoty.devices.count + gnoty.devices.find_in_batches(:batch_size => 100) do |devices| + APN::Connection.open_for_delivery({:cert => self.cert}) do |conn, sock| + devices.each do |device| + begin + conn.write(gnoty.message_for_sending(device)) + rescue Exception => e + puts e.message + failed += 1 + end + end end - gnoty.sent_at = Time.now - gnoty.save end + puts "Sent to: #{devices_to_send - failed}/#{devices_to_send} " + gnoty.sent_at = Time.now + gnoty.save end end end - + def send_group_notification(gnoty) - if self.cert.nil? + if self.cert.nil? raise APN::Errors::MissingCertificateError.new return end @@ -93,14 +100,14 @@ def send_group_notification(gnoty) end end end - + def self.send_group_notifications apps = APN::App.all apps.each do |app| app.send_group_notifications end - end - + end + # Retrieves a list of APN::Device instnces from Apple using # the devices method. It then checks to see if the # last_registered_at date of each APN::Device is @@ -117,8 +124,10 @@ def process_devices return end APN::App.process_devices_for_cert(self.cert) - end # process_devices - + end + + # process_devices + def self.process_devices apps = APN::App.all apps.each do |app| @@ -129,23 +138,23 @@ def self.process_devices APN::App.process_devices_for_cert(global_cert) end end - + def self.process_devices_for_cert(the_cert) puts "in APN::App.process_devices_for_cert" APN::Feedback.devices(the_cert).each do |device| if device.last_registered_at < device.feedback_at puts "device #{device.id} -> #{device.last_registered_at} < #{device.feedback_at}" device.destroy - else + else puts "device #{device.id} -> #{device.last_registered_at} not < #{device.feedback_at}" end - end + end end - - + + protected def log_connection_exception(ex) puts ex.message end - + end \ No newline at end of file diff --git a/lib/apn_on_rails/app/models/apn/base.rb b/lib/apn_on_rails/app/models/apn/base.rb index c54fdc51..290537ff 100644 --- a/lib/apn_on_rails/app/models/apn/base.rb +++ b/lib/apn_on_rails/app/models/apn/base.rb @@ -1,6 +1,8 @@ module APN class Base < ActiveRecord::Base # :nodoc: + self.abstract_class = true + def self.table_name # :nodoc: self.to_s.gsub("::", "_").tableize end diff --git a/lib/apn_on_rails/app/models/apn/device.rb b/lib/apn_on_rails/app/models/apn/device.rb index 1864ca0a..3e94461f 100644 --- a/lib/apn_on_rails/app/models/apn/device.rb +++ b/lib/apn_on_rails/app/models/apn/device.rb @@ -15,7 +15,7 @@ class APN::Device < APN::Base has_many :unsent_notifications, :class_name => 'APN::Notification', :conditions => 'sent_at is null' validates_uniqueness_of :token, :scope => :app_id - validates_format_of :token, :with => /^[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}$/ + #validates_format_of :token, :with => /^[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}\s[a-z0-9]{8}$/ before_create :set_last_registered_at diff --git a/lib/apn_on_rails/app/models/apn/group_notification.rb b/lib/apn_on_rails/app/models/apn/group_notification.rb index fb7f8e8f..19679877 100644 --- a/lib/apn_on_rails/app/models/apn/group_notification.rb +++ b/lib/apn_on_rails/app/models/apn/group_notification.rb @@ -1,18 +1,19 @@ +# encoding: utf-8 class APN::GroupNotification < APN::Base include ::ActionView::Helpers::TextHelper extend ::ActionView::Helpers::TextHelper serialize :custom_properties - + belongs_to :group, :class_name => 'APN::Group' - has_one :app, :class_name => 'APN::App', :through => :group - has_many :device_groupings, :through => :group - + has_one :app, :class_name => 'APN::App', :through => :group + has_many :device_groupings, :through => :group + validates_presence_of :group_id - + def devices self.group.devices end - + # Stores the text alert message you want to send to the device. # # If the message is over 150 characters long it will get truncated @@ -23,7 +24,7 @@ def alert=(message) end write_attribute('alert', message) end - + # Creates a Hash that will be the payload of an APN. # # Example: @@ -45,17 +46,25 @@ def apple_hash result['aps']['alert'] = self.alert if self.alert result['aps']['badge'] = self.badge.to_i if self.badge if self.sound - result['aps']['sound'] = self.sound if self.sound.is_a? String + result['aps']['sound'] = self.sound if self.sound.is_a?(String) && self.sound.strip.present? result['aps']['sound'] = "1.aiff" if self.sound.is_a?(TrueClass) end if self.custom_properties - self.custom_properties.each do |key,value| + self.custom_properties.each do |key, value| result["#{key}"] = "#{value}" end end result end - + + def payload + multi_json_dump(apple_hash) + end + + def payload_size + payload.bytesize + end + # Creates the JSON string required for an APN message. # # Example: @@ -67,13 +76,37 @@ def apple_hash def to_apple_json self.apple_hash.to_json end - + # Creates the binary message needed to send to Apple. + #def message_for_sending(device) + # json = self.to_apple_json.gsub(/\\u([0-9a-z]{4})/) { |s| [$1.to_i(16)].pack("U") } # This will create non encoded string. Otherwise the string is encoded from utf8 to ascii with unicode representation (i.e. \\u05d2) + # message = "\0\0 #{device.to_hexa}\0".force_encoding("UTF-8") + "#{json.length.chr}#{json}".force_encoding("UTF-8") + # raise APN::Errors::ExceededMessageSizeError.new(message) if message.size.to_i > 256 + # message + #end + + # This method conforms to the enhanced binary format. + # http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW4 def message_for_sending(device) - json = self.to_apple_json - message = "\0\0 #{device.to_hexa}\0#{json.length.chr}#{json}" + message = [1, device.id, 1.day.to_i, 0, 32, device.token, payload_size, payload].pack("cNNccH*na*") raise APN::Errors::ExceededMessageSizeError.new(message) if message.size.to_i > 256 message end - + + + private + + def multi_json_load(string, options = {}) + # Calling load on multi_json less than v1.3.0 attempts to load a file from disk. Check the version explicitly. + if Gem.loaded_specs['multi_json'].version >= Gem::Version.create('1.3.0') + MultiJson.load(string, options) + else + MultiJson.decode(string, options) + end + end + + def multi_json_dump(string, options = {}) + MultiJson.respond_to?(:dump) ? MultiJson.dump(string, options) : MultiJson.encode(string, options) + end + end # APN::Notification \ No newline at end of file diff --git a/lib/apn_on_rails/app/models/apn/notification.rb b/lib/apn_on_rails/app/models/apn/notification.rb index 068f13a8..473d055b 100644 --- a/lib/apn_on_rails/app/models/apn/notification.rb +++ b/lib/apn_on_rails/app/models/apn/notification.rb @@ -54,7 +54,7 @@ def apple_hash result['aps']['alert'] = self.alert if self.alert result['aps']['badge'] = self.badge.to_i if self.badge if self.sound - result['aps']['sound'] = self.sound if self.sound.is_a? String + result['aps']['sound'] = self.sound if self.sound.is_a?(String) && self.sound.strip.present? result['aps']['sound'] = "1.aiff" if self.sound.is_a?(TrueClass) end if self.custom_properties @@ -79,7 +79,7 @@ def to_apple_json # Creates the binary message needed to send to Apple. def message_for_sending - json = self.to_apple_json + json = self.to_apple_json.gsub(/\\u([0-9a-z]{4})/) {|s| [$1.to_i(16)].pack("U")} # This will create non encoded string. Otherwise the string is encoded from utf8 to ascii with unicode representation (i.e. \\u05d2) message = "\0\0 #{self.device.to_hexa}\0#{json.length.chr}#{json}" raise APN::Errors::ExceededMessageSizeError.new(message) if message.size.to_i > 256 message diff --git a/lib/apn_on_rails/libs/connection.rb b/lib/apn_on_rails/libs/connection.rb index c44f8a7e..ef06de77 100644 --- a/lib/apn_on_rails/libs/connection.rb +++ b/lib/apn_on_rails/libs/connection.rb @@ -1,3 +1,4 @@ +# encoding: utf-8 module APN module Connection @@ -59,7 +60,7 @@ def open(options = {}, &block) # :nodoc: ssl.connect yield ssl, sock if block_given? - + ensure ssl.close sock.close end diff --git a/lib/apn_on_rails/libs/feedback.rb b/lib/apn_on_rails/libs/feedback.rb index 258ed22e..86f64c2f 100644 --- a/lib/apn_on_rails/libs/feedback.rb +++ b/lib/apn_on_rails/libs/feedback.rb @@ -13,10 +13,10 @@ class << self def devices(cert, &block) devices = [] return if cert.nil? - APN::Connection.open_for_feedback({:cert => cert}) do |conn, sock| + APN::Connection.open_for_feedback({:cert => cert}) do |conn, sock| while line = conn.read(38) # Read 38 bytes from the SSL socket - feedback = line.unpack('N1n1H140') - token = feedback[2].scan(/.{0,8}/).join(' ').strip + feedback = line.unpack('N1n1H140') + token = feedback[2].strip device = APN::Device.find(:first, :conditions => {:token => token}) if device device.feedback_at = Time.at(feedback[0]) @@ -25,7 +25,7 @@ def devices(cert, &block) end end devices.each(&block) if block_given? - return devices + devices end # devices def process_devices