Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

batching finds, fall back to default app, change last_registered_at b…

…efore_save
  • Loading branch information...
commit 14897be6c22c1b44302dfad936e1b4b9bc43801d 2 parents 0af1f4e + 8a118a1
@PRXci PRXci authored
View
3  Gemfile
@@ -8,8 +8,9 @@ gem 'configatron'
# Add dependencies to develop your gem here.
# Include everything needed to run rake, tests, features, etc.
group :development do
+ gem 'autotest'
gem 'sqlite3-ruby'
- gem "rspec", ">= 2.0.0.beta.19"
+ gem "rspec", ">= 2.0.0"
gem "bundler", ">= 1.0.0.rc.5"
gem "jeweler", "~> 1.5.0.pre2"
gem "rcov", ">= 0"
View
22 Gemfile.lock
@@ -7,6 +7,7 @@ GEM
activerecord (2.3.9)
activesupport (= 2.3.9)
activesupport (2.3.9)
+ autotest (4.3.2)
configatron (2.6.4)
yamler (>= 0.1.0)
diff-lcs (1.1.2)
@@ -18,16 +19,16 @@ GEM
rack (1.1.0)
rake (0.8.7)
rcov (0.9.9)
- rspec (2.0.0.rc)
- rspec-core (= 2.0.0.rc)
- rspec-expectations (= 2.0.0.rc)
- rspec-mocks (= 2.0.0.rc)
- rspec-core (2.0.0.rc)
- rspec-expectations (2.0.0.rc)
+ rspec (2.0.1)
+ rspec-core (~> 2.0.1)
+ rspec-expectations (~> 2.0.1)
+ rspec-mocks (~> 2.0.1)
+ rspec-core (2.0.1)
+ rspec-expectations (2.0.1)
diff-lcs (>= 1.1.2)
- rspec-mocks (2.0.0.rc)
- rspec-core (= 2.0.0.rc)
- rspec-expectations (= 2.0.0.rc)
+ rspec-mocks (2.0.1)
+ rspec-core (~> 2.0.1)
+ rspec-expectations (~> 2.0.1)
sqlite3-ruby (1.3.1)
yamler (0.1.0)
@@ -37,9 +38,10 @@ PLATFORMS
DEPENDENCIES
actionpack (~> 2.3.8)
activerecord (~> 2.3.8)
+ autotest
bundler (>= 1.0.0.rc.5)
configatron
jeweler (~> 1.5.0.pre2)
rcov
- rspec (>= 2.0.0.beta.19)
+ rspec (>= 2.0.0)
sqlite3-ruby
View
28 README
@@ -4,7 +4,7 @@ APN on Rails is a Ruby on Rails gem that allows you to easily add Apple Push Not
support to your Rails application.
It supports:
-* Multiple iPhone apps managed from the same Rails application
+* Multiple iPhone apps managed from the same Rails application as well as a legacy default "app" with certs stored in config
* Individual notifications and group notifications
* Alerts, badges, sounds, and custom properties in notifications
* Pull notifications
@@ -14,13 +14,16 @@ It supports:
Multiple iPhone Apps: In previous versions of this gem a single Rails application was set up to
manage push notifications for a single iPhone app. In many cases it is useful to have a single Rails
app manage push notifications for multiple iPhone apps. With the addition of an APN::App model, this
-is now possible. The certificates are now stored on instances of APN::APP and all devices are associated
-with a particular app.
+is now possible. The certificates are now stored on instances of APN::App and devices are intended to be associated
+with a particular app. For compatibility with existing implementations it is still possible to create devices that
+are not associated with an APN::App and to send individual notifications to them using the certs stored in the
+config directory.
Individual and Group Notifications: Previous versions of this gem treated each notification individually
and did not provide a built-in way to send a broadcast notification to a group of devices. Group notifications
are now built into the gem. A group notification is associated with a group of devices and shares its
-contents across the entire group of devices.
+contents across the entire group of devices. (Group notifications are only available for groups of devices associated
+with an APN::App)
Notification Content Areas: Notifications may contain alerts, badges, sounds, and custom properties.
@@ -28,7 +31,14 @@ Pull Notifications: This version of the gem supports an alternative notification
on pulls from client devices and does not interact with the Apple Push Notification servers. This feature
may be used entirely independently of the push notification features. Pull notifications may be
created for an app. A client app can query for the most recent pull notification available since a
-given date to retrieve any notifications waiting for it.
+given date to retrieve any notifications waiting for it.
+
+==Version 0.4.1 Notes
+
+* Backwards compatibility. 0.4.0 required a manual upgrade to associate existing and new devices with an APN::App model. This version allows continued use of devices that are associated with a default "app" that stores its certificates in the config directory. This ought to allow upgrade to this version without code changes.
+* Batched finds. Finds on the APN::Device model that can return large numbers of records have been batched to limit memory impact.
+* Custom properties migration. At a pre-0.4.0 version the custom_properties attribute was added to the migration template that created the notifications table. This introduced a potential problem for gem users who had previously run this migration. The custom_properties alteration to the apn_notifications table has been moved to its own migration and should work regardless of whether your apn_notifications table already has a custom_properties attribute.
+* last_registered_at changed to work intuitively. The last_registered_at attribute of devices was being updated only on creation potentially causing a bug in which a device that opts out of APNs and then opts back in before apn_on_rails received feedback about it might miss a period of APNs that it should receive.
==Acknowledgements:
@@ -105,9 +115,9 @@ Now, to create the tables you need for APN on Rails, run the following task:
APN on Rails uses the Configatron gem, http://github.com/markbates/configatron/tree/master,
to configure itself. (With the change to multi-app support, the certifications are stored in the
-database rather than in the config directory. These configurations remain for now.)
-APN on Rails has the following default configurations that you change as you
-see fit:
+database rather than in the config directory, however, it is still possible to use the default "app" and the certificates
+stored in the config directory. For this setup, the following configurations apply.)
+APN on Rails has the following default configurations that you change as you see fit:
# development (delivery):
configatron.apn.passphrase # => ''
@@ -142,7 +152,7 @@ That way you ensure you have the latest version of the database tables needed.
==Example (assuming you have created an app and stored your keys on it):
$ ./script/console
- >> app = APN::App.find(:first)
+ >> app = APN::App.create(:name => "My App", :apn_dev_cert => "PASTE YOUR DEV CERT HERE", :apn_prod_cert => "PASTE YOUR PROD CERT HERE")
>> device = APN::Device.create(:token => "XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX",:app_id => app.id)
>> notification = APN::Notification.new
>> notification.device = device
View
23 README.textile
@@ -4,7 +4,7 @@ APN on Rails is a Ruby on Rails gem that allows you to easily add Apple Push Not
support to your Rails application.
It supports:
-* Multiple iPhone apps managed from the same Rails application
+* Multiple iPhone apps managed from the same Rails application as well as a legacy default "app" with certs stored in config
* Individual notifications and group notifications
* Alerts, badges, sounds, and custom properties in notifications
* Pull notifications
@@ -14,13 +14,16 @@ h2. Feature Descriptions
Multiple iPhone Apps: In previous versions of this gem a single Rails application was set up to
manage push notifications for a single iPhone app. In many cases it is useful to have a single Rails
app manage push notifications for multiple iPhone apps. With the addition of an APN::App model, this
-is now possible. The certificates are now stored on instances of APN::APP and all devices are associated
-with a particular app.
+is now possible. The certificates are now stored on instances of APN::App and all devices are intended to be associated
+with a particular app. For compatibility with existing implementations it is still possible to create devices that
+are not associated with an APN::App and to send individual notifications to them using the certs stored in the
+config directory.
Individual and Group Notifications: Previous versions of this gem treated each notification individually
and did not provide a built-in way to send a broadcast notification to a group of devices. Group notifications
are now built into the gem. A group notification is associated with a group of devices and shares its
-contents across the entire group of devices.
+contents across the entire group of devices. (Group notifications are only available for groups of devices associated
+with an APN::App)
Notification Content Areas: Notifications may contain alerts, badges, sounds, and custom properties.
@@ -30,6 +33,13 @@ may be used entirely independently of the push notification features. Pull noti
created for an app. A client app can query for the most recent pull notification available since a
given date to retrieve any notifications waiting for it.
+h2. Version 0.4.1 Notes
+
+* Backwards compatibility. 0.4.0 required a manual upgrade to associate existing and new devices with an APN::App model. This version allows continued use of devices that are associated with a default "app" that stores its certificates in the config directory. This ought to allow upgrade to this version without code changes.
+* Batched finds. Finds on the APN::Device model that can return large numbers of records have been batched to limit memory impact.
+* Custom properties migration. At a pre-0.4.0 version the custom_properties attribute was added to the migration template that created the notifications table. This introduced a potential problem for gem users who had previously run this migration. The custom_properties alteration to the apn_notifications table has been moved to its own migration and should work regardless of whether your apn_notifications table already has a custom_properties attribute.
+* last_registered_at changed to work intuitively. The last_registered_at attribute of devices was being updated only on creation potentially causing a bug in which a device that opts out of APNs and then opts back in before apn_on_rails received feedback about it might miss a period of APNs that it should receive.
+
h2. Acknowledgements:
From Mark Bates:
@@ -121,7 +131,8 @@ Now, to create the tables you need for APN on Rails, run the following task:
APN on Rails uses the Configatron gem, http://github.com/markbates/configatron/tree/master,
to configure itself. (With the change to multi-app support, the certifications are stored in the
-database rather than in the config directory. These configurations remain for now.)
+database rather than in the config directory, however, it is still possible to use the default "app" and the certificates
+stored in the config directory. For this setup, the following configurations apply.)
APN on Rails has the following default configurations that you change as you
see fit:
@@ -166,7 +177,7 @@ h2. Example (assuming you have created an app and stored your keys on it):
<pre><code>
$ ./script/console
- >> app = APN::App.find(:first)
+ >> app = APN::App.create(:name => "My App", :apn_dev_cert => "PASTE YOUR DEV CERT HERE", :apn_prod_cert => "PASTE YOUR PROD CERT HERE")
>> device = APN::Device.create(:token => "XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX",:app_id => app.id)
>> notification = APN::Notification.new
>> notification.device = device
View
10 apn_on_rails.gemspec
@@ -31,6 +31,7 @@ easily add Apple Push Notification (iPhone) support to your Rails application.
"Rakefile",
"VERSION",
"apn_on_rails.gemspec",
+ "autotest/discover.rb",
"generators/apn_migrations_generator.rb",
"generators/templates/apn_migrations/001_create_apn_devices.rb",
"generators/templates/apn_migrations/002_create_apn_notifications.rb",
@@ -108,8 +109,9 @@ easily add Apple Push Notification (iPhone) support to your Rails application.
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
s.add_runtime_dependency(%q<configatron>, [">= 0"])
+ s.add_development_dependency(%q<autotest>, [">= 0"])
s.add_development_dependency(%q<sqlite3-ruby>, [">= 0"])
- s.add_development_dependency(%q<rspec>, [">= 2.0.0.beta.19"])
+ s.add_development_dependency(%q<rspec>, [">= 2.0.0"])
s.add_development_dependency(%q<bundler>, [">= 1.0.0.rc.5"])
s.add_development_dependency(%q<jeweler>, ["~> 1.5.0.pre2"])
s.add_development_dependency(%q<rcov>, [">= 0"])
@@ -117,8 +119,9 @@ easily add Apple Push Notification (iPhone) support to your Rails application.
s.add_development_dependency(%q<activerecord>, ["~> 2.3.8"])
else
s.add_dependency(%q<configatron>, [">= 0"])
+ s.add_dependency(%q<autotest>, [">= 0"])
s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
- s.add_dependency(%q<rspec>, [">= 2.0.0.beta.19"])
+ s.add_dependency(%q<rspec>, [">= 2.0.0"])
s.add_dependency(%q<bundler>, [">= 1.0.0.rc.5"])
s.add_dependency(%q<jeweler>, ["~> 1.5.0.pre2"])
s.add_dependency(%q<rcov>, [">= 0"])
@@ -127,8 +130,9 @@ easily add Apple Push Notification (iPhone) support to your Rails application.
end
else
s.add_dependency(%q<configatron>, [">= 0"])
+ s.add_dependency(%q<autotest>, [">= 0"])
s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
- s.add_dependency(%q<rspec>, [">= 2.0.0.beta.19"])
+ s.add_dependency(%q<rspec>, [">= 2.0.0"])
s.add_dependency(%q<bundler>, [">= 1.0.0.rc.5"])
s.add_dependency(%q<jeweler>, ["~> 1.5.0.pre2"])
s.add_dependency(%q<rcov>, [">= 0"])
View
1  autotest/discover.rb
@@ -0,0 +1 @@
+Autotest.add_discovery { "rspec2" }
View
6 generators/templates/apn_migrations/010_alter_apn_notifications.rb
@@ -1,5 +1,11 @@
class AlterApnNotifications < ActiveRecord::Migration # :nodoc:
+ module APN # :nodoc:
+ class Notification < ActiveRecord::Base # :nodoc:
+ set_table_name 'apn_notifications'
+ end
+ end
+
def self.up
unless APN::Notification.column_names.include?("custom_properties")
add_column :apn_notifications, :custom_properties, :text
View
11 generators/templates/apn_migrations/011_make_device_token_index_nonunique.rb
@@ -0,0 +1,11 @@
+class MakeDeviceTokenIndexNonunique < ActiveRecord::Migration
+ def self.up
+ remove_index :apn_devices, :column => :token
+ add_index :apn_devices, :token
+ end
+
+ def self.down
+ remove_index :apn_devices, :column => :token
+ add_index :apn_devices, :token, :unique => true
+ end
+end
View
59 lib/apn_on_rails/app/models/apn/app.rb
@@ -23,15 +23,7 @@ def send_notifications
raise APN::Errors::MissingCertificateError.new
return
end
- unless self.unsent_notifications.nil? || self.unsent_notifications.empty?
- APN::Connection.open_for_delivery({:cert => self.cert}) do |conn, sock|
- unsent_notifications.find_each do |noty|
- conn.write(noty.message_for_sending)
- noty.sent_at = Time.now
- noty.save
- end
- end
- end
+ APN::App.send_notifications_for_cert(self.cert, self.id)
end
def self.send_notifications
@@ -39,6 +31,32 @@ def self.send_notifications
apps.each do |app|
app.send_notifications
end
+ global_cert = File.read(configatron.apn.cert)
+ if global_cert
+ 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
+ end
+ rescue

