Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

working on tests, adding in all my changes

  • Loading branch information...
commit 43c8492655f2ac3a9d4ecc355564c4c0183fe5bc 1 parent f94d0b3
@PRXci PRXci authored
Showing with 833 additions and 128 deletions.
  1. +80 −0 .specification
  2. +51 −9 README
  3. +78 −21 README.textile
  4. +18 −0 generators/templates/apn_migrations/004_create_apn_apps.rb
  5. +23 −0 generators/templates/apn_migrations/005_create_groups.rb
  6. +11 −0 generators/templates/apn_migrations/006_alter_apn_groups.rb
  7. +27 −0 generators/templates/apn_migrations/007_create_device_groups.rb
  8. +23 −0 generators/templates/apn_migrations/008_create_apn_group_notifications.rb
  9. +16 −0 generators/templates/apn_migrations/009_create_in_app_notifications.rb
  10. +10 −6 lib/apn_on_rails/apn_on_rails.rb
  11. +114 −0 lib/apn_on_rails/app/models/apn/app.rb
  12. +3 −1 lib/apn_on_rails/app/models/apn/device.rb
  13. +16 −0 lib/apn_on_rails/app/models/apn/device_grouping.rb
  14. +12 −0 lib/apn_on_rails/app/models/apn/group.rb
  15. +79 −0 lib/apn_on_rails/app/models/apn/group_notification.rb
  16. +15 −0 lib/apn_on_rails/app/models/apn/in_app_notification.rb
  17. +5 −29 lib/apn_on_rails/app/models/apn/notification.rb
  18. +2 −1  lib/apn_on_rails/libs/connection.rb
  19. +6 −18 lib/apn_on_rails/libs/feedback.rb
  20. +13 −4 lib/apn_on_rails/tasks/apn.rake
  21. +127 −0 spec/apn_on_rails/app/models/apn/app_spec.rb
  22. +0 −22 spec/apn_on_rails/app/models/apn/notification_spec.rb
  23. +1 −1  spec/apn_on_rails/libs/connection_spec.rb
  24. +3 −15 spec/apn_on_rails/libs/feedback_spec.rb
  25. +27 −0 spec/factories/app_factory.rb
  26. +2 −1  spec/factories/device_factory.rb
  27. +22 −0 spec/factories/device_grouping_factory.rb
  28. +27 −0 spec/factories/group_factory.rb
  29. +22 −0 spec/factories/group_notification_factory.rb
