Skip to content
This repository

Fix for group notification #36

Open
wants to merge 15 commits into from
This page is out of date. Refresh to see the latest.
1  .gitignore
@@ -15,3 +15,4 @@ profile
15 15 spec/active_record/test.db
16 16 .bundle
17 17 **/.svn
  18 +.idea
8 README
@@ -123,21 +123,21 @@ APN on Rails has the following default configurations that you change as you see
123 123 configatron.apn.passphrase # => ''
124 124 configatron.apn.port # => 2195
125 125 configatron.apn.host # => 'gateway.sandbox.push.apple.com'
126   - configatron.apn.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_development.pem')
  126 + configatron.apn.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_development.pem')
127 127
128 128 # production (delivery):
129 129 configatron.apn.host # => 'gateway.push.apple.com'
130   - configatron.apn.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_production.pem')
  130 + configatron.apn.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_production.pem')
131 131
132 132 # development (feedback):
133 133 configatron.apn.feedback.passphrase # => ''
134 134 configatron.apn.feedback.port # => 2196
135 135 configatron.apn.feedback.host # => 'feedback.sandbox.push.apple.com'
136   - configatron.apn.feedback.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_development.pem')
  136 + configatron.apn.feedback.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_development.pem')
137 137
138 138 # production (feedback):
139 139 configatron.apn.feedback.host # => 'feedback.push.apple.com'
140   - configatron.apn.feedback.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_production.pem')
  140 + configatron.apn.feedback.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_production.pem')
141 141
142 142 That's it, now you're ready to start creating notifications.
143 143
8 README.textile
Source Rendered
@@ -141,21 +141,21 @@ see fit:
141 141 configatron.apn.passphrase # => ''
142 142 configatron.apn.port # => 2195
143 143 configatron.apn.host # => 'gateway.sandbox.push.apple.com'
144   - configatron.apn.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_development.pem')
  144 + configatron.apn.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_development.pem')
145 145
146 146 # production (delivery):
147 147 configatron.apn.host # => 'gateway.push.apple.com'
148   - configatron.apn.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_production.pem')
  148 + configatron.apn.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_production.pem')
149 149
150 150 # development (feedback):
151 151 configatron.apn.feedback.passphrase # => ''
152 152 configatron.apn.feedback.port # => 2196
153 153 configatron.apn.feedback.host # => 'feedback.sandbox.push.apple.com'
154   - configatron.apn.feedback.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_development.pem')
  154 + configatron.apn.feedback.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_development.pem')
155 155
156 156 # production (feedback):
157 157 configatron.apn.feedback.host # => 'feedback.push.apple.com'
158   - configatron.apn.feedback.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_production.pem')
  158 + configatron.apn.feedback.cert #=> File.join(::Rails.root, 'config', 'apple_push_notification_production.pem')
159 159 </pre></code>
160 160
161 161 That's it, now you're ready to start creating notifications.
8 lib/apn_on_rails/apn_on_rails.rb
@@ -3,13 +3,13 @@
3 3 require 'configatron'
4 4
5 5 rails_root = File.join(FileUtils.pwd, 'rails_root')
6   -if defined?(RAILS_ROOT)
7   - rails_root = RAILS_ROOT
  6 +if defined?(::Rails.root)
  7 + rails_root = ::Rails.root.to_s
8 8 end
9 9
10 10 rails_env = 'development'
11   -if defined?(RAILS_ENV)
12   - rails_env = RAILS_ENV
  11 +if defined?(::Rails.env)
  12 + rails_env = ::Rails.env
13 13 end
14 14
15 15 configatron.apn.set_default(:passphrase, '')
101 lib/apn_on_rails/app/models/apn/app.rb
... ... @@ -1,16 +1,17 @@
  1 +# encoding: utf-8
1 2 class APN::App < APN::Base
2   -
  3 +
3 4 has_many :groups, :class_name => 'APN::Group', :dependent => :destroy
4 5 has_many :devices, :class_name => 'APN::Device', :dependent => :destroy
5 6 has_many :notifications, :through => :devices, :dependent => :destroy
6 7 has_many :unsent_notifications, :through => :devices
7 8 has_many :group_notifications, :through => :groups
8 9 has_many :unsent_group_notifications, :through => :groups
9   -
  10 +
10 11 def cert
11   - (RAILS_ENV == 'production' ? apn_prod_cert : apn_dev_cert)
  12 + (::Rails.env.production? ? apn_prod_cert : apn_dev_cert)