Would it be possible to either catch more explicit error classes here, or in some other way bubble up things that have gone wrong?

Swallowing all exceptions has the potential to hide debuggable connection problems from people (as I just discovered with an ssl cert issue).

If you do swallow allow, maybe catch the exception, and then log it, or send it to a method that does nothing but which we could override and send to hoptoad or something like that?

@rebeccanesson Collaborator

It's definitely a good idea to log the errors that occur at this point. I want to continue to catch them all there because I don't want the failure to send for one app to affect the ability to send for another one. I'll look into adding this or if you have suggestions of how it should be implemented, feel free.

Yes, I agree with not wanting one notifications error to ruin the whole party.

How about something like - https://gist.github.com/669650

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ end
+ # end
end
def send_group_notifications
@@ -49,7 +67,6 @@ def send_group_notifications
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|
- puts "number of devices is #{gnoty.devices.size}"
gnoty.devices.find_each do |device|
conn.write(gnoty.message_for_sending(device))
end
@@ -98,11 +115,7 @@ def process_devices
raise APN::Errors::MissingCertificateError.new
return
end
- APN::Feedback.devices(self.cert).each do |device|
- if device.last_registered_at < device.feedback_at
- device.destroy
- end
- end
+ APN::App.process_devices_for_cert(self.cert)
end # process_devices
def self.process_devices
@@ -110,6 +123,22 @@ def self.process_devices
apps.each do |app|
app.process_devices
end
+ global_cert = File.read(configatron.apn.cert)
+ if global_cert
+ 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
+ puts "device #{device.id} -> #{device.last_registered_at} not < #{device.feedback_at}"
+ end
+ end
end
end
View
5 lib/apn_on_rails/app/models/apn/device.rb
@@ -17,7 +17,7 @@ class APN::Device < APN::Base
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}$/
- before_save :set_last_registered_at
+ before_create :set_last_registered_at
# The <tt>feedback_at</tt> accessor is set when the
# device is marked as potentially disconnected from your
@@ -42,9 +42,8 @@ def to_hexa
[self.token.delete(' ')].pack('H*')
end
- private
def set_last_registered_at
- self.last_registered_at = Time.now if self.last_registered_at.nil?
+ self.last_registered_at = Time.now #if self.last_registered_at.nil?
end
end
View
69 spec/apn_on_rails/app/models/apn/app_spec.rb
@@ -10,27 +10,59 @@
device = DeviceFactory.create({:app_id => app.id})
notifications = [NotificationFactory.create({:device_id => device.id}),
NotificationFactory.create({:device_id => device.id})]
- notifications.each_with_index do |notify, i|
- notify.stub(:message_for_sending).and_return("message-#{i}")
- notify.should_receive(:sent_at=).with(instance_of(Time))
- notify.should_receive(:save)
- end
-
- APN::App.should_receive(:all).and_return([app])
- app.should_receive(:unsent_notifications).at_least(:once).and_return(notifications)
+
+ notifications.each_with_index do |notify, i|
+ notify.stub(:message_for_sending).and_return("message-#{i}")
+ notify.should_receive(:sent_at=).with(instance_of(Time))
+ notify.should_receive(:save)
+ end
+
+ APN::App.should_receive(:all).once.and_return([app])
app.should_receive(:cert).twice.and_return(app.apn_dev_cert)
+ APN::Device.should_receive(:find_each).twice.and_yield(device)
+
+ device.should_receive(:unsent_notifications).and_return(notifications,[])
+
+
ssl_mock = mock('ssl_mock')
ssl_mock.should_receive(:write).with('message-0')
ssl_mock.should_receive(:write).with('message-1')
- APN::Connection.should_receive(:open_for_delivery).and_yield(ssl_mock, nil)
-
+ APN::Connection.should_receive(:open_for_delivery).twice.and_yield(ssl_mock, nil)
APN::App.send_notifications
end
end
+ describe 'send_notifications_not_associated_with_an_app' do
+
+ it 'should send unsent notifications that are associated with devices that are not with any app' do
+ RAILS_ENV = 'staging'
+ device = DeviceFactory.create
+ device.app_id = nil
+ device.save
+ APN::App.all.each { |a| a.destroy }
+ notifications = [NotificationFactory.create({:device_id => device.id}),
+ NotificationFactory.create({:device_id => device.id})]
+
+ notifications.each_with_index do |notify, i|
+ notify.stub(:message_for_sending).and_return("message-#{i}")
+ notify.should_receive(:sent_at=).with(instance_of(Time))
+ notify.should_receive(:save)
+ end
+
+ APN::Device.should_receive(:find_each).and_yield(device)
+ device.should_receive(:unsent_notifications).and_return(notifications)
+
+ ssl_mock = mock('ssl_mock')
+ ssl_mock.should_receive(:write).with('message-0')
+ ssl_mock.should_receive(:write).with('message-1')
+ APN::Connection.should_receive(:open_for_delivery).and_yield(ssl_mock, nil)
+ APN::App.send_notifications
+ end
+ end
+
describe 'send_group_notifications' do
it 'should send the unsent group notifications' do
@@ -133,7 +165,8 @@
app = AppFactory.create
devices = [DeviceFactory.create(:app_id => app.id, :last_registered_at => 1.week.ago, :feedback_at => Time.now),
DeviceFactory.create(:app_id => app.id, :last_registered_at => 1.week.from_now, :feedback_at => Time.now)]
- APN::Feedback.should_receive(:devices).and_return(devices)
+ puts "device ids are #{devices[0].id} and #{devices[1].id}"
+ APN::Feedback.should_receive(:devices).twice.and_return(devices)
APN::App.should_receive(:all).and_return([app])
app.should_receive(:cert).twice.and_return(app.apn_dev_cert)
lambda {
@@ -143,6 +176,20 @@
end
+ describe 'process_devices for global app' do
+
+ it 'should destroy devices that have a last_registered_at date that is before the feedback_at date that have no app' do
+ device = DeviceFactory.create(:app_id => nil, :last_registered_at => 1.week.ago, :feedback_at => Time.now)
+ device.app_id = nil
+ device.save
+ APN::Feedback.should_receive(:devices).and_return([device])
+ APN::App.should_receive(:all).and_return([])
+ lambda {
+ APN::App.process_devices
+ }.should change(APN::Device, :count).by(-1)
+ end
+ end
+
describe 'nil cert when processing devices' do
it 'should raise an exception for processing devices for an app with no cert' do
View
12 spec/apn_on_rails/app/models/apn/device_spec.rb
@@ -40,19 +40,19 @@
end
- describe 'before_save' do
+ describe 'before_create' do
- it 'should set the last_registered_at date to Time.now if nil' do
+ it 'should set the last_registered_at date to Time.now' do
time = Time.now
Time.stub(:now).and_return(time)
device = DeviceFactory.create
device.last_registered_at.should_not be_nil
device.last_registered_at.to_s.should == time.to_s
- ago = 1.week.ago
- device = DeviceFactory.create(:last_registered_at => ago)
- device.last_registered_at.should_not be_nil
- device.last_registered_at.to_s.should == ago.to_s
+ # ago = 1.week.ago
+ # device = DeviceFactory.create(:last_registered_at => ago)
+ # device.last_registered_at.should_not be_nil
+ # device.last_registered_at.to_s.should == ago.to_s
end
end
View
6 spec/factories/app_factory.rb
@@ -9,9 +9,9 @@ def new(options = {})
end
def create(options = {})
- device = AppFactory.new(options)
- device.save
- return device
+ app = AppFactory.new(options)
+ app.save
+ return app
end
def random_cert
Please sign in to comment.
Something went wrong with that request. Please try again.