View
80 .specification
@@ -0,0 +1,80 @@
+--- !ruby/object:Gem::Specification
+name: apn_on_rails
+version: !ruby/object:Gem::Version
+ version: 0.3.1
+platform: ruby
+authors:
+- markbates
+autorequire:
+bindir: bin
+cert_chain: []
+
+date: 2010-01-26 00:00:00 -05:00
+default_executable:
+dependencies:
+- !ruby/object:Gem::Dependency
+ name: configatron
+ type: :runtime
+ version_requirement:
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: "0"
+ version:
+description: "apn_on_rails was developed by: markbates"
+email: mark@markbates.com
+executables: []
+
+extensions: []
+
+extra_rdoc_files:
+- README
+- LICENSE
+files:
+- lib/apn_on_rails/apn_on_rails.rb
+- lib/apn_on_rails/app/models/apn/base.rb
+- lib/apn_on_rails/app/models/apn/device.rb
+- lib/apn_on_rails/app/models/apn/notification.rb
+- lib/apn_on_rails/libs/connection.rb
+- lib/apn_on_rails/libs/feedback.rb
+- lib/apn_on_rails/tasks/apn.rake
+- lib/apn_on_rails/tasks/db.rake
+- lib/apn_on_rails.rb
+- lib/apn_on_rails_tasks.rb
+- README
+- LICENSE
+- generators/apn_migrations_generator.rb
+- generators/templates/apn_migrations/001_create_apn_devices.rb
+- generators/templates/apn_migrations/002_create_apn_notifications.rb
+- generators/templates/apn_migrations/003_alter_apn_devices.rb
+has_rdoc: true
+homepage: http://www.metabates.com
+licenses: []
+
+post_install_message:
+rdoc_options: []
+
+require_paths:
+- lib
+required_ruby_version: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: "0"
+ version:
+required_rubygems_version: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: "0"
+ version:
+requirements: []
+
+rubyforge_project: magrathea
+rubygems_version: 1.3.5
+signing_key:
+specification_version: 3
+summary: apn_on_rails
+test_files: []
+
View
60 README
@@ -1,15 +1,49 @@
=APN on Rails (Apple Push Notifications on Rails)
APN on Rails is a Ruby on Rails gem that allows you to easily add Apple Push Notification (iPhone)
-support to your Rails application.
+support to your Rails application.
+
+It supports:
+* Multiple iPhone apps managed from the same Rails application
+* Individual notifications and group notifications
+* Alerts, badges, sounds, and custom properties in notifications
+* In-App (or pull) notifications
+
+== 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.
+
+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.
+
+Notification Content Areas: Notifications may contain alerts, badges, sounds, and custom properties.
+
+In-App or Pull Notifications: This version of the gem supports an alternative notification method that relies
+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. In-App or Pull notifications may be
+created for an app. A client app can query for the most recent in-app notification available since a
+given date to retrieve any notifications waiting for it.
==Acknowledgements:
+From Mark Bates:
+
This gem is a re-write of a plugin that was written by Fabien Penso and Sam Soffes.
Their plugin was a great start, but it just didn't quite reach the level I hoped it would.
I've re-written, as a gem, added a ton of tests, and I would like to think that I made it
a little nicer and easier to use.
+From Rebecca Nesson (PRX.org):
+
+This gem extends the original version that Mark Bates adapted. His gem did the hard
+work of setting up and handling all communication with the Apple push notification servers.
+
==Converting Your Certificate:
Once you have the certificate from Apple for your application, export your key
@@ -23,10 +57,10 @@ Now covert the p12 file to a pem file:
$ openssl pkcs12 -in cert.p12 -out apple_push_notification_production.pem -nodes -clcerts
-Put 'apple_push_notification_production.pem' in config/
-
If you are using a development certificate, then change the name to apple_push_notification_development.pem instead.
+Store the contents of the certificate files on the app model for the app you want to send notifications to.
+
==Installing:
===Stable (RubyForge):
@@ -35,7 +69,7 @@ If you are using a development certificate, then change the name to apple_push_n
===Edge (GitHub):
- $ sudo gem install markbates-apn_on_rails --source=http://gems.github.com
+ $ sudo gem install PRX-apn_on_rails.git --source=http://gems.github.com
===Rails Gem Management:
@@ -45,7 +79,7 @@ If you like to use the built in Rails gem management:
Or, if you like to live on the edge:
- config.gem 'markbates-apn_on_rails', :lib => 'apn_on_rails', :source => 'http://gems.github.com'
+ config.gem 'PRX-apn_on_rails', :lib => 'apn_on_rails', :source => 'http://gems.github.com'
==Setup and Configuration:
@@ -70,7 +104,9 @@ Now, to create the tables you need for APN on Rails, run the following task:
$ ruby script/generate apn_migrations
APN on Rails uses the Configatron gem, http://github.com/markbates/configatron/tree/master,
-to configure itself. APN on Rails has the following default configurations that you change as you
+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:
# development (delivery):
@@ -103,21 +139,27 @@ If you are upgrading to a new version of APN on Rails you should always run:
That way you ensure you have the latest version of the database tables needed.
-==Example:
+==Example (assuming you have created an app and stored your keys on it):
$ ./script/console
- >> device = APN::Device.create(:token => "XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX")
+ >> app = APN::App.find(:first)
+ >> device = APN::Device.create(:token => "XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX",:app_id => app.id)
>> notification = APN::Notification.new
>> notification.device = device
>> notification.badge = 5
>> notification.sound = true
>> notification.alert = "foobar"
+ >> notification.custom_properties = {:link => "http://www.prx.org"}
>> notification.save
-You can use the following Rake task to deliver your notifications:
+You can use the following Rake task to deliver your individual notifications:
$ rake apn:notifications:deliver
+And the following task to deliver your group notifications:
+
+ $ rake apn:group_notifications:deliver
+
The Rake task will find any unsent notifications in the database. If there aren't any notifications
it will simply do nothing. If there are notifications waiting to be delivered it will open a single connection
to Apple and push all the notifications through that one connection. Apple does not like people opening/closing
View
99 README.textile
@@ -1,15 +1,49 @@
h1. APN on Rails (Apple Push Notifications on Rails)
APN on Rails is a Ruby on Rails gem that allows you to easily add Apple Push Notification (iPhone)
-support to your Rails application.
+support to your Rails application.
+
+It supports:
+* Multiple iPhone apps managed from the same Rails application
+* Individual notifications and group notifications
+* Alerts, badges, sounds, and custom properties in notifications
+* In-App (or pull) notifications
+
+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.
+
+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.
+
+Notification Content Areas: Notifications may contain alerts, badges, sounds, and custom properties.
+
+In-App or Pull Notifications: This version of the gem supports an alternative notification method that relies
+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. In-App or Pull notifications may be
+created for an app. A client app can query for the most recent in-app notification available since a
+given date to retrieve any notifications waiting for it.
h2. Acknowledgements:
+From Mark Bates:
+
This gem is a re-write of a plugin that was written by Fabien Penso and Sam Soffes.
Their plugin was a great start, but it just didn't quite reach the level I hoped it would.
I've re-written, as a gem, added a ton of tests, and I would like to think that I made it
a little nicer and easier to use.
+From Rebecca Nesson (PRX.org):
+
+This gem extends the original version that Mark Bates adapted. His gem did the hard
+work of setting up and handling all communication with the Apple push notification servers.
+
h2. Converting Your Certificate:
Once you have the certificate from Apple for your application, export your key
@@ -20,37 +54,42 @@ and the apple certificate as p12 files. Here is a quick walkthrough on how to do
3. Choose the p12 format from the drop down and name it `cert.p12`.
Now covert the p12 file to a pem file:
+
<pre><code>
$ openssl pkcs12 -in cert.p12 -out apple_push_notification_production.pem -nodes -clcerts
-</code></pre>
-
-Put 'apple_push_notification_production.pem' in config/
+</pre></code>
If you are using a development certificate, then change the name to apple_push_notification_development.pem instead.
+Store the contents of the certificate files on the app model for the app you want to send notifications to.
+
h2. Installing:
h3. Stable (RubyForge):
+
<pre><code>
$ sudo gem install apn_on_rails
-</code></pre>
+</pre></code>
h3. Edge (GitHub):
+
<pre><code>
- $ sudo gem install markbates-apn_on_rails --source=http://gems.github.com
-</code></pre>
+ $ sudo gem install PRX-apn_on_rails.git --source=http://gems.github.com
+</pre></code>
h3. Rails Gem Management:
If you like to use the built in Rails gem management:
+
<pre><code>
config.gem 'apn_on_rails'
-</code></pre>
+</pre></code>
Or, if you like to live on the edge:
+
<pre><code>
- config.gem 'markbates-apn_on_rails', :lib => 'apn_on_rails', :source => 'http://gems.github.com'
-</code></pre>
+ config.gem 'PRX-apn_on_rails', :lib => 'apn_on_rails', :source => 'http://gems.github.com'
+</pre></code>
h2. Setup and Configuration:
@@ -58,27 +97,32 @@ Once you have the gem installed via your favorite gem installation, you need to
start to use it:
Add the following require, wherever it makes sense to you:
+
<pre><code>
require 'apn_on_rails'
-</code></pre>
+</pre></code>
You also need to add the following to your Rakefile so you can use the
Rake tasks that ship with APN on Rails:
+
<pre><code>
begin
require 'apn_on_rails_tasks'
rescue MissingSourceFile => e
puts e.message
end
-</code></pre>
+</pre></code>
Now, to create the tables you need for APN on Rails, run the following task:
+
<pre><code>
$ ruby script/generate apn_migrations
-</code></pre>
+</pre></code>
APN on Rails uses the Configatron gem, http://github.com/markbates/configatron/tree/master,
-to configure itself. APN on Rails has the following default configurations that you change as you
+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:
<pre><code>
@@ -101,36 +145,49 @@ see fit:
# production (feedback):
configatron.apn.feedback.host # => 'feedback.push.apple.com'
configatron.apn.feedback.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_production.pem')
-</code></pre>
+</pre></code>
That's it, now you're ready to start creating notifications.
h3. Upgrade Notes:
If you are upgrading to a new version of APN on Rails you should always run:
+
<pre><code>
$ ruby script/generate apn_migrations
-</code></pre>
+</pre></code>
That way you ensure you have the latest version of the database tables needed.
+(There is an unaddressed problem in which migration 002 was modified in the repo to add the column custom_properties.
+If you installed the gem prior to that change and try to upgrade following this path you will have to add the
+custom_properties column to the apn_notifications table by hand.)
-h2. Example:
+h2. Example (assuming you have created an app and stored your keys on it):
<pre><code>
$ ./script/console
- >> device = APN::Device.create(:token => "XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX")
+ >> app = APN::App.find(:first)
+ >> device = APN::Device.create(:token => "XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX",:app_id => app.id)
>> notification = APN::Notification.new
>> notification.device = device
>> notification.badge = 5
>> notification.sound = true
>> notification.alert = "foobar"
+ >> notification.custom_properties = {:link => "http://www.prx.org"}
>> notification.save
-</code></pre>
+</pre></code>
+
+You can use the following Rake task to deliver your individual notifications:
-You can use the following Rake task to deliver your notifications:
<pre><code>
$ rake apn:notifications:deliver
-</code></pre>
+</pre></code>
+
+And the following task to deliver your group notifications:
+
+<pre><code>
+ $ rake apn:group_notifications:deliver
+</pre></code>
The Rake task will find any unsent notifications in the database. If there aren't any notifications
it will simply do nothing. If there are notifications waiting to be delivered it will open a single connection
View
18 generators/templates/apn_migrations/004_create_apn_apps.rb
@@ -0,0 +1,18 @@
+class CreateApnApps < ActiveRecord::Migration # :nodoc:
+ def self.up
+ create_table :apn_apps do |t|
+ t.text :apn_dev_cert
+ t.text :apn_prod_cert
+
+ t.timestamps
+ end
+
+ add_column :apn_devices, :app_id, :integer
+
+ end
+
+ def self.down
+ drop_table :apps
+ remove_column :apn_devices, :app_id
+ end
+end
View
23 generators/templates/apn_migrations/005_create_groups.rb
@@ -0,0 +1,23 @@
+class CreateGroups < ActiveRecord::Migration # :nodoc:
+ def self.up
+ create_table :apn_groups do |t|
+ t.column :name, :string
+
+ t.timestamps
+ end
+
+ create_table :apn_devices_apn_groups, :id => false do |t|
+ t.column :group_id, :integer
+ t.column :device_id, :integer
+ end
+
+ add_index :apn_devices_apn_groups, [:group_id, :device_id]
+ add_index :apn_devices_apn_groups, :device_id
+ add_index :apn_devices_apn_groups, :group_id
+ end
+
+ def self.down
+ drop_table :apn_groups
+ drop_table :apn_devices_apn_groups
+ end
+end
View
11 generators/templates/apn_migrations/006_alter_apn_groups.rb
@@ -0,0 +1,11 @@
+class AlterApnGroups < ActiveRecord::Migration # :nodoc:
+
+ def self.up
+ add_column :apn_groups, :app_id, :integer
+ end
+
+ def self.down
+ remove_column :apn_groups, :app_id
+ end
+
+end
View
27 generators/templates/apn_migrations/007_create_device_groups.rb
@@ -0,0 +1,27 @@
+class CreateDeviceGroups < ActiveRecord::Migration # :nodoc:
+ def self.up
+ drop_table :apn_devices_apn_groups
+
+ create_table :apn_device_groupings do |t|
+ t.column :group_id, :integer
+ t.column :device_id, :integer
+ end
+
+ add_index :apn_device_groupings, [:group_id, :device_id]
+ add_index :apn_device_groupings, :device_id
+ add_index :apn_device_groupings, :group_id
+ end
+
+ def self.down
+ drop_table :apn_device_groupings
+
+ create_table :apn_devices_apn_groups, :id => false do |t|
+ t.column :group_id, :integer
+ t.column :device_id, :integer
+ end
+
+ add_index :apn_devices_apn_groups, [:group_id, :device_id]
+ add_index :apn_devices_apn_groups, :device_id
+ add_index :apn_devices_apn_groups, :group_id
+ end
+end
View
23 generators/templates/apn_migrations/008_create_apn_group_notifications.rb
@@ -0,0 +1,23 @@
+class CreateApnGroupNotifications < ActiveRecord::Migration # :nodoc:
+
+ def self.up
+
+ create_table :apn_group_notifications do |t|
+ t.integer :group_id, :null => false
+ t.string :device_language, :size => 5 # if you don't want to send localized strings
+ t.string :sound
+ t.string :alert, :size => 150
+ t.integer :badge
+ t.text :custom_properties
+ t.datetime :sent_at
+ t.timestamps
+ end
+
+ add_index :apn_group_notifications, :group_id
+ end
+
+ def self.down
+ drop_table :apn_group_notifications
+ end
+
+end
View
16 generators/templates/apn_migrations/009_create_in_app_notifications.rb
@@ -0,0 +1,16 @@
+class CreateInAppNotifications < ActiveRecord::Migration
+ def self.up
+ create_table :apn_in_app_notifications do |t|
+ t.integer :app_id
+ t.string :title
+ t.string :content
+ t.string :link
+
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :apn_in_app_notifications
+ end
+end
View
16 lib/apn_on_rails/apn_on_rails.rb
@@ -45,10 +45,19 @@ def initialize(message) # :nodoc:
end
+ class MissingCertificateError < StandardError
+ def initialize
+ super("This app has no certificate")
+ end
+ end
+
end # Errors
end # APN
+base = File.join(File.dirname(__FILE__), 'app', 'models', 'apn', 'base.rb')
+require base
+
Dir.glob(File.join(File.dirname(__FILE__), 'app', 'models', 'apn', '*.rb')).sort.each do |f|
require f
end
@@ -57,11 +66,6 @@ def initialize(message) # :nodoc:
path = File.join(File.dirname(__FILE__), 'app', dir)
$LOAD_PATH << path
# puts "Adding #{path}"
- begin
ActiveSupport::Dependencies.load_paths << path
ActiveSupport::Dependencies.load_once_paths.delete(path)
- rescue NameError
- Dependencies.load_paths << path
- Dependencies.load_once_paths.delete(path)
- end
-end
+end
View
114 lib/apn_on_rails/app/models/apn/app.rb
@@ -0,0 +1,114 @@
+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)
+ end
+
+ # Opens a connection to the Apple APN server and attempts to batch deliver
+ # an Array of group notifications.
+ #
+ #
+ # As each APN::GroupNotification is sent the <tt>sent_at</tt> column will be timestamped,
+ # so as to not be sent again.
+ #
+ def send_notifications
+ if self.cert.nil?
+ 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.each do |noty|
+ conn.write(noty.message_for_sending)
+ noty.sent_at = Time.now
+ noty.save
+ end
+ end
+ end
+ end
+
+ def self.send_notifications
+ apps = APN::App.all
+ apps.each do |app|
+ app.send_notifications
+ end
+ end
+
+ def send_group_notifications
+ 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.each do |device|
+ conn.write(gnoty.message_for_sending(device))
+ end
+ gnoty.sent_at = Time.now
+ gnoty.save
+ end
+ end
+ end
+ end
+
+ def send_group_notification(gnoty)
+ if self.cert.nil?
+ raise APN::Errors::MissingCertificateError.new
+ return
+ end
+ unless gnoty.nil?
+ APN::Connection.open_for_delivery({:cert => self.cert}) do |conn, sock|
+ gnoty.devices.each do |device|
+ conn.write(gnoty.message_for_sending(device))
+ end
+ gnoty.sent_at = Time.now
+ gnoty.save
+ end
+ end
+ end
+
+ def self.send_group_notifications
+ apps = APN::App.all
+ apps.each do |app|
+ app.send_group_notifications
+ end
+ end
+
+ # Retrieves a list of APN::Device instnces from Apple using
+ # the <tt>devices</tt> method. It then checks to see if the
+ # <tt>last_registered_at</tt> date of each APN::Device is
+ # before the date that Apple says the device is no longer
+ # accepting notifications then the device is deleted. Otherwise
+ # it is assumed that the application has been re-installed
+ # and is available for notifications.
+ #
+ # This can be run from the following Rake task:
+ # $ rake apn:feedback:process
+ def process_devices
+ if self.cert.nil?
+ 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
+ end # process_devices
+
+ def self.process_devices
+ apps = APN::App.all
+ apps.each do |app|
+ app.process_devices
+ end
+ end
+
+end
View
4 lib/apn_on_rails/app/models/apn/device.rb
@@ -10,9 +10,11 @@
# Device.create(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz')
class APN::Device < APN::Base
+ belongs_to :app, :class_name => 'APN::App'
has_many :notifications, :class_name => 'APN::Notification'
+ has_many :unsent_notifications, :class_name => 'APN::Notification', :conditions => 'sent_at is null'
- validates_uniqueness_of :token
+ 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
View
16 lib/apn_on_rails/app/models/apn/device_grouping.rb
@@ -0,0 +1,16 @@
+class APN::DeviceGrouping < APN::Base
+
+ belongs_to :group, :class_name => 'APN::Group'
+ belongs_to :device, :class_name => 'APN::Device'
+
+ validates_presence_of :device_id, :group_id
+ validate :same_app_id
+ validates_uniqueness_of :device_id, :scope => :group_id
+
+ def same_app_id
+ unless self.group and self.device and self.group.app_id == self.device.app_id
+ errors.add_to_base("device and group must belong to the same app")
+ end
+ end
+
+end
View
12 lib/apn_on_rails/app/models/apn/group.rb
@@ -0,0 +1,12 @@
+class APN::Group < APN::Base
+
+ belongs_to :app, :class_name => 'APN::App'
+ has_many :device_groupings, :class_name => "APN::DeviceGrouping", :dependent => :destroy
+ has_many :devices, :class_name => 'APN::Device', :through => :device_groupings
+ has_many :group_notifications, :class_name => 'APN::GroupNotification'
+ has_many :unsent_group_notifications, :class_name => 'APN::GroupNotification', :conditions => 'sent_at is null'
+
+ validates_presence_of :app_id
+ validates_uniqueness_of :name, :scope => :app_id
+
+end
View
79 lib/apn_on_rails/app/models/apn/group_notification.rb
@@ -0,0 +1,79 @@
+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
+
+ 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
+ # to 150 characters with a <tt>...</tt>
+ def alert=(message)
+ if !message.blank? && message.size > 150
+ message = truncate(message, :length => 150)
+ end
+ write_attribute('alert', message)
+ end
+
+ # Creates a Hash that will be the payload of an APN.
+ #
+ # Example:
+ # apn = APN::Notification.new
+ # apn.badge = 5
+ # apn.sound = 'my_sound.aiff'
+ # apn.alert = 'Hello!'
+ # apn.apple_hash # => {"aps" => {"badge" => 5, "sound" => "my_sound.aiff", "alert" => "Hello!"}}
+ #
+ # Example 2:
+ # apn = APN::Notification.new
+ # apn.badge = 0
+ # apn.sound = true
+ # apn.custom_properties = {"typ" => 1}
+ # apn.apple_hast # => {"aps" => {"badge" => 0}}
+ def apple_hash
+ result = {}
+ result['aps'] = {}
+ 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'] = "1.aiff" if self.sound.is_a?(TrueClass)
+ end
+ if self.custom_properties
+ self.custom_properties.each do |key,value|
+ result["#{key}"] = "#{value}"
+ end
+ end
+ result
+ end
+
+ # Creates the JSON string required for an APN message.
+ #
+ # Example:
+ # apn = APN::Notification.new
+ # apn.badge = 5
+ # apn.sound = 'my_sound.aiff'
+ # apn.alert = 'Hello!'
+ # apn.to_apple_json # => '{"aps":{"badge":5,"sound":"my_sound.aiff","alert":"Hello!"}}'
+ 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
+ message = "\0\0 #{device.to_hexa}\0#{json.length.chr}#{json}"
+ raise APN::Errors::ExceededMessageSizeError.new(message) if message.size.to_i > 256
+ message
+ end
+
+end # APN::Notification
View
15 lib/apn_on_rails/app/models/apn/in_app_notification.rb
@@ -0,0 +1,15 @@
+class APN::InAppNotification < APN::Base
+ belongs_to :app, :class_name => 'APN::App'
+
+ validates_presence_of :app_id
+
+ def self.latest_since(app_id, since_date=nil)
+ conditions = if since_date
+ ["app_id = ? AND created_at > since_date", app_id, since_date]
+ else
+ ["app_id = ?", app_id]
+ end
+
+ first(:order => "created_at DESC", :conditions => conditions)
+ end
+end
View
34 lib/apn_on_rails/app/models/apn/notification.rb
@@ -20,6 +20,7 @@ class APN::Notification < APN::Base
serialize :custom_properties
belongs_to :device, :class_name => 'APN::Device'
+ has_one :app, :class_name => 'APN::App', :through => :device
# Stores the text alert message you want to send to the device.
#
@@ -84,34 +85,9 @@ def message_for_sending
message
end
- class << self
-
- # Opens a connection to the Apple APN server and attempts to batch deliver
- # an Array of notifications.
- #
- # This method expects an Array of APN::Notifications. If no parameter is passed
- # in then it will use the following:
- # APN::Notification.all(:conditions => {:sent_at => nil})
- #
- # As each APN::Notification is sent the <tt>sent_at</tt> column will be timestamped,
- # so as to not be sent again.
- #
- # This can be run from the following Rake task:
- # $ rake apn:notifications:deliver
- def send_notifications(notifications = APN::Notification.all(:conditions => {:sent_at => nil}))
- unless notifications.nil? || notifications.empty?
-
- APN::Connection.open_for_delivery do |conn, sock|
- notifications.each do |noty|
- conn.write(noty.message_for_sending)
- noty.sent_at = Time.now
- noty.save
- end
- end
-
- end
- end
-
- end # class << self
+ def self.send_notifications
+ ActiveSupport::Deprecation.warn("The method APN::Notification.send_notifications is deprecated. Use APN::App.send_notifications instead.")
+ APN::App.send_notfications
+ end
end # APN::Notification
View
3  lib/apn_on_rails/libs/connection.rb
@@ -47,7 +47,8 @@ def open(options = {}, &block) # :nodoc:
:passphrase => configatron.apn.passphrase,
:host => configatron.apn.host,
:port => configatron.apn.port}.merge(options)
- cert = File.read(options[:cert])
+ #cert = File.read(options[:cert])
+ cert = options[:cert]
ctx = OpenSSL::SSL::SSLContext.new
ctx.key = OpenSSL::PKey::RSA.new(cert, options[:passphrase])
ctx.cert = OpenSSL::X509::Certificate.new(cert)
View
24 lib/apn_on_rails/libs/feedback.rb
@@ -10,9 +10,10 @@ class << self
# has received feedback from Apple. Each APN::Device will
# have it's <tt>feedback_at</tt> accessor marked with the time
# that Apple believes the device de-registered itself.
- def devices(&block)
+ def devices(cert, &block)
devices = []
- APN::Connection.open_for_feedback do |conn, sock|
+ return if cert.nil?
+ APN::Connection.open_for_feedback({:cert => cert}) do |conn, sock|
while line = sock.gets # Read lines from the socket
line.strip!
feedback = line.unpack('N1n1H140')
@@ -28,23 +29,10 @@ def devices(&block)
return devices
end # devices
- # Retrieves a list of APN::Device instnces from Apple using
- # the <tt>devices</tt> method. It then checks to see if the
- # <tt>last_registered_at</tt> date of each APN::Device is
- # before the date that Apple says the device is no longer
- # accepting notifications then the device is deleted. Otherwise
- # it is assumed that the application has been re-installed
- # and is available for notifications.
- #
- # This can be run from the following Rake task:
- # $ rake apn:feedback:process
def process_devices
- APN::Feedback.devices.each do |device|
- if device.last_registered_at < device.feedback_at
- device.destroy
- end
- end
- end # process_devices
+ ActiveSupport::Deprecation.warn("The method APN::Feedback.process_devices is deprecated. Use APN::App.process_devices instead.")
+ APN::App.process_devices
+ end
end # class << self
View
17 lib/apn_on_rails/tasks/apn.rake
@@ -4,18 +4,27 @@ namespace :apn do
desc "Deliver all unsent APN notifications."
task :deliver => [:environment] do
- APN::Notification.send_notifications
+ APN::App.send_notifications
end
-
+
end # notifications
+
+ namespace :group_notifications do
+
+ desc "Deliver all unsent APN Group notifications."
+ task :deliver => [:environment] do
+ APN::App.send_group_notifications
+ end
+
+ end # group_notifications
namespace :feedback do
desc "Process all devices that have feedback from APN."
task :process => [:environment] do
- APN::Feedback.process_devices
+ APN::App.process_devices
end
end
-end # apn
+end # apn
View
127 spec/apn_on_rails/app/models/apn/app_spec.rb
@@ -0,0 +1,127 @@
+require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper.rb')
+
+describe APN::App do
+
+ describe 'send_notifications' do
+
+ it 'should send the unsent notifications' do
+
+ app = AppFactory.create
+ 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)
+ app.should_receive(:cert).twice.and_return(app.apn_dev_cert)
+
+ 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
+
+ app = AppFactory.create
+ device = DeviceFactory.create({:app_id => app.id})
+ group = GroupFactory.create({:app_id => app.id})
+ device_grouping = DeviceGroupingFactory.create({:group_id => group.id,:device_id => device.id})
+ puts "device grouping group is #{device_grouping.group.id} device grouping device is #{device_grouping.device.id}"
+ gnotys = [GroupNotificationFactory.create({:group_id => group}),
+ GroupNotificationFactory.create({:group_id => group})]
+ gnotys.each_with_index do |gnoty, i|
+ gnoty.stub(:message_for_sending).at_least(:once).with(device).and_return("message-#{i}")
+ gnoty.should_receive(:sent_at=).with(instance_of(Time))
+ gnoty.should_receive(:save)
+ end
+
+ APN::App.should_receive(:all).and_return([app])
+ app.should_receive(:unsent_group_notifications).at_least(:once).and_return(gnotys)
+ app.should_receive(:cert).twice.and_return(app.apn_dev_cert)
+
+ 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_group_notifications
+
+ end
+
+ end
+
+ describe 'nil cert when sending notifications' do
+
+ it 'should raise an exception for sending notifications for an app with no cert' do
+ app = AppFactory.create
+ APN::App.should_receive(:all).and_return([app])
+ app.should_receive(:cert).and_return(nil)
+ lambda {
+ APN::App.send_notifications
+ }.should raise_error(APN::Errors::MissingCertificateError)
+ end
+
+ end
+
+ describe 'process_devices' do
+
+ it 'should destroy devices that have a last_registered_at date that is before the feedback_at date' do
+ 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)
+ APN::App.should_receive(:all).and_return([app])
+ app.should_receive(:cert).twice.and_return(app.apn_dev_cert)
+ 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
+ app = AppFactory.create
+ APN::App.should_receive(:all).and_return([app])
+ app.should_receive(:cert).and_return(nil)
+ lambda {
+ APN::App.process_devices
+ }.should raise_error(APN::Errors::MissingCertificateError)
+ end
+
+ end
+
+ describe 'cert for production environment' do
+
+ it 'should return the production cert for the app' do
+ app = AppFactory.create
+ RAILS_ENV = 'production'
+ app.cert.should == app.apn_prod_cert
+ end
+
+ end
+
+ describe 'cert for development and staging environment' do
+
+ it 'should return the development cert for the app' do
+ app = AppFactory.create
+ RAILS_ENV = 'staging'
+ app.cert.should == app.apn_dev_cert
+ end
+ end
+
+end
View
22 spec/apn_on_rails/app/models/apn/notification_spec.rb
@@ -56,26 +56,4 @@
end
- describe 'send_notifications' do
-
- it 'should send the notifications in an Array' do
-
- notifications = [NotificationFactory.create, NotificationFactory.create]
- 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
-
- 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::Notification.send_notifications(notifications)
-
- end
-
- end
-
end
View
2  spec/apn_on_rails/libs/connection_spec.rb
@@ -6,7 +6,7 @@
it 'should create a connection to Apple, yield it, and then close' do
rsa_mock = mock('rsa_mock')
- OpenSSL::PKey::RSA.should_receive(:new).with(apn_cert, '').and_return(rsa_mock)
+ OpenSSL::PKey::RSA.should_receive(:new).and_return(rsa_mock)
cert_mock = mock('cert_mock')
OpenSSL::X509::Certificate.should_receive(:new).and_return(cert_mock)
View
18 spec/apn_on_rails/libs/feedback_spec.rb
@@ -7,6 +7,7 @@
before(:each) do
@time = Time.now
@device = DeviceFactory.create
+ @cert = mock('cert_mock')
@data_mock = mock('data_mock')
@data_mock.should_receive(:strip!)
@@ -21,7 +22,7 @@
it 'should an Array of devices that need to be processed' do
APN::Connection.should_receive(:open_for_feedback).and_yield(@ssl_mock, @sock_mock)
- devices = APN::Feedback.devices
+ devices = APN::Feedback.devices(@cert)
devices.size.should == 1
r_device = devices.first
r_device.token.should == @device.token
@@ -31,7 +32,7 @@
it 'should yield up each device' do
APN::Connection.should_receive(:open_for_feedback).and_yield(@ssl_mock, @sock_mock)
lambda {
- APN::Feedback.devices do |r_device|
+ APN::Feedback.devices(@cert) do |r_device|
r_device.token.should == @device.token
r_device.feedback_at.to_s.should == @time.to_s
raise BlockRan.new
@@ -41,17 +42,4 @@
end
- describe 'process_devices' do
-
- it 'should destroy devices that have a last_registered_at date that is before the feedback_at date' do
- devices = [DeviceFactory.create(:last_registered_at => 1.week.ago, :feedback_at => Time.now),
- DeviceFactory.create(:last_registered_at => 1.week.from_now, :feedback_at => Time.now)]
- APN::Feedback.should_receive(:devices).and_return(devices)
- lambda {
- APN::Feedback.process_devices
- }.should change(APN::Device, :count).by(-1)
- end
-
- end
-
end
View
27 spec/factories/app_factory.rb
@@ -0,0 +1,27 @@
+module AppFactory
+
+ class << self
+
+ def new(options = {})
+ options = {:apn_dev_cert => AppFactory.random_cert,
+ :apn_prod_cert => AppFactory.random_cert}.merge(options)
+ return APN::App.new(options)
+ end
+
+ def create(options = {})
+ device = AppFactory.new(options)
+ device.save
+ return device
+ end
+
+ def random_cert
+ tok = []
+ tok << String.randomize(50)
+ tok.join('').downcase
+ end
+
+ end
+
+end
+
+AppFactory.create
View
3  spec/factories/device_factory.rb
@@ -3,7 +3,8 @@ module DeviceFactory
class << self
def new(options = {})
- options = {:token => DeviceFactory.random_token}.merge(options)
+ app = APN::App.first
+ options = {:token => DeviceFactory.random_token, :app_id => app.id}.merge(options)
return APN::Device.new(options)
end
View
22 spec/factories/device_grouping_factory.rb
@@ -0,0 +1,22 @@
+module DeviceGroupingFactory
+
+ class << self
+
+ def new(options = {})
+ device = APN::Device.first
+ group = APN::Group.first
+ options = {:device_id => device.id, :group_id => group.id}.merge(options)
+ return APN::DeviceGrouping.new(options)
+ end
+
+ def create(options = {})
+ device_grouping = DeviceGroupingFactory.new(options)
+ device_grouping.save
+ return device_grouping
+ end
+
+ end
+
+end
+
+DeviceGroupingFactory.create
View
27 spec/factories/group_factory.rb
@@ -0,0 +1,27 @@
+module GroupFactory
+
+ class << self
+
+ def new(options = {})
+ app = AppFactory.create
+ options = {:app_id => app.id, :name => GroupFactory.random_name}.merge(options)
+ return APN::Group.new(options)
+ end
+
+ def create(options = {})
+ group = GroupFactory.new(options)
+ group.save
+ return group
+ end
+
+ def random_name
+ tok = []
+ tok << String.randomize(8)
+ tok.join(' ').downcase
+ end
+
+ end
+
+end
+
+GroupFactory.create
View
22 spec/factories/group_notification_factory.rb
@@ -0,0 +1,22 @@
+module GroupNotificationFactory
+
+ class << self
+
+ def new(options = {})
+ group = APN::Group.first
+ options = {:group_id => group.id, :sound => 'my_sound.aiff',
+ :badge => 5, :alert => 'Hello!'}.merge(options)
+ return APN::GroupNotification.new(options)
+ end
+
+ def create(options = {})
+ notification = GroupNotificationFactory.new(options)
+ notification.save
+ return notification
+ end
+
+ end
+
+end
+
+GroupNotificationFactory.create
Please sign in to comment.
Something went wrong with that request. Please try again.