12 13 end
13   -
  14 +
14 15 # Opens a connection to the Apple APN server and attempts to batch deliver
15 16 # an Array of group notifications.
16 17 #
@@ -25,9 +26,9 @@ def send_notifications
25 26 end
26 27 APN::App.send_notifications_for_cert(self.cert, self.id)
27 28 end
28   -
  29 +
29 30 def self.send_notifications
30   - apps = APN::App.all
  31 + apps = APN::App.all
31 32 apps.each do |app|
32 33 app.send_notifications
33 34 end
@@ -36,50 +37,56 @@ def self.send_notifications
36 37 send_notifications_for_cert(global_cert, nil)
37 38 end
38 39 end
39   -
  40 +
40 41 def self.send_notifications_for_cert(the_cert, app_id)
41 42 # unless self.unsent_notifications.nil? || self.unsent_notifications.empty?
42   - if (app_id == nil)
43   - conditions = "app_id is null"
44   - else
45   - conditions = ["app_id = ?", app_id]
46   - end
47   - begin
48   - APN::Connection.open_for_delivery({:cert => the_cert}) do |conn, sock|
49   - APN::Device.find_each(:conditions => conditions) do |dev|
50   - dev.unsent_notifications.each do |noty|
51   - conn.write(noty.message_for_sending)
52   - noty.sent_at = Time.now
53   - noty.save
54   - end
55   - end
  43 + if (app_id == nil)
  44 + conditions = "app_id is null"
  45 + else
  46 + conditions = ["app_id = ?", app_id]
  47 + end
  48 + APN::Connection.open_for_delivery({:cert => the_cert}) do |conn, sock|
  49 + APN::Device.find_each(:conditions => conditions) do |dev|
  50 + dev.unsent_notifications.each do |noty|
  51 + conn.write(noty.message_for_sending)
  52 + noty.sent_at = Time.now
  53 + noty.save
56 54 end
57   - rescue Exception => e
58   - log_connection_exception(e)
59 55 end
60   - # end
  56 + end
  57 + # end
61 58 end
62   -
  59 +
63 60 def send_group_notifications
64   - if self.cert.nil?
  61 + if self.cert.nil?
65 62 raise APN::Errors::MissingCertificateError.new
66 63 return
67 64 end
68   - unless self.unsent_group_notifications.nil? || self.unsent_group_notifications.empty?
69   - APN::Connection.open_for_delivery({:cert => self.cert}) do |conn, sock|
70   - unsent_group_notifications.each do |gnoty|
71   - gnoty.devices.find_each do |device|
72   - conn.write(gnoty.message_for_sending(device))
  65 + unless self.unsent_group_notifications.nil? || self.unsent_group_notifications.empty?
  66 + unsent_group_notifications.each do |gnoty|
  67 + failed = 0
  68 + devices_to_send = gnoty.devices.count
  69 + gnoty.devices.find_in_batches(:batch_size => 100) do |devices|
  70 + APN::Connection.open_for_delivery({:cert => self.cert}) do |conn, sock|
  71 + devices.each do |device|
  72 + begin
  73 + conn.write(gnoty.message_for_sending(device))
  74 + rescue Exception => e
  75 + puts e.message
  76 + failed += 1
  77 + end
  78 + end
73 79 end
74   - gnoty.sent_at = Time.now
75   - gnoty.save
76 80 end
  81 + puts "Sent to: #{devices_to_send - failed}/#{devices_to_send} "
  82 + gnoty.sent_at = Time.now
  83 + gnoty.save
77 84 end
78 85 end
79 86 end
80   -
  87 +
81 88 def send_group_notification(gnoty)
82   - if self.cert.nil?
  89 + if self.cert.nil?
83 90 raise APN::Errors::MissingCertificateError.new
84 91 return
85 92 end
@@ -93,14 +100,14 @@ def send_group_notification(gnoty)
93 100 end
94 101 end
95 102 end
96   -
  103 +
97 104 def self.send_group_notifications
98 105 apps = APN::App.all
99 106 apps.each do |app|
100 107 app.send_group_notifications
101 108 end
102   - end
103   -
  109 + end
  110 +
104 111 # Retrieves a list of APN::Device instnces from Apple using
105 112 # the <tt>devices</tt> method. It then checks to see if the
106 113 # <tt>last_registered_at</tt> date of each APN::Device is
@@ -117,8 +124,10 @@ def process_devices
117 124 return
118 125 end
119 126 APN::App.process_devices_for_cert(self.cert)
120   - end # process_devices
121   -
  127 + end
  128 +
  129 + # process_devices
  130 +
122 131 def self.process_devices
123 132 apps = APN::App.all
124 133 apps.each do |app|
@@ -129,23 +138,23 @@ def self.process_devices
129 138 APN::App.process_devices_for_cert(global_cert)
130 139 end
131 140 end
132   -
  141 +
133 142 def self.process_devices_for_cert(the_cert)
134 143 puts "in APN::App.process_devices_for_cert"
135 144 APN::Feedback.devices(the_cert).each do |device|
136 145 if device.last_registered_at < device.feedback_at
137 146 puts "device #{device.id} -> #{device.last_registered_at} < #{device.feedback_at}"
138 147 device.destroy
139   - else
  148 + else
140 149 puts "device #{device.id} -> #{device.last_registered_at} not < #{device.feedback_at}"
141 150 end
142   - end
  151 + end
143 152 end
144   -
145   -
  153 +
  154 +
146 155 protected
147 156 def log_connection_exception(ex)
148 157 puts ex.message
149 158 end
150   -
  159 +
151 160 end
2  lib/apn_on_rails/app/models/apn/base.rb
... ... @@ -1,6 +1,8 @@
1 1 module APN
2 2 class Base < ActiveRecord::Base # :nodoc:
3 3
  4 + self.abstract_class = true
  5 +
4 6 def self.table_name # :nodoc:
5 7 self.to_s.gsub("::", "_").tableize
6 8 end
2  lib/apn_on_rails/app/models/apn/device.rb
@@ -15,7 +15,7 @@ class APN::Device < APN::Base
15 15 has_many :unsent_notifications, :class_name => 'APN::Notification', :conditions => 'sent_at is null'
16 16
17 17 validates_uniqueness_of :token, :scope => :app_id
18   - 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}$/
  18 + #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}$/
19 19
20 20 before_create :set_last_registered_at
21 21
61 lib/apn_on_rails/app/models/apn/group_notification.rb
... ... @@ -1,18 +1,19 @@
  1 +# encoding: utf-8
1 2 class APN::GroupNotification < APN::Base
2 3 include ::ActionView::Helpers::TextHelper
3 4 extend ::ActionView::Helpers::TextHelper
4 5 serialize :custom_properties
5   -
  6 +
6 7 belongs_to :group, :class_name => 'APN::Group'
7   - has_one :app, :class_name => 'APN::App', :through => :group
8   - has_many :device_groupings, :through => :group
9   -
  8 + has_one :app, :class_name => 'APN::App', :through => :group
  9 + has_many :device_groupings, :through => :group
  10 +
10 11 validates_presence_of :group_id
11   -
  12 +
12 13 def devices
13 14 self.group.devices
14 15 end
15   -
  16 +
16 17 # Stores the text alert message you want to send to the device.
17 18 #
18 19 # If the message is over 150 characters long it will get truncated
@@ -23,7 +24,7 @@ def alert=(message)
23 24 end
24 25 write_attribute('alert', message)
25 26 end
26   -
  27 +
27 28 # Creates a Hash that will be the payload of an APN.
28 29 #
29 30 # Example:
@@ -45,17 +46,25 @@ def apple_hash
45 46 result['aps']['alert'] = self.alert if self.alert
46 47 result['aps']['badge'] = self.badge.to_i if self.badge
47 48 if self.sound
48   - result['aps']['sound'] = self.sound if self.sound.is_a? String
  49 + result['aps']['sound'] = self.sound if self.sound.is_a?(String) && self.sound.strip.present?
49 50 result['aps']['sound'] = "1.aiff" if self.sound.is_a?(TrueClass)
50 51 end
51 52 if self.custom_properties
52   - self.custom_properties.each do |key,value|
  53 + self.custom_properties.each do |key, value|
53 54 result["#{key}"] = "#{value}"
54 55 end
55 56 end
56 57 result
57 58 end
58   -
  59 +
  60 + def payload
  61 + multi_json_dump(apple_hash)
  62 + end
  63 +
  64 + def payload_size
  65 + payload.bytesize
  66 + end
  67 +
59 68 # Creates the JSON string required for an APN message.
60 69 #
61 70 # Example:
@@ -67,13 +76,37 @@ def apple_hash
67 76 def to_apple_json
68 77 self.apple_hash.to_json
69 78 end
70   -
  79 +
71 80 # Creates the binary message needed to send to Apple.
  81 + #def message_for_sending(device)
  82 + # 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)
  83 + # message = "\0\0 #{device.to_hexa}\0".force_encoding("UTF-8") + "#{json.length.chr}#{json}".force_encoding("UTF-8")
  84 + # raise APN::Errors::ExceededMessageSizeError.new(message) if message.size.to_i > 256
  85 + # message
  86 + #end
  87 +
  88 + # This method conforms to the enhanced binary format.
  89 + # http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW4
72 90 def message_for_sending(device)
73   - json = self.to_apple_json
74   - message = "\0\0 #{device.to_hexa}\0#{json.length.chr}#{json}"
  91 + message = [1, device.id, 1.day.to_i, 0, 32, device.token, payload_size, payload].pack("cNNccH*na*")
75 92 raise APN::Errors::ExceededMessageSizeError.new(message) if message.size.to_i > 256
76 93 message
77 94 end
78   -
  95 +
  96 +
  97 + private
  98 +
  99 + def multi_json_load(string, options = {})
  100 + # Calling load on multi_json less than v1.3.0 attempts to load a file from disk. Check the version explicitly.
  101 + if Gem.loaded_specs['multi_json'].version >= Gem::Version.create('1.3.0')
  102 + MultiJson.load(string, options)
  103 + else
  104 + MultiJson.decode(string, options)
  105 + end
  106 + end
  107 +
  108 + def multi_json_dump(string, options = {})
  109 + MultiJson.respond_to?(:dump) ? MultiJson.dump(string, options) : MultiJson.encode(string, options)
  110 + end
  111 +
79 112 end # APN::Notification
4 lib/apn_on_rails/app/models/apn/notification.rb
@@ -54,7 +54,7 @@ def apple_hash
54 54 result['aps']['alert'] = self.alert if self.alert
55 55 result['aps']['badge'] = self.badge.to_i if self.badge
56 56 if self.sound
57   - result['aps']['sound'] = self.sound if self.sound.is_a? String
  57 + result['aps']['sound'] = self.sound if self.sound.is_a?(String) && self.sound.strip.present?
58 58 result['aps']['sound'] = "1.aiff" if self.sound.is_a?(TrueClass)
59 59 end
60 60 if self.custom_properties
@@ -79,7 +79,7 @@ def to_apple_json
79 79
80 80 # Creates the binary message needed to send to Apple.
81 81 def message_for_sending
82   - json = self.to_apple_json
  82 + 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)
83 83 message = "\0\0 #{self.device.to_hexa}\0#{json.length.chr}#{json}"
84 84 raise APN::Errors::ExceededMessageSizeError.new(message) if message.size.to_i > 256
85 85 message
3  lib/apn_on_rails/libs/connection.rb
... ... @@ -1,3 +1,4 @@
  1 +# encoding: utf-8
1 2 module APN
2 3 module Connection
3 4
@@ -59,7 +60,7 @@ def open(options = {}, &block) # :nodoc:
59 60 ssl.connect
60 61
61 62 yield ssl, sock if block_given?
62   -
  63 + ensure
63 64 ssl.close
64 65 sock.close
65 66 end
8 lib/apn_on_rails/libs/feedback.rb
@@ -13,10 +13,10 @@ class << self
13 13 def devices(cert, &block)
14 14 devices = []
15 15 return if cert.nil?
16   - APN::Connection.open_for_feedback({:cert => cert}) do |conn, sock|
  16 + APN::Connection.open_for_feedback({:cert => cert}) do |conn, sock|
17 17 while line = conn.read(38) # Read 38 bytes from the SSL socket
18   - feedback = line.unpack('N1n1H140')
19   - token = feedback[2].scan(/.{0,8}/).join(' ').strip
  18 + feedback = line.unpack('N1n1H140')
  19 + token = feedback[2].strip
20 20 device = APN::Device.find(:first, :conditions => {:token => token})
21 21 if device
22 22 device.feedback_at = Time.at(feedback[0])
@@ -25,7 +25,7 @@ def devices(cert, &block)
25 25 end
26 26 end
27 27 devices.each(&block) if block_given?
28   - return devices
  28 + devices
29 29 end # devices
30 30
31 31 def process_devices

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.