├── .rspec ├── autotest └── discover.rb ├── lib ├── apn_on_rails │ ├── version.rb │ ├── app │ │ └── models │ │ │ └── apn │ │ │ ├── base.rb │ │ │ ├── device_grouping.rb │ │ │ ├── group.rb │ │ │ ├── pull_notification.rb │ │ │ ├── device.rb │ │ │ ├── group_notification.rb │ │ │ ├── notification.rb │ │ │ └── app.rb │ ├── rails │ │ └── railtie.rb │ ├── tasks │ │ ├── db.rake │ │ └── apn.rake │ ├── libs │ │ ├── feedback.rb │ │ └── connection.rb │ └── apn_on_rails.rb ├── apn_on_rails.rb ├── apn_on_rails_tasks.rb └── generators │ └── apn_on_rails │ └── install │ ├── USAGE │ ├── templates │ ├── 006_alter_apn_groups.rb │ ├── 001_create_apn_devices.rb │ ├── 012_add_launch_notification_to_apn_pull_notifications.rb │ ├── 011_make_device_token_index_nonunique.rb │ ├── 009_create_pull_notifications.rb │ ├── 004_create_apn_apps.rb │ ├── 010_alter_apn_notifications.rb │ ├── 005_create_groups.rb │ ├── 008_create_apn_group_notifications.rb │ ├── 002_create_apn_notifications.rb │ ├── 003_alter_apn_devices.rb │ └── 007_create_device_groups.rb │ └── install_generator.rb ├── spec ├── fixtures │ ├── hexa.bin │ └── message_for_sending.bin ├── extensions │ └── string.rb ├── active_record │ └── setup_ar.rb ├── factories │ ├── device_grouping_factory.rb │ ├── group_factory.rb │ ├── app_factory.rb │ ├── notification_factory.rb │ ├── pull_notification_factory.rb │ ├── group_notification_factory.rb │ └── device_factory.rb ├── rails_root │ └── config │ │ └── apple_push_notification_development.pem ├── apn_on_rails │ ├── libs │ │ ├── connection_spec.rb │ │ └── feedback_spec.rb │ └── app │ │ └── models │ │ └── apn │ │ ├── device_spec.rb │ │ ├── group_notification_spec.rb │ │ ├── notification_spec.rb │ │ ├── pull_notification_spec.rb │ │ └── app_spec.rb └── spec_helper.rb ├── Gemfile ├── .gitignore ├── generators ├── templates │ └── apn_migrations │ │ ├── 006_alter_apn_groups.rb │ │ ├── 001_create_apn_devices.rb │ │ ├── 012_add_launch_notification_to_apn_pull_notifications.rb │ │ ├── 011_make_device_token_index_nonunique.rb │ │ ├── 009_create_pull_notifications.rb │ │ ├── 004_create_apn_apps.rb │ │ ├── 010_alter_apn_notifications.rb │ │ ├── 005_create_groups.rb │ │ ├── 008_create_apn_group_notifications.rb │ │ ├── 002_create_apn_notifications.rb │ │ ├── 003_alter_apn_devices.rb │ │ └── 007_create_device_groups.rb └── apn_migrations_generator.rb ├── Rakefile ├── LICENSE ├── apn_on_rails.gemspec ├── Gemfile.lock ├── .specification ├── README └── README.textile /.rspec: -------------------------------------------------------------------------------- 1 | -f d 2 | --colour 3 | -------------------------------------------------------------------------------- /autotest/discover.rb: -------------------------------------------------------------------------------- 1 | Autotest.add_discovery { "rspec2" } -------------------------------------------------------------------------------- /lib/apn_on_rails/version.rb: -------------------------------------------------------------------------------- 1 | module ApnOnRails 2 | VERSION = "0.5.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/hexa.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/apn_on_rails/master/spec/fixtures/hexa.bin -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in apn_on_rails.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /spec/fixtures/message_for_sending.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/apn_on_rails/master/spec/fixtures/message_for_sending.bin -------------------------------------------------------------------------------- /lib/apn_on_rails.rb: -------------------------------------------------------------------------------- 1 | Dir.glob(File.join(File.dirname(__FILE__), 'apn_on_rails', '**/*.rb')).sort.each do |f| 2 | require File.expand_path(f) 3 | end 4 | 5 | -------------------------------------------------------------------------------- /lib/apn_on_rails_tasks.rb: -------------------------------------------------------------------------------- 1 | Dir.glob(File.join(File.dirname(__FILE__), 'apn_on_rails', 'tasks', '**/*.rake')).each do |f| 2 | load File.expand_path(f) 3 | end -------------------------------------------------------------------------------- /lib/generators/apn_on_rails/install/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generates the migrations necessary for APN on Rails 3 | 4 | Usage: 5 | rails g apn:apn_migrations 6 | 7 | Examples: 8 | rails g apn:apn_migrations 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | doc 4 | tmp 5 | pkg 6 | *.gem 7 | *.pid 8 | coverage 9 | coverage.data 10 | build/* 11 | *.pbxuser 12 | *.mode1v3 13 | .svn 14 | profile 15 | spec/active_record/test.db 16 | .bundle 17 | **/.svn 18 | -------------------------------------------------------------------------------- /lib/apn_on_rails/app/models/apn/base.rb: -------------------------------------------------------------------------------- 1 | module APN 2 | class Base < ActiveRecord::Base # :nodoc: 3 | 4 | self.abstract_class = true 5 | 6 | def self.table_name # :nodoc: 7 | self.to_s.gsub("::", "_").tableize 8 | end 9 | 10 | end 11 | end -------------------------------------------------------------------------------- /generators/templates/apn_migrations/006_alter_apn_groups.rb: -------------------------------------------------------------------------------- 1 | class AlterApnGroups < ActiveRecord::Migration # :nodoc: 2 | 3 | def self.up 4 | add_column :apn_groups, :app_id, :integer 5 | end 6 | 7 | def self.down 8 | remove_column :apn_groups, :app_id 9 | end 10 | 11 | end -------------------------------------------------------------------------------- /lib/generators/apn_on_rails/install/templates/006_alter_apn_groups.rb: -------------------------------------------------------------------------------- 1 | class AlterApnGroups < ActiveRecord::Migration # :nodoc: 2 | 3 | def self.up 4 | add_column :apn_groups, :app_id, :integer 5 | end 6 | 7 | def self.down 8 | remove_column :apn_groups, :app_id 9 | end 10 | 11 | end -------------------------------------------------------------------------------- /spec/extensions/string.rb: -------------------------------------------------------------------------------- 1 | class String 2 | 3 | def self.randomize(length = 10) 4 | chars = ("A".."H").to_a + ("J".."N").to_a + ("P".."T").to_a + ("W".."Z").to_a + ("3".."9").to_a 5 | newpass = "" 6 | 1.upto(length) { |i| newpass << chars[rand(chars.size-1)] } 7 | return newpass.upcase 8 | end 9 | 10 | end -------------------------------------------------------------------------------- /generators/templates/apn_migrations/001_create_apn_devices.rb: -------------------------------------------------------------------------------- 1 | class CreateApnDevices < ActiveRecord::Migration # :nodoc: 2 | def self.up 3 | create_table :apn_devices do |t| 4 | t.text :token, :size => 71, :null => false 5 | 6 | t.timestamps 7 | end 8 | end 9 | 10 | def self.down 11 | drop_table :apn_devices 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/apn_on_rails/rails/railtie.rb: -------------------------------------------------------------------------------- 1 | if defined?(::Rails::Railtie) # backwards compatible 2 | 3 | module APN 4 | module Rails 5 | 6 | class Railtie < ::Rails::Railtie 7 | rake_tasks do 8 | Dir[File.join(File.dirname(__FILE__),'..', 'tasks/*.rake')].each { |f| load f } 9 | end 10 | end 11 | 12 | end 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /generators/templates/apn_migrations/012_add_launch_notification_to_apn_pull_notifications.rb: -------------------------------------------------------------------------------- 1 | class AddLaunchNotificationToApnPullNotifications < ActiveRecord::Migration 2 | def self.up 3 | add_column :apn_pull_notifications, :launch_notification, :boolean 4 | end 5 | 6 | def self.down 7 | remove_column :apn_pull_notifications, :launch_notification 8 | end 9 | end -------------------------------------------------------------------------------- /lib/generators/apn_on_rails/install/templates/001_create_apn_devices.rb: -------------------------------------------------------------------------------- 1 | class CreateApnDevices < ActiveRecord::Migration # :nodoc: 2 | def self.up 3 | create_table :apn_devices do |t| 4 | t.text :token, :size => 71, :null => false 5 | 6 | t.timestamps 7 | end 8 | end 9 | 10 | def self.down 11 | drop_table :apn_devices 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/generators/apn_on_rails/install/templates/012_add_launch_notification_to_apn_pull_notifications.rb: -------------------------------------------------------------------------------- 1 | class AddLaunchNotificationToApnPullNotifications < ActiveRecord::Migration 2 | def self.up 3 | add_column :apn_pull_notifications, :launch_notification, :boolean 4 | end 5 | 6 | def self.down 7 | remove_column :apn_pull_notifications, :launch_notification 8 | end 9 | end -------------------------------------------------------------------------------- /generators/templates/apn_migrations/011_make_device_token_index_nonunique.rb: -------------------------------------------------------------------------------- 1 | class MakeDeviceTokenIndexNonunique < ActiveRecord::Migration 2 | def self.up 3 | remove_index :apn_devices, :column => :token 4 | add_index :apn_devices, :token 5 | end 6 | 7 | def self.down 8 | remove_index :apn_devices, :column => :token 9 | add_index :apn_devices, :token, :unique => true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/apn_on_rails/install/templates/011_make_device_token_index_nonunique.rb: -------------------------------------------------------------------------------- 1 | class MakeDeviceTokenIndexNonunique < ActiveRecord::Migration 2 | def self.up 3 | remove_index :apn_devices, :column => :token 4 | add_index :apn_devices, :token 5 | end 6 | 7 | def self.down 8 | remove_index :apn_devices, :column => :token 9 | add_index :apn_devices, :token, :unique => true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /generators/templates/apn_migrations/009_create_pull_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreatePullNotifications < ActiveRecord::Migration 2 | def self.up 3 | create_table :apn_pull_notifications do |t| 4 | t.integer :app_id 5 | t.string :title 6 | t.string :content 7 | t.string :link 8 | 9 | t.timestamps 10 | end 11 | end 12 | 13 | def self.down 14 | drop_table :apn_pull_notifications 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/apn_on_rails/install/templates/009_create_pull_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreatePullNotifications < ActiveRecord::Migration 2 | def self.up 3 | create_table :apn_pull_notifications do |t| 4 | t.integer :app_id 5 | t.string :title 6 | t.string :content 7 | t.string :link 8 | 9 | t.timestamps 10 | end 11 | end 12 | 13 | def self.down 14 | drop_table :apn_pull_notifications 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/apn_on_rails/tasks/db.rake: -------------------------------------------------------------------------------- 1 | namespace :apn do 2 | 3 | namespace :db do 4 | 5 | task :migrate do 6 | puts %{ 7 | This task no longer exists. Please generate the migrations like this: 8 | 9 | $ ruby script/generate apn_migrations 10 | 11 | or 12 | 13 | $ rails g apn_on_rails:install 14 | 15 | Then just run the migrations like you would normally: 16 | 17 | $ rake db:migrate 18 | }.strip 19 | end 20 | 21 | end # db 22 | 23 | end # apn -------------------------------------------------------------------------------- /lib/generators/apn_on_rails/install/templates/004_create_apn_apps.rb: -------------------------------------------------------------------------------- 1 | class CreateApnApps < ActiveRecord::Migration # :nodoc: 2 | def self.up 3 | create_table :apn_apps do |t| 4 | t.text :apn_dev_cert 5 | t.text :apn_prod_cert 6 | 7 | t.timestamps 8 | end 9 | 10 | add_column :apn_devices, :app_id, :integer 11 | 12 | end 13 | 14 | def self.down 15 | drop_table :apn_apps 16 | remove_column :apn_devices, :app_id 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/apn_on_rails/app/models/apn/device_grouping.rb: -------------------------------------------------------------------------------- 1 | class APN::DeviceGrouping < APN::Base 2 | 3 | belongs_to :group, :class_name => 'APN::Group' 4 | belongs_to :device, :class_name => 'APN::Device' 5 | 6 | validates_presence_of :device_id, :group_id 7 | validate :same_app_id 8 | validates_uniqueness_of :device_id, :scope => :group_id 9 | 10 | def same_app_id 11 | unless self.group and self.device and self.group.app_id == self.device.app_id 12 | errors.add_to_base("device and group must belong to the same app") 13 | end 14 | end 15 | 16 | end -------------------------------------------------------------------------------- /spec/active_record/setup_ar.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'active_record' 3 | require 'logger' 4 | 5 | 6 | logger = Logger.new(STDOUT) 7 | logger.level = Logger::INFO 8 | ActiveRecord::Base.logger = logger 9 | 10 | db_file = File.join(File.dirname(__FILE__), 'test.db') 11 | FileUtils.rm(db_file) if File.exists?(db_file) 12 | 13 | ActiveRecord::Base.establish_connection({ 14 | :adapter => 'sqlite3', 15 | :database => db_file 16 | }) 17 | 18 | ActiveRecord::Migrator.up(File.join(File.dirname(__FILE__), '..', '..', 'generators', 'templates', 'apn_migrations')) 19 | 20 | # raise hell -------------------------------------------------------------------------------- /spec/factories/device_grouping_factory.rb: -------------------------------------------------------------------------------- 1 | module DeviceGroupingFactory 2 | 3 | class << self 4 | 5 | def new(options = {}) 6 | device = APN::Device.first 7 | group = APN::Group.first 8 | options = {:device_id => device.id, :group_id => group.id}.merge(options) 9 | return APN::DeviceGrouping.new(options) 10 | end 11 | 12 | def create(options = {}) 13 | device_grouping = DeviceGroupingFactory.new(options) 14 | device_grouping.save 15 | return device_grouping 16 | end 17 | 18 | end 19 | 20 | end 21 | 22 | DeviceGroupingFactory.create -------------------------------------------------------------------------------- /lib/apn_on_rails/app/models/apn/group.rb: -------------------------------------------------------------------------------- 1 | class APN::Group < APN::Base 2 | 3 | belongs_to :app, :class_name => 'APN::App' 4 | has_many :device_groupings, :class_name => "APN::DeviceGrouping", :dependent => :destroy 5 | has_many :devices, :class_name => 'APN::Device', :through => :device_groupings 6 | has_many :group_notifications, :class_name => 'APN::GroupNotification' 7 | has_many :unsent_group_notifications, :class_name => 'APN::GroupNotification', :conditions => 'sent_at is null' 8 | 9 | validates_presence_of :app_id 10 | validates_uniqueness_of :name, :scope => :app_id 11 | 12 | end -------------------------------------------------------------------------------- /generators/templates/apn_migrations/004_create_apn_apps.rb: -------------------------------------------------------------------------------- 1 | class CreateApnApps < ActiveRecord::Migration # :nodoc: 2 | 3 | module APN # :nodoc: 4 | class Device < ActiveRecord::Base # :nodoc: 5 | set_table_name 'apn_devices' 6 | end 7 | end 8 | 9 | def self.up 10 | create_table :apn_apps do |t| 11 | t.text :apn_dev_cert 12 | t.text :apn_prod_cert 13 | 14 | t.timestamps 15 | end 16 | 17 | add_column :apn_devices, :app_id, :integer 18 | 19 | end 20 | 21 | def self.down 22 | drop_table :apn_apps 23 | remove_column :apn_devices, :app_id 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/factories/group_factory.rb: -------------------------------------------------------------------------------- 1 | module GroupFactory 2 | 3 | class << self 4 | 5 | def new(options = {}) 6 | app = APN::App.first 7 | options = {:app_id => app.id, :name => GroupFactory.random_name}.merge(options) 8 | return APN::Group.new(options) 9 | end 10 | 11 | def create(options = {}) 12 | group = GroupFactory.new(options) 13 | group.save 14 | return group 15 | end 16 | 17 | def random_name 18 | tok = [] 19 | tok << String.randomize(8) 20 | tok.join(' ').downcase 21 | end 22 | 23 | end 24 | 25 | end 26 | 27 | GroupFactory.create -------------------------------------------------------------------------------- /spec/factories/app_factory.rb: -------------------------------------------------------------------------------- 1 | module AppFactory 2 | 3 | class << self 4 | 5 | def new(options = {}) 6 | options = {:apn_dev_cert => AppFactory.random_cert, 7 | :apn_prod_cert => AppFactory.random_cert}.merge(options) 8 | return APN::App.new(options) 9 | end 10 | 11 | def create(options = {}) 12 | app = AppFactory.new(options) 13 | app.save 14 | return app 15 | end 16 | 17 | def random_cert 18 | tok = [] 19 | tok << String.randomize(50) 20 | tok.join('').downcase 21 | end 22 | 23 | end 24 | 25 | end 26 | 27 | AppFactory.create -------------------------------------------------------------------------------- /spec/factories/notification_factory.rb: -------------------------------------------------------------------------------- 1 | module NotificationFactory 2 | 3 | class << self 4 | 5 | def new(options = {}) 6 | device = APN::Device.first 7 | options = {:device_id => device.id, :sound => 'my_sound.aiff', 8 | :badge => 5, :alert => 'Hello!', :custom_properties => {'typ' => 1}}.merge(options) 9 | return APN::Notification.new(options) 10 | end 11 | 12 | def create(options = {}) 13 | notification = NotificationFactory.new(options) 14 | notification.save 15 | return notification 16 | end 17 | 18 | end 19 | 20 | end 21 | 22 | NotificationFactory.create -------------------------------------------------------------------------------- /spec/factories/pull_notification_factory.rb: -------------------------------------------------------------------------------- 1 | module PullNotificationFactory 2 | 3 | class << self 4 | 5 | def new(options = {}) 6 | app = APN::App.first 7 | options = {:app_id => app.id, :title => 'Pull Notification Title', 8 | :content => 'blah blah blah', :link => 'http://www.prx.org', :launch_notification => false}.merge(options) 9 | return APN::PullNotification.new(options) 10 | end 11 | 12 | def create(options = {}) 13 | noty = PullNotificationFactory.new(options) 14 | noty.save 15 | return noty 16 | end 17 | 18 | end 19 | 20 | end 21 | 22 | PullNotificationFactory.create -------------------------------------------------------------------------------- /spec/factories/group_notification_factory.rb: -------------------------------------------------------------------------------- 1 | module GroupNotificationFactory 2 | 3 | class << self 4 | 5 | def new(options = {}) 6 | group = APN::Group.first 7 | options = {:group_id => group.id, :sound => 'my_sound.aiff', 8 | :badge => 5, :alert => 'Hello!', :custom_properties => {'typ' => 1}}.merge(options) 9 | return APN::GroupNotification.new(options) 10 | end 11 | 12 | def create(options = {}) 13 | notification = GroupNotificationFactory.new(options) 14 | notification.save 15 | return notification 16 | end 17 | 18 | end 19 | 20 | end 21 | 22 | GroupNotificationFactory.create -------------------------------------------------------------------------------- /lib/generators/apn_on_rails/install/templates/010_alter_apn_notifications.rb: -------------------------------------------------------------------------------- 1 | class AlterApnNotifications < ActiveRecord::Migration # :nodoc: 2 | 3 | module APN # :nodoc: 4 | class Notification < ActiveRecord::Base # :nodoc: 5 | set_table_name 'apn_notifications' 6 | end 7 | end 8 | 9 | def self.up 10 | unless APN::Notification.column_names.include?("custom_properties") 11 | add_column :apn_notifications, :custom_properties, :text 12 | end 13 | end 14 | 15 | def self.down 16 | if APN::Notification.column_names.include?("custom_properties") 17 | remove_column :apn_notifications, :custom_properties 18 | end 19 | end 20 | 21 | end -------------------------------------------------------------------------------- /spec/factories/device_factory.rb: -------------------------------------------------------------------------------- 1 | module DeviceFactory 2 | 3 | class << self 4 | 5 | def new(options = {}) 6 | app = APN::App.first 7 | options = {:token => DeviceFactory.random_token, :app_id => app.id}.merge(options) 8 | return APN::Device.new(options) 9 | end 10 | 11 | def create(options = {}) 12 | device = DeviceFactory.new(options) 13 | device.save 14 | return device 15 | end 16 | 17 | def random_token 18 | tok = [] 19 | 8.times do 20 | tok << String.randomize(8) 21 | end 22 | tok.join(' ').downcase 23 | end 24 | 25 | end 26 | 27 | end 28 | 29 | DeviceFactory.create -------------------------------------------------------------------------------- /generators/templates/apn_migrations/010_alter_apn_notifications.rb: -------------------------------------------------------------------------------- 1 | class AlterApnNotifications < ActiveRecord::Migration # :nodoc: 2 | 3 | module APN # :nodoc: 4 | class Notification < ActiveRecord::Base # :nodoc: 5 | set_table_name 'apn_notifications' 6 | end 7 | end 8 | 9 | def self.up 10 | # unless APN::Notification.column_names.include?("custom_properties") 11 | add_column(:apn_notifications, :custom_properties, :text) rescue nil 12 | # end 13 | end 14 | 15 | def self.down 16 | # if APN::Notification.column_names.include?("custom_properties") 17 | remove_column(:apn_notifications, :custom_properties) rescue nil 18 | # end 19 | end 20 | 21 | end -------------------------------------------------------------------------------- /generators/templates/apn_migrations/005_create_groups.rb: -------------------------------------------------------------------------------- 1 | class CreateGroups < ActiveRecord::Migration # :nodoc: 2 | def self.up 3 | create_table :apn_groups do |t| 4 | t.column :name, :string 5 | 6 | t.timestamps 7 | end 8 | 9 | create_table :apn_devices_apn_groups, :id => false do |t| 10 | t.column :group_id, :integer 11 | t.column :device_id, :integer 12 | end 13 | 14 | add_index :apn_devices_apn_groups, [:group_id, :device_id] 15 | add_index :apn_devices_apn_groups, :device_id 16 | add_index :apn_devices_apn_groups, :group_id 17 | end 18 | 19 | def self.down 20 | drop_table :apn_groups 21 | drop_table :apn_devices_apn_groups 22 | end 23 | end -------------------------------------------------------------------------------- /generators/templates/apn_migrations/008_create_apn_group_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateApnGroupNotifications < ActiveRecord::Migration # :nodoc: 2 | 3 | def self.up 4 | 5 | create_table :apn_group_notifications do |t| 6 | t.integer :group_id, :null => false 7 | t.string :device_language, :size => 5 # if you don't want to send localized strings 8 | t.string :sound 9 | t.string :alert, :size => 150 10 | t.integer :badge 11 | t.text :custom_properties 12 | t.datetime :sent_at 13 | t.timestamps 14 | end 15 | 16 | add_index :apn_group_notifications, :group_id 17 | end 18 | 19 | def self.down 20 | drop_table :apn_group_notifications 21 | end 22 | 23 | end -------------------------------------------------------------------------------- /lib/generators/apn_on_rails/install/templates/005_create_groups.rb: -------------------------------------------------------------------------------- 1 | class CreateGroups < ActiveRecord::Migration # :nodoc: 2 | def self.up 3 | create_table :apn_groups do |t| 4 | t.column :name, :string 5 | 6 | t.timestamps 7 | end 8 | 9 | create_table :apn_devices_apn_groups, :id => false do |t| 10 | t.column :group_id, :integer 11 | t.column :device_id, :integer 12 | end 13 | 14 | add_index :apn_devices_apn_groups, [:group_id, :device_id] 15 | add_index :apn_devices_apn_groups, :device_id 16 | add_index :apn_devices_apn_groups, :group_id 17 | end 18 | 19 | def self.down 20 | drop_table :apn_groups 21 | drop_table :apn_devices_apn_groups 22 | end 23 | end -------------------------------------------------------------------------------- /lib/generators/apn_on_rails/install/templates/008_create_apn_group_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateApnGroupNotifications < ActiveRecord::Migration # :nodoc: 2 | 3 | def self.up 4 | 5 | create_table :apn_group_notifications do |t| 6 | t.integer :group_id, :null => false 7 | t.string :device_language, :size => 5 # if you don't want to send localized strings 8 | t.string :sound 9 | t.string :alert, :size => 150 10 | t.integer :badge 11 | t.text :custom_properties 12 | t.datetime :sent_at 13 | t.timestamps 14 | end 15 | 16 | add_index :apn_group_notifications, :group_id 17 | end 18 | 19 | def self.down 20 | drop_table :apn_group_notifications 21 | end 22 | 23 | end -------------------------------------------------------------------------------- /generators/templates/apn_migrations/002_create_apn_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateApnNotifications < ActiveRecord::Migration # :nodoc: 2 | 3 | def self.up 4 | 5 | create_table :apn_notifications do |t| 6 | t.integer :device_id, :null => false 7 | t.integer :errors_nb, :default => 0 # used for storing errors from apple feedbacks 8 | t.string :device_language, :size => 5 # if you don't want to send localized strings 9 | t.string :sound 10 | t.string :alert, :size => 150 11 | t.integer :badge 12 | t.datetime :sent_at 13 | t.timestamps 14 | end 15 | 16 | add_index :apn_notifications, :device_id 17 | end 18 | 19 | def self.down 20 | drop_table :apn_notifications 21 | end 22 | 23 | end -------------------------------------------------------------------------------- /lib/generators/apn_on_rails/install/templates/002_create_apn_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateApnNotifications < ActiveRecord::Migration # :nodoc: 2 | 3 | def self.up 4 | 5 | create_table :apn_notifications do |t| 6 | t.integer :device_id, :null => false 7 | t.integer :errors_nb, :default => 0 # used for storing errors from apple feedbacks 8 | t.string :device_language, :size => 5 # if you don't want to send localized strings 9 | t.string :sound 10 | t.string :alert, :size => 150 11 | t.integer :badge 12 | t.datetime :sent_at 13 | t.timestamps 14 | end 15 | 16 | add_index :apn_notifications, :device_id 17 | end 18 | 19 | def self.down 20 | drop_table :apn_notifications 21 | end 22 | 23 | end -------------------------------------------------------------------------------- /lib/apn_on_rails/tasks/apn.rake: -------------------------------------------------------------------------------- 1 | namespace :apn do 2 | 3 | namespace :notifications do 4 | 5 | desc "Deliver all unsent APN notifications." 6 | task :deliver => [:environment] do 7 | APN::App.send_notifications 8 | end 9 | 10 | end # notifications 11 | 12 | namespace :group_notifications do 13 | 14 | desc "Deliver all unsent APN Group notifications." 15 | task :deliver => [:environment] do 16 | APN::App.send_group_notifications 17 | end 18 | 19 | end # group_notifications 20 | 21 | namespace :feedback do 22 | 23 | desc "Process all devices that have feedback from APN." 24 | task :process => [:environment] do 25 | APN::App.process_devices 26 | end 27 | 28 | end 29 | 30 | end # apn 31 | -------------------------------------------------------------------------------- /generators/templates/apn_migrations/003_alter_apn_devices.rb: -------------------------------------------------------------------------------- 1 | class AlterApnDevices < ActiveRecord::Migration # :nodoc: 2 | 3 | module APN # :nodoc: 4 | class Device < ActiveRecord::Base # :nodoc: 5 | set_table_name 'apn_devices' 6 | end 7 | end 8 | 9 | def self.up 10 | add_column :apn_devices, :last_registered_at, :datetime 11 | 12 | execute 'update apn_devices set last_registered_at = created_at' 13 | 14 | change_column :apn_devices, :token, :string, :size => 100, :null => false 15 | add_index :apn_devices, :token, :unique => true 16 | end 17 | 18 | def self.down 19 | change_column :apn_devices, :token, :string 20 | remove_index :apn_devices, :column => :token 21 | remove_column :apn_devices, :last_registered_at 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/generators/apn_on_rails/install/templates/003_alter_apn_devices.rb: -------------------------------------------------------------------------------- 1 | class AlterApnDevices < ActiveRecord::Migration # :nodoc: 2 | 3 | module APN # :nodoc: 4 | class Device < ActiveRecord::Base # :nodoc: 5 | set_table_name 'apn_devices' 6 | end 7 | end 8 | 9 | def self.up 10 | add_column :apn_devices, :last_registered_at, :datetime 11 | 12 | APN::Device.all.each do |device| 13 | device.last_registered_at = device.created_at 14 | device.save! 15 | end 16 | change_column :apn_devices, :token, :string, :size => 100, :null => false 17 | add_index :apn_devices, :token, :unique => true 18 | end 19 | 20 | def self.down 21 | change_column :apn_devices, :token, :string 22 | remove_index :apn_devices, :column => :token 23 | remove_column :apn_devices, :last_registered_at 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /generators/templates/apn_migrations/007_create_device_groups.rb: -------------------------------------------------------------------------------- 1 | class CreateDeviceGroups < ActiveRecord::Migration # :nodoc: 2 | def self.up 3 | drop_table :apn_devices_apn_groups 4 | 5 | create_table :apn_device_groupings do |t| 6 | t.column :group_id, :integer 7 | t.column :device_id, :integer 8 | end 9 | 10 | add_index :apn_device_groupings, [:group_id, :device_id] 11 | add_index :apn_device_groupings, :device_id 12 | add_index :apn_device_groupings, :group_id 13 | end 14 | 15 | def self.down 16 | drop_table :apn_device_groupings 17 | 18 | create_table :apn_devices_apn_groups, :id => false do |t| 19 | t.column :group_id, :integer 20 | t.column :device_id, :integer 21 | end 22 | 23 | add_index :apn_devices_apn_groups, [:group_id, :device_id] 24 | add_index :apn_devices_apn_groups, :device_id 25 | add_index :apn_devices_apn_groups, :group_id 26 | end 27 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | require "bundler/gem_tasks" 4 | 5 | begin 6 | Bundler.setup(:default, :development) 7 | rescue Bundler::BundlerError => e 8 | $stderr.puts e.message 9 | $stderr.puts "Run `bundle install` to install missing gems" 10 | exit e.status_code 11 | end 12 | 13 | require 'rake' 14 | require 'rspec/core' 15 | require 'rspec/core/rake_task' 16 | RSpec::Core::RakeTask.new(:spec) do |spec| 17 | spec.pattern = FileList['spec/**/*_spec.rb'] 18 | end 19 | 20 | RSpec::Core::RakeTask.new(:rcov) do |spec| 21 | spec.pattern = 'spec/**/*_spec.rb' 22 | spec.rcov = true 23 | end 24 | 25 | task :default => :spec 26 | 27 | require 'rake/rdoctask' 28 | Rake::RDocTask.new do |rdoc| 29 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 30 | 31 | rdoc.rdoc_dir = 'rdoc' 32 | rdoc.title = "apn #{version}" 33 | rdoc.rdoc_files.include('README*') 34 | rdoc.rdoc_files.include('lib/**/*.rb') 35 | end 36 | -------------------------------------------------------------------------------- /lib/generators/apn_on_rails/install/templates/007_create_device_groups.rb: -------------------------------------------------------------------------------- 1 | class CreateDeviceGroups < ActiveRecord::Migration # :nodoc: 2 | def self.up 3 | drop_table :apn_devices_apn_groups 4 | 5 | create_table :apn_device_groupings do |t| 6 | t.column :group_id, :integer 7 | t.column :device_id, :integer 8 | end 9 | 10 | add_index :apn_device_groupings, [:group_id, :device_id] 11 | add_index :apn_device_groupings, :device_id 12 | add_index :apn_device_groupings, :group_id 13 | end 14 | 15 | def self.down 16 | drop_table :apn_device_groupings 17 | 18 | create_table :apn_devices_apn_groups, :id => false do |t| 19 | t.column :group_id, :integer 20 | t.column :device_id, :integer 21 | end 22 | 23 | add_index :apn_devices_apn_groups, [:group_id, :device_id] 24 | add_index :apn_devices_apn_groups, :device_id 25 | add_index :apn_devices_apn_groups, :group_id 26 | end 27 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2009 markbates 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/apn_on_rails/app/models/apn/pull_notification.rb: -------------------------------------------------------------------------------- 1 | class APN::PullNotification < APN::Base 2 | belongs_to :app, :class_name => 'APN::App' 3 | 4 | validates_presence_of :app_id 5 | 6 | def self.latest_since(app_id, since_date=nil) 7 | if since_date 8 | res = first(:order => "created_at DESC", 9 | :conditions => ["app_id = ? AND created_at > ? AND launch_notification = ?", app_id, since_date, false]) 10 | else 11 | res = first(:order => "created_at DESC", 12 | :conditions => ["app_id = ? AND launch_notification = ?", app_id, true]) 13 | res = first(:order => "created_at DESC", 14 | :conditions => ["app_id = ? AND launch_notification = ?", app_id, false]) unless res 15 | end 16 | res 17 | end 18 | 19 | def self.all_since(app_id, since_date=nil) 20 | if since_date 21 | res = all(:order => "created_at DESC", 22 | :conditions => ["app_id = ? AND created_at > ? AND launch_notification = ?", app_id, since_date, false]) 23 | else 24 | res = all(:order => "created_at DESC", 25 | :conditions => ["app_id = ? AND launch_notification = ?", app_id, false]) 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /generators/apn_migrations_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails_generator' 2 | require 'rails/generators/migration' 3 | # Generates the migrations necessary for APN on Rails. 4 | # This should be run upon install and upgrade of the 5 | # APN on Rails gem. 6 | # 7 | # $ ruby script/generate apn_migrations 8 | class ApnMigrationsGenerator < Rails::Generator::Base 9 | desc "Generates the migrations necessary for APN on Rails." 10 | 11 | def manifest # :nodoc: 12 | record do |m| 13 | timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S") 14 | db_migrate_path = File.join('db', 'migrate') 15 | 16 | m.directory(db_migrate_path) 17 | 18 | Dir.glob(File.join(File.dirname(__FILE__), 'templates', 'apn_migrations', '*.rb')).sort.each_with_index do |f, i| 19 | f = File.basename(f) 20 | f.match(/\d+\_(.+)/) 21 | timestamp = timestamp.succ 22 | if Dir.glob(File.join(db_migrate_path, "*_#{$1}")).empty? 23 | m.file(File.join('apn_migrations', f), 24 | File.join(db_migrate_path, "#{timestamp}_#{$1}"), 25 | {:collision => :skip}) 26 | end 27 | end 28 | 29 | end # record 30 | 31 | end # manifest 32 | 33 | end # ApnMigrationsGenerator 34 | -------------------------------------------------------------------------------- /spec/rails_root/config/apple_push_notification_development.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDDDCCAfSgAwIBAgIBATANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzEN 3 | MAsGA1UECgwEaG9tZTERMA8GA1UECwwIbWFjYmF0ZXMxCzAJBgNVBAMMAkNBMB4X 4 | DTA5MDIyMjE5MDUyOVoXDTEwMDIyMjE5MDUyOVowUzELMAkGA1UEBhMCVVMxDTAL 5 | BgNVBAoMBGhvbWUxETAPBgNVBAsMCG1hY2JhdGVzMQswCQYDVQQLDAJDQTEVMBMG 6 | A1UEAwwMaGVsbG8tc2VydmVyMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCr 7 | LhTbcQc6hpYVeB8O94JzWnS41wZTaHReYe2mAxkIH9gF11Gm/Tejdfy7TboVsVtD 8 | FZ+vrVYPFnnVZG2UNDUkfBvkbCBrFQ8glnAHGRYtDxdFjrLDxm0BOfC58wEtV2cM 9 | hZhiLqjHFuSjHuAlAUshfCfWmKbEeDVtFSDxUMa6iQIDAQABo4GFMIGCMAwGA1Ud 10 | EwEB/wQCMAAwMQYJYIZIAYb4QgENBCQWIlJ1YnkvT3BlblNTTCBHZW5lcmF0ZWQg 11 | Q2VydGlmaWNhdGUwHQYDVR0OBBYEFEIBIEcdhFKPB+QILbsupdz3uD6YMAsGA1Ud 12 | DwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQUFAAOCAQEA 13 | dOKP/y/hsdnn2cbYpu2I6r8Lzql52XKa+jOaOT0TyPhmUAz0bFUgA53a202MDhbS 14 | KDVhIkC88KTjyyRwVNnwsrS5JD/IOXIJw/vy9VX14aCymPkup0TQR6ZIicKrjcMS 15 | yhmU5I0+fmsUN4PnayOuT/tJ0giy/x+1L/pgMicS47TvyNLB0vl34FplgmH6zlXv 16 | nS/5phroEJm71DPyDNNzoohZo54YHpGmvEDqjLc6DB+Ihu6/sghmd5dlSPNqsubO 17 | sBQeOyNuscbXo6MXI8uDYrZ/PqAtdzPXBjB7LXvVs69YT4KT7BaO3rqobgfJ0kNU 18 | e7roqj04VUJGmU47qrMLBg== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /lib/generators/apn_on_rails/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators' 2 | require 'rails/generators/migration' 3 | 4 | # Generates the migrations necessary for APN on Rails. 5 | # This should be run upon install and upgrade of the 6 | # APN on Rails gem. 7 | # 8 | # $ rails generate apn:apn_migrations 9 | module ApnOnRails 10 | module Generators 11 | 12 | class InstallGenerator < Rails::Generators::Base 13 | include Rails::Generators::Migration 14 | source_root(File.expand_path(File.join(File.dirname(__FILE__), 'templates'))) 15 | desc "add the migrations" 16 | 17 | def self.next_migration_number(path) 18 | unless @prev_migration_nr 19 | @prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i 20 | else 21 | @prev_migration_nr += 1 22 | end 23 | @prev_migration_nr.to_s 24 | end 25 | 26 | def create_migrations 27 | Dir.glob(File.join(self.class.source_root, '*.rb')).sort.each_with_index do |f, i| 28 | source = File.basename(f) 29 | source.match(/\d+\_(.+)/) 30 | destination = "db/migrate/#{$1}" 31 | migration_template source, destination 32 | end 33 | end 34 | 35 | end 36 | 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /apn_on_rails.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "apn_on_rails/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = %q{apn_on_rails} 7 | s.version = ApnOnRails::VERSION 8 | s.authors = ["markbates", "Rebecca Nesson"] 9 | s.email = %q{tech-team@prx.org} 10 | s.homepage = %q{http://github.com/PRX/apn_on_rails} 11 | s.summary = %q{Apple Push Notifications on Rails} 12 | s.description = %q{APN on Rails is a Ruby on Rails gem that allows you to 13 | easily add Apple Push Notification (iPhone) support to your Rails application. 14 | } 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = ["lib"] 20 | 21 | s.add_runtime_dependency('configatron') 22 | s.add_runtime_dependency('activerecord') 23 | s.add_runtime_dependency('actionpack') 24 | 25 | s.add_development_dependency('rake') 26 | s.add_development_dependency('autotest') 27 | s.add_development_dependency('sqlite3') 28 | s.add_development_dependency('rspec') 29 | s.add_development_dependency('bundler') 30 | s.add_development_dependency('rcov') 31 | end 32 | 33 | -------------------------------------------------------------------------------- /spec/apn_on_rails/libs/connection_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../spec_helper' 2 | 3 | describe APN::Connection do 4 | 5 | describe 'open_for_delivery' do 6 | 7 | it 'should create a connection to Apple, yield it, and then close' do 8 | rsa_mock = mock('rsa_mock') 9 | OpenSSL::PKey::RSA.should_receive(:new).and_return(rsa_mock) 10 | 11 | cert_mock = mock('cert_mock') 12 | OpenSSL::X509::Certificate.should_receive(:new).and_return(cert_mock) 13 | 14 | ctx_mock = mock('ctx_mock') 15 | ctx_mock.should_receive(:key=).with(rsa_mock) 16 | ctx_mock.should_receive(:cert=).with(cert_mock) 17 | OpenSSL::SSL::SSLContext.should_receive(:new).and_return(ctx_mock) 18 | 19 | tcp_mock = mock('tcp_mock') 20 | tcp_mock.should_receive(:close) 21 | TCPSocket.should_receive(:new).with('gateway.sandbox.push.apple.com', 2195).and_return(tcp_mock) 22 | 23 | ssl_mock = mock('ssl_mock') 24 | ssl_mock.should_receive(:sync=).with(true) 25 | ssl_mock.should_receive(:connect) 26 | ssl_mock.should_receive(:write).with('message-0') 27 | ssl_mock.should_receive(:write).with('message-1') 28 | ssl_mock.should_receive(:close) 29 | OpenSSL::SSL::SSLSocket.should_receive(:new).with(tcp_mock, ctx_mock).and_return(ssl_mock) 30 | 31 | APN::Connection.open_for_delivery do |conn, sock| 32 | conn.write('message-0') 33 | conn.write('message-1') 34 | end 35 | 36 | end 37 | 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /spec/apn_on_rails/libs/feedback_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../spec_helper' 2 | 3 | describe APN::Feedback do 4 | 5 | describe 'devices' do 6 | 7 | before(:each) do 8 | @time = Time.now 9 | @device = DeviceFactory.create 10 | @cert = mock('cert_mock') 11 | 12 | @data_mock = mock('data_mock') 13 | @data_mock.should_receive(:unpack).with('N1n1H140').and_return([@time.to_i, 12388, @device.token.delete(' ')]) 14 | 15 | @ssl_mock = mock('ssl_mock') 16 | @ssl_mock.should_receive(:read).with(38).twice.and_return(@data_mock, nil) 17 | @sock_mock = mock('sock_mock') 18 | end 19 | 20 | it 'should an Array of devices that need to be processed' do 21 | APN::Connection.should_receive(:open_for_feedback).and_yield(@ssl_mock, @sock_mock) 22 | 23 | devices = APN::Feedback.devices(@cert) 24 | devices.size.should == 1 25 | r_device = devices.first 26 | r_device.token.should == @device.token 27 | r_device.feedback_at.to_s.should == @time.to_s 28 | end 29 | 30 | it 'should yield up each device' do 31 | APN::Connection.should_receive(:open_for_feedback).and_yield(@ssl_mock, @sock_mock) 32 | lambda { 33 | APN::Feedback.devices(@cert) do |r_device| 34 | r_device.token.should == @device.token 35 | r_device.feedback_at.to_s.should == @time.to_s 36 | raise BlockRan.new 37 | end 38 | }.should raise_error(BlockRan) 39 | end 40 | 41 | end 42 | 43 | end -------------------------------------------------------------------------------- /lib/apn_on_rails/libs/feedback.rb: -------------------------------------------------------------------------------- 1 | module APN 2 | # Module for talking to the Apple Feedback Service. 3 | # The service is meant to let you know when a device is no longer 4 | # registered to receive notifications for your application. 5 | module Feedback 6 | 7 | class << self 8 | 9 | # Returns an Array of APN::Device objects that 10 | # has received feedback from Apple. Each APN::Device will 11 | # have it's feedback_at accessor marked with the time 12 | # that Apple believes the device de-registered itself. 13 | def devices(cert, &block) 14 | devices = [] 15 | return if cert.nil? 16 | APN::Connection.open_for_feedback({:cert => cert}) do |conn, sock| 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 20 | device = APN::Device.find(:first, :conditions => {:token => token}) 21 | if device 22 | device.feedback_at = Time.at(feedback[0]) 23 | devices << device 24 | end 25 | end 26 | end 27 | devices.each(&block) if block_given? 28 | return devices 29 | end # devices 30 | 31 | def process_devices 32 | ActiveSupport::Deprecation.warn("The method APN::Feedback.process_devices is deprecated. Use APN::App.process_devices instead.") 33 | APN::App.process_devices 34 | end 35 | 36 | end # class << self 37 | 38 | end # Feedback 39 | end # APN -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | apn_on_rails (0.5.1) 5 | actionpack 6 | activerecord 7 | configatron 8 | 9 | GEM 10 | remote: http://rubygems.org/ 11 | specs: 12 | ZenTest (4.4.2) 13 | actionpack (3.2.1) 14 | activemodel (= 3.2.1) 15 | activesupport (= 3.2.1) 16 | builder (~> 3.0.0) 17 | erubis (~> 2.7.0) 18 | journey (~> 1.0.1) 19 | rack (~> 1.4.0) 20 | rack-cache (~> 1.1) 21 | rack-test (~> 0.6.1) 22 | sprockets (~> 2.1.2) 23 | activemodel (3.2.1) 24 | activesupport (= 3.2.1) 25 | builder (~> 3.0.0) 26 | activerecord (3.2.1) 27 | activemodel (= 3.2.1) 28 | activesupport (= 3.2.1) 29 | arel (~> 3.0.0) 30 | tzinfo (~> 0.3.29) 31 | activesupport (3.2.1) 32 | i18n (~> 0.6) 33 | multi_json (~> 1.0) 34 | arel (3.0.0) 35 | autotest (4.4.6) 36 | ZenTest (>= 4.4.1) 37 | builder (3.0.0) 38 | configatron (2.9.0) 39 | yamler (>= 0.1.0) 40 | diff-lcs (1.1.2) 41 | erubis (2.7.0) 42 | hike (1.2.1) 43 | i18n (0.6.0) 44 | journey (1.0.1) 45 | multi_json (1.0.4) 46 | rack (1.4.1) 47 | rack-cache (1.1) 48 | rack (>= 0.4) 49 | rack-test (0.6.1) 50 | rack (>= 1.0) 51 | rake (0.8.7) 52 | rcov (0.9.9) 53 | rspec (2.4.0) 54 | rspec-core (~> 2.4.0) 55 | rspec-expectations (~> 2.4.0) 56 | rspec-mocks (~> 2.4.0) 57 | rspec-core (2.4.0) 58 | rspec-expectations (2.4.0) 59 | diff-lcs (~> 1.1.2) 60 | rspec-mocks (2.4.0) 61 | sprockets (2.1.2) 62 | hike (~> 1.2) 63 | rack (~> 1.0) 64 | tilt (!= 1.3.0, ~> 1.1) 65 | sqlite3 (1.3.5) 66 | tilt (1.3.3) 67 | tzinfo (0.3.31) 68 | yamler (0.1.0) 69 | 70 | PLATFORMS 71 | ruby 72 | 73 | DEPENDENCIES 74 | apn_on_rails! 75 | autotest 76 | bundler 77 | rake 78 | rcov 79 | rspec 80 | sqlite3 81 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rspec' 3 | require 'action_view' 4 | 5 | Dir.glob(File.join(File.dirname(__FILE__), 'extensions', '*.rb')).sort.each do |f| 6 | require f 7 | end 8 | 9 | require File.join(File.dirname(__FILE__), 'active_record', 'setup_ar.rb') 10 | 11 | require File.join(File.dirname(__FILE__), '..', 'lib', 'apn_on_rails') 12 | 13 | # Dir.glob(File.join(File.dirname(__FILE__), 'factories', '*.rb')).sort.each do |f| 14 | # require f 15 | # end 16 | 17 | require File.join(File.dirname(__FILE__), 'factories', 'app_factory.rb') 18 | require File.join(File.dirname(__FILE__), 'factories', 'device_factory.rb') 19 | require File.join(File.dirname(__FILE__), 'factories', 'group_factory.rb') 20 | require File.join(File.dirname(__FILE__), 'factories', 'device_grouping_factory.rb') 21 | require File.join(File.dirname(__FILE__), 'factories', 'group_notification_factory.rb') 22 | require File.join(File.dirname(__FILE__), 'factories', 'notification_factory.rb') 23 | require File.join(File.dirname(__FILE__), 'factories', 'pull_notification_factory.rb') 24 | 25 | configatron.apn.cert = File.expand_path(File.join(File.dirname(__FILE__), 'rails_root', 'config', 'apple_push_notification_development.pem')) 26 | 27 | RSpec.configure do |config| 28 | 29 | config.before(:all) do 30 | 31 | end 32 | 33 | config.after(:all) do 34 | 35 | end 36 | 37 | config.before(:each) do 38 | 39 | end 40 | 41 | config.after(:each) do 42 | 43 | end 44 | 45 | end 46 | 47 | def fixture_path(*name) 48 | return File.join(File.dirname(__FILE__), 'fixtures', *name) 49 | end 50 | 51 | def fixture_value(*name) 52 | return File.read(fixture_path(*name)) 53 | end 54 | 55 | def write_fixture(name, value) 56 | File.open(fixture_path(*name), 'w') {|f| f.write(value)} 57 | end 58 | 59 | def apn_cert 60 | File.read(File.join(File.dirname(__FILE__), 'rails_root', 'config', 'apple_push_notification_development.pem')) 61 | end 62 | 63 | class BlockRan < StandardError 64 | end -------------------------------------------------------------------------------- /lib/apn_on_rails/app/models/apn/device.rb: -------------------------------------------------------------------------------- 1 | # Represents an iPhone (or other APN enabled device). 2 | # An APN::Device can have many APN::Notification. 3 | # 4 | # In order for the APN::Feedback system to work properly you *MUST* 5 | # touch the last_registered_at column everytime someone opens 6 | # your application. If you do not, then it is possible, and probably likely, 7 | # that their device will be removed and will no longer receive notifications. 8 | # 9 | # Example: 10 | # Device.create(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz') 11 | class APN::Device < APN::Base 12 | 13 | belongs_to :app, :class_name => 'APN::App' 14 | has_many :notifications, :class_name => 'APN::Notification' 15 | has_many :unsent_notifications, :class_name => 'APN::Notification', :conditions => 'sent_at is null' 16 | 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}$/ 19 | 20 | before_create :set_last_registered_at 21 | 22 | # The feedback_at accessor is set when the 23 | # device is marked as potentially disconnected from your 24 | # application by Apple. 25 | attr_accessor :feedback_at 26 | 27 | # Stores the token (Apple's device ID) of the iPhone (device). 28 | # 29 | # If the token comes in like this: 30 | # '<5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz>' 31 | # Then the '<' and '>' will be stripped off. 32 | def token=(token) 33 | res = token.scan(/\<(.+)\>/).first 34 | unless res.nil? || res.empty? 35 | token = res.first 36 | end 37 | write_attribute('token', token) 38 | end 39 | 40 | # Returns the hexadecimal representation of the device's token. 41 | def to_hexa 42 | [self.token.delete(' ')].pack('H*') 43 | end 44 | 45 | def set_last_registered_at 46 | self.last_registered_at = Time.now #if self.last_registered_at.nil? 47 | end 48 | 49 | end -------------------------------------------------------------------------------- /.specification: -------------------------------------------------------------------------------- 1 | --- !ruby/object:Gem::Specification 2 | name: apn_on_rails 3 | version: !ruby/object:Gem::Version 4 | version: 0.3.1 5 | platform: ruby 6 | authors: 7 | - markbates 8 | autorequire: 9 | bindir: bin 10 | cert_chain: [] 11 | 12 | date: 2010-01-26 00:00:00 -05:00 13 | default_executable: 14 | dependencies: 15 | - !ruby/object:Gem::Dependency 16 | name: configatron 17 | type: :runtime 18 | version_requirement: 19 | version_requirements: !ruby/object:Gem::Requirement 20 | requirements: 21 | - - ">=" 22 | - !ruby/object:Gem::Version 23 | version: "0" 24 | version: 25 | description: "apn_on_rails was developed by: markbates" 26 | email: mark@markbates.com 27 | executables: [] 28 | 29 | extensions: [] 30 | 31 | extra_rdoc_files: 32 | - README 33 | - LICENSE 34 | files: 35 | - lib/apn_on_rails/apn_on_rails.rb 36 | - lib/apn_on_rails/app/models/apn/base.rb 37 | - lib/apn_on_rails/app/models/apn/device.rb 38 | - lib/apn_on_rails/app/models/apn/notification.rb 39 | - lib/apn_on_rails/libs/connection.rb 40 | - lib/apn_on_rails/libs/feedback.rb 41 | - lib/apn_on_rails/tasks/apn.rake 42 | - lib/apn_on_rails/tasks/db.rake 43 | - lib/apn_on_rails.rb 44 | - lib/apn_on_rails_tasks.rb 45 | - README 46 | - LICENSE 47 | - generators/apn_migrations_generator.rb 48 | - generators/templates/apn_migrations/001_create_apn_devices.rb 49 | - generators/templates/apn_migrations/002_create_apn_notifications.rb 50 | - generators/templates/apn_migrations/003_alter_apn_devices.rb 51 | has_rdoc: true 52 | homepage: http://www.metabates.com 53 | licenses: [] 54 | 55 | post_install_message: 56 | rdoc_options: [] 57 | 58 | require_paths: 59 | - lib 60 | required_ruby_version: !ruby/object:Gem::Requirement 61 | requirements: 62 | - - ">=" 63 | - !ruby/object:Gem::Version 64 | version: "0" 65 | version: 66 | required_rubygems_version: !ruby/object:Gem::Requirement 67 | requirements: 68 | - - ">=" 69 | - !ruby/object:Gem::Version 70 | version: "0" 71 | version: 72 | requirements: [] 73 | 74 | rubyforge_project: magrathea 75 | rubygems_version: 1.3.5 76 | signing_key: 77 | specification_version: 3 78 | summary: apn_on_rails 79 | test_files: [] 80 | 81 | -------------------------------------------------------------------------------- /spec/apn_on_rails/app/models/apn/device_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper.rb') 2 | 3 | describe APN::Device do 4 | 5 | describe 'token' do 6 | 7 | it 'should be unique' do 8 | t = APN::Device.first.token 9 | device = DeviceFactory.new(:token => t) 10 | device.should_not be_valid 11 | device.errors['token'].should include('has already been taken') 12 | 13 | device = DeviceFactory.new(:token => device.token.succ) 14 | device.should be_valid 15 | end 16 | 17 | it 'should get cleansed if it contains brackets' do 18 | token = DeviceFactory.random_token 19 | device = DeviceFactory.new(:token => "<#{token}>") 20 | device.token.should == token 21 | device.token.should_not == "<#{token}>" 22 | end 23 | 24 | it 'should be in the correct pattern' do 25 | device = DeviceFactory.new(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz') 26 | device.should be_valid 27 | device.token = '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6' 28 | device.should_not be_valid 29 | device.token = '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7!!' 30 | device.should_not be_valid 31 | end 32 | 33 | end 34 | 35 | describe 'to_hexa' do 36 | 37 | it 'should convert the text string to hexadecimal' do 38 | device = DeviceFactory.new(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz') 39 | device.to_hexa.should == fixture_value('hexa.bin') 40 | end 41 | 42 | end 43 | 44 | describe 'before_create' do 45 | 46 | it 'should set the last_registered_at date to Time.now' do 47 | time = Time.now 48 | Time.stub(:now).and_return(time) 49 | device = DeviceFactory.create 50 | device.last_registered_at.should_not be_nil 51 | device.last_registered_at.to_s.should == time.to_s 52 | 53 | # ago = 1.week.ago 54 | # device = DeviceFactory.create(:last_registered_at => ago) 55 | # device.last_registered_at.should_not be_nil 56 | # device.last_registered_at.to_s.should == ago.to_s 57 | end 58 | 59 | end 60 | 61 | end -------------------------------------------------------------------------------- /spec/apn_on_rails/app/models/apn/group_notification_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper.rb') 2 | 3 | describe APN::GroupNotification do 4 | 5 | describe 'alert' do 6 | 7 | it 'should trim the message to 150 characters' do 8 | noty = APN::GroupNotification.new 9 | noty.alert = 'a' * 200 10 | noty.alert.should == ('a' * 147) + '...' 11 | end 12 | 13 | end 14 | 15 | describe 'apple_hash' do 16 | 17 | it 'should return a hash of the appropriate params for Apple' do 18 | noty = APN::GroupNotification.first 19 | noty.apple_hash.should == {"aps" => {"badge" => 5, "sound" => "my_sound.aiff", "alert" => "Hello!"},"typ" => "1"} 20 | noty.custom_properties = nil 21 | noty.apple_hash.should == {"aps" => {"badge" => 5, "sound" => "my_sound.aiff", "alert" => "Hello!"}} 22 | noty.badge = nil 23 | noty.apple_hash.should == {"aps" => {"sound" => "my_sound.aiff", "alert" => "Hello!"}} 24 | noty.alert = nil 25 | noty.apple_hash.should == {"aps" => {"sound" => "my_sound.aiff"}} 26 | noty.sound = nil 27 | noty.apple_hash.should == {"aps" => {}} 28 | noty.sound = true 29 | noty.apple_hash.should == {"aps" => {"sound" => "1.aiff"}} 30 | end 31 | 32 | end 33 | 34 | describe 'to_apple_json' do 35 | 36 | it 'should return the necessary JSON for Apple' do 37 | noty = APN::GroupNotification.first 38 | noty.to_apple_json.should == %{{"typ":"1","aps":{"badge":5,"sound":"my_sound.aiff","alert":"Hello!"}}} 39 | end 40 | 41 | end 42 | 43 | describe 'message_for_sending' do 44 | 45 | it 'should create a binary message to be sent to Apple' do 46 | noty = APN::GroupNotification.first 47 | noty.custom_properties = nil 48 | device = DeviceFactory.new(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz') 49 | noty.message_for_sending(device).should == fixture_value('message_for_sending.bin') 50 | end 51 | 52 | it 'should raise an APN::Errors::ExceededMessageSizeError if the message is too big' do 53 | app = AppFactory.create 54 | device = DeviceFactory.create({:app_id => app.id}) 55 | group = GroupFactory.create({:app_id => app.id}) 56 | device_grouping = DeviceGroupingFactory.create({:group_id => group.id,:device_id => device.id}) 57 | noty = GroupNotificationFactory.new(:group_id => group.id, :sound => true, :badge => nil) 58 | noty.send(:write_attribute, 'alert', 'a' * 183) 59 | lambda { 60 | noty.message_for_sending(device) 61 | }.should raise_error(APN::Errors::ExceededMessageSizeError) 62 | end 63 | 64 | end 65 | 66 | end -------------------------------------------------------------------------------- /lib/apn_on_rails/apn_on_rails.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'openssl' 3 | require 'configatron' 4 | 5 | rails_root = File.join(FileUtils.pwd, 'rails_root') 6 | if defined?(Rails.root) 7 | rails_root = Rails.root.to_s 8 | end 9 | 10 | rails_env = 'development' 11 | if defined?(Rails.env) 12 | rails_env = Rails.env 13 | end 14 | 15 | configatron.apn.set_default(:passphrase, '') 16 | configatron.apn.set_default(:port, 2195) 17 | 18 | configatron.apn.feedback.set_default(:passphrase, configatron.apn.passphrase) 19 | configatron.apn.feedback.set_default(:port, 2196) 20 | 21 | if rails_env == 'production' 22 | configatron.apn.set_default(:host, 'gateway.push.apple.com') 23 | configatron.apn.set_default(:cert, File.join(rails_root, 'config', 'apple_push_notification_production.pem')) 24 | 25 | configatron.apn.feedback.set_default(:host, 'feedback.push.apple.com') 26 | configatron.apn.feedback.set_default(:cert, configatron.apn.cert) 27 | else 28 | configatron.apn.set_default(:host, 'gateway.sandbox.push.apple.com') 29 | configatron.apn.set_default(:cert, File.join(rails_root, 'config', 'apple_push_notification_development.pem')) 30 | 31 | configatron.apn.feedback.set_default(:host, 'feedback.sandbox.push.apple.com') 32 | configatron.apn.feedback.set_default(:cert, configatron.apn.cert) 33 | end 34 | 35 | module APN # :nodoc: 36 | 37 | module Errors # :nodoc: 38 | 39 | # Raised when a notification message to Apple is longer than 256 bytes. 40 | class ExceededMessageSizeError < StandardError 41 | 42 | def initialize(message) # :nodoc: 43 | super("The maximum size allowed for a notification payload is 256 bytes: '#{message}'") 44 | end 45 | 46 | end 47 | 48 | class MissingCertificateError < StandardError 49 | def initialize 50 | super("This app has no certificate") 51 | end 52 | end 53 | 54 | end # Errors 55 | 56 | end # APN 57 | 58 | base = File.join(File.dirname(__FILE__), 'app', 'models', 'apn', 'base.rb') 59 | require base 60 | 61 | Dir.glob(File.join(File.dirname(__FILE__), 'app', 'models', 'apn', '*.rb')).sort.each do |f| 62 | require f 63 | end 64 | 65 | %w{ models controllers helpers }.each do |dir| 66 | path = File.join(File.dirname(__FILE__), 'app', dir) 67 | $LOAD_PATH << path 68 | # puts "Adding #{path}" 69 | begin 70 | if ActiveSupport::Dependencies.respond_to? :autoload_paths 71 | ActiveSupport::Dependencies.autoload_paths << path 72 | ActiveSupport::Dependencies.autoload_once_paths.delete(path) 73 | else 74 | ActiveSupport::Dependencies.load_paths << path 75 | ActiveSupport::Dependencies.load_once_paths.delete(path) 76 | end 77 | rescue NameError 78 | Dependencies.load_paths << path 79 | Dependencies.load_once_paths.delete(path) 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/apn_on_rails/app/models/apn/group_notification.rb: -------------------------------------------------------------------------------- 1 | class APN::GroupNotification < APN::Base 2 | include ::ActionView::Helpers::TextHelper 3 | extend ::ActionView::Helpers::TextHelper 4 | serialize :custom_properties 5 | 6 | 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 | 10 | validates_presence_of :group_id 11 | 12 | def devices 13 | self.group.devices 14 | end 15 | 16 | # Stores the text alert message you want to send to the device. 17 | # 18 | # If the message is over 150 characters long it will get truncated 19 | # to 150 characters with a ... 20 | def alert=(message) 21 | if !message.blank? && message.size > 150 22 | message = truncate(message, :length => 150) 23 | end 24 | write_attribute('alert', message) 25 | end 26 | 27 | # Creates a Hash that will be the payload of an APN. 28 | # 29 | # Example: 30 | # apn = APN::GroupNotification.new 31 | # apn.badge = 5 32 | # apn.sound = 'my_sound.aiff' 33 | # apn.alert = 'Hello!' 34 | # apn.apple_hash # => {"aps" => {"badge" => 5, "sound" => "my_sound.aiff", "alert" => "Hello!"}} 35 | # 36 | # Example 2: 37 | # apn = APN::GroupNotification.new 38 | # apn.badge = 0 39 | # apn.sound = true 40 | # apn.custom_properties = {"typ" => 1} 41 | # apn.apple_hash # => {"aps" => {"badge" => 0, "sound" => 1.aiff},"typ" => "1"} 42 | def apple_hash 43 | result = {} 44 | result['aps'] = {} 45 | result['aps']['alert'] = self.alert if self.alert 46 | result['aps']['badge'] = self.badge.to_i if self.badge 47 | if self.sound 48 | result['aps']['sound'] = self.sound if self.sound.is_a? String 49 | result['aps']['sound'] = "1.aiff" if self.sound.is_a?(TrueClass) 50 | end 51 | if self.custom_properties 52 | self.custom_properties.each do |key,value| 53 | result["#{key}"] = "#{value}" 54 | end 55 | end 56 | result 57 | end 58 | 59 | # Creates the JSON string required for an APN message. 60 | # 61 | # Example: 62 | # apn = APN::Notification.new 63 | # apn.badge = 5 64 | # apn.sound = 'my_sound.aiff' 65 | # apn.alert = 'Hello!' 66 | # apn.to_apple_json # => '{"aps":{"badge":5,"sound":"my_sound.aiff","alert":"Hello!"}}' 67 | def to_apple_json 68 | self.apple_hash.to_json 69 | end 70 | 71 | # Creates the binary message needed to send to Apple. 72 | def message_for_sending(device) 73 | json = self.to_apple_json 74 | message = "\0\0 #{device.to_hexa}\0#{json.length.chr}#{json}" 75 | raise APN::Errors::ExceededMessageSizeError.new(message) if message.size.to_i > 256 76 | message 77 | end 78 | 79 | end # APN::Notification -------------------------------------------------------------------------------- /spec/apn_on_rails/app/models/apn/notification_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper.rb') 2 | 3 | describe APN::Notification do 4 | 5 | describe 'alert' do 6 | 7 | it 'should trim the message to 150 characters' do 8 | noty = APN::Notification.new 9 | noty.alert = 'a' * 200 10 | noty.alert.should == ('a' * 147) + '...' 11 | end 12 | 13 | end 14 | 15 | describe 'apple_hash' do 16 | 17 | it 'should return a hash of the appropriate params for Apple' do 18 | noty = APN::Notification.first 19 | noty.apple_hash.should == {"aps" => {"badge" => 5, "sound" => "my_sound.aiff", "alert" => "Hello!"},"typ" => "1"} 20 | noty.custom_properties = nil 21 | noty.apple_hash.should == {"aps" => {"badge" => 5, "sound" => "my_sound.aiff", "alert" => "Hello!"}} 22 | noty.badge = nil 23 | noty.apple_hash.should == {"aps" => {"sound" => "my_sound.aiff", "alert" => "Hello!"}} 24 | noty.alert = nil 25 | noty.apple_hash.should == {"aps" => {"sound" => "my_sound.aiff"}} 26 | noty.sound = nil 27 | noty.apple_hash.should == {"aps" => {}} 28 | noty.sound = true 29 | noty.apple_hash.should == {"aps" => {"sound" => "1.aiff"}} 30 | end 31 | 32 | end 33 | 34 | describe 'to_apple_json' do 35 | 36 | it 'should return the necessary JSON for Apple' do 37 | noty = APN::Notification.first 38 | noty.to_apple_json.should == %{{"typ":"1","aps":{"badge":5,"sound":"my_sound.aiff","alert":"Hello!"}}} 39 | end 40 | 41 | end 42 | 43 | describe 'message_for_sending' do 44 | 45 | it 'should create a binary message to be sent to Apple' do 46 | noty = APN::Notification.first 47 | noty.custom_properties = nil 48 | noty.device = DeviceFactory.new(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz') 49 | noty.message_for_sending.should == fixture_value('message_for_sending.bin') 50 | end 51 | 52 | it 'should raise an APN::Errors::ExceededMessageSizeError if the message is too big' do 53 | noty = NotificationFactory.new(:device_id => DeviceFactory.create, :sound => true, :badge => nil) 54 | noty.send(:write_attribute, 'alert', 'a' * 183) 55 | lambda { 56 | noty.message_for_sending 57 | }.should raise_error(APN::Errors::ExceededMessageSizeError) 58 | end 59 | 60 | end 61 | 62 | describe 'send_notifications' do 63 | 64 | it 'should warn the user the method is deprecated and call the corresponding method on APN::App' do 65 | ActiveSupport::Deprecation.should_receive(:warn) 66 | APN::App.should_receive(:send_notifications) 67 | APN::Notification.send_notifications 68 | end 69 | end 70 | 71 | end -------------------------------------------------------------------------------- /lib/apn_on_rails/libs/connection.rb: -------------------------------------------------------------------------------- 1 | module APN 2 | module Connection 3 | 4 | class << self 5 | 6 | # Yields up an SSL socket to write notifications to. 7 | # The connections are close automatically. 8 | # 9 | # Example: 10 | # APN::Configuration.open_for_delivery do |conn| 11 | # conn.write('my cool notification') 12 | # end 13 | # 14 | # Configuration parameters are: 15 | # 16 | # configatron.apn.passphrase = '' 17 | # configatron.apn.port = 2195 18 | # configatron.apn.host = 'gateway.sandbox.push.apple.com' # Development 19 | # configatron.apn.host = 'gateway.push.apple.com' # Production 20 | # configatron.apn.cert = File.join(rails_root, 'config', 'apple_push_notification_development.pem')) # Development 21 | # configatron.apn.cert = File.join(rails_root, 'config', 'apple_push_notification_production.pem')) # Production 22 | def open_for_delivery(options = {}, &block) 23 | open(options, &block) 24 | end 25 | 26 | # Yields up an SSL socket to receive feedback from. 27 | # The connections are close automatically. 28 | # Configuration parameters are: 29 | # 30 | # configatron.apn.feedback.passphrase = '' 31 | # configatron.apn.feedback.port = 2196 32 | # configatron.apn.feedback.host = 'feedback.sandbox.push.apple.com' # Development 33 | # configatron.apn.feedback.host = 'feedback.push.apple.com' # Production 34 | # configatron.apn.feedback.cert = File.join(rails_root, 'config', 'apple_push_notification_development.pem')) # Development 35 | # configatron.apn.feedback.cert = File.join(rails_root, 'config', 'apple_push_notification_production.pem')) # Production 36 | def open_for_feedback(options = {}, &block) 37 | options = {:cert => configatron.apn.feedback.cert, 38 | :passphrase => configatron.apn.feedback.passphrase, 39 | :host => configatron.apn.feedback.host, 40 | :port => configatron.apn.feedback.port}.merge(options) 41 | open(options, &block) 42 | end 43 | 44 | private 45 | def open(options = {}, &block) # :nodoc: 46 | options = {:cert => configatron.apn.cert, 47 | :passphrase => configatron.apn.passphrase, 48 | :host => configatron.apn.host, 49 | :port => configatron.apn.port}.merge(options) 50 | #cert = File.read(options[:cert]) 51 | cert = options[:cert] 52 | ctx = OpenSSL::SSL::SSLContext.new 53 | ctx.key = OpenSSL::PKey::RSA.new(cert, options[:passphrase]) 54 | ctx.cert = OpenSSL::X509::Certificate.new(cert) 55 | 56 | sock = TCPSocket.new(options[:host], options[:port]) 57 | ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx) 58 | ssl.sync = true 59 | ssl.connect 60 | 61 | yield ssl, sock if block_given? 62 | 63 | ssl.close 64 | sock.close 65 | end 66 | 67 | end 68 | 69 | end # Connection 70 | end # APN -------------------------------------------------------------------------------- /lib/apn_on_rails/app/models/apn/notification.rb: -------------------------------------------------------------------------------- 1 | # Represents the message you wish to send. 2 | # An APN::Notification belongs to an APN::Device. 3 | # 4 | # Example: 5 | # apn = APN::Notification.new 6 | # apn.badge = 5 7 | # apn.sound = 'my_sound.aiff' 8 | # apn.alert = 'Hello!' 9 | # apn.device = APN::Device.find(1) 10 | # apn.save 11 | # 12 | # To deliver call the following method: 13 | # APN::Notification.send_notifications 14 | # 15 | # As each APN::Notification is sent the sent_at column will be timestamped, 16 | # so as to not be sent again. 17 | class APN::Notification < APN::Base 18 | include ::ActionView::Helpers::TextHelper 19 | extend ::ActionView::Helpers::TextHelper 20 | serialize :custom_properties 21 | 22 | belongs_to :device, :class_name => 'APN::Device' 23 | has_one :app, :class_name => 'APN::App', :through => :device 24 | 25 | # Stores the text alert message you want to send to the device. 26 | # 27 | # If the message is over 150 characters long it will get truncated 28 | # to 150 characters with a ... 29 | def alert=(message) 30 | if !message.blank? && message.size > 150 31 | message = truncate(message, :length => 150) 32 | end 33 | write_attribute('alert', message) 34 | end 35 | 36 | # Creates a Hash that will be the payload of an APN. 37 | # 38 | # Example: 39 | # apn = APN::Notification.new 40 | # apn.badge = 5 41 | # apn.sound = 'my_sound.aiff' 42 | # apn.alert = 'Hello!' 43 | # apn.apple_hash # => {"aps" => {"badge" => 5, "sound" => "my_sound.aiff", "alert" => "Hello!"}} 44 | # 45 | # Example 2: 46 | # apn = APN::Notification.new 47 | # apn.badge = 0 48 | # apn.sound = true 49 | # apn.custom_properties = {"typ" => 1} 50 | # apn.apple_hash # => {"aps" => {"badge" => 0, "sound" => "1.aiff"}, "typ" => "1"} 51 | def apple_hash 52 | result = {} 53 | result['aps'] = {} 54 | result['aps']['alert'] = self.alert if self.alert 55 | result['aps']['badge'] = self.badge.to_i if self.badge 56 | if self.sound 57 | result['aps']['sound'] = self.sound if self.sound.is_a? String 58 | result['aps']['sound'] = "1.aiff" if self.sound.is_a?(TrueClass) 59 | end 60 | if self.custom_properties 61 | self.custom_properties.each do |key,value| 62 | result["#{key}"] = "#{value}" 63 | end 64 | end 65 | result 66 | end 67 | 68 | # Creates the JSON string required for an APN message. 69 | # 70 | # Example: 71 | # apn = APN::Notification.new 72 | # apn.badge = 5 73 | # apn.sound = 'my_sound.aiff' 74 | # apn.alert = 'Hello!' 75 | # apn.to_apple_json # => '{"aps":{"badge":5,"sound":"my_sound.aiff","alert":"Hello!"}}' 76 | def to_apple_json 77 | self.apple_hash.to_json 78 | end 79 | 80 | # Creates the binary message needed to send to Apple. 81 | def message_for_sending 82 | json = self.to_apple_json 83 | message = "\0\0 #{self.device.to_hexa}\0#{json.length.chr}#{json}" 84 | raise APN::Errors::ExceededMessageSizeError.new(message) if message.size.to_i > 256 85 | message 86 | end 87 | 88 | def self.send_notifications 89 | ActiveSupport::Deprecation.warn("The method APN::Notification.send_notifications is deprecated. Use APN::App.send_notifications instead.") 90 | APN::App.send_notifications 91 | end 92 | 93 | end # APN::Notification -------------------------------------------------------------------------------- /spec/apn_on_rails/app/models/apn/pull_notification_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper.rb') 2 | 3 | describe APN::PullNotification do 4 | 5 | describe 'latest_since_when_already_seen_latest' do 6 | 7 | it 'should return nothing because since date is after the latest pull notification' do 8 | app = APN::App.first 9 | noty1 = PullNotificationFactory.create({:app_id => app.id}) 10 | noty1.created_at = Time.now - 1.week 11 | noty1.save 12 | APN::PullNotification.latest_since(app.id,Time.now).should == nil 13 | end 14 | 15 | end 16 | 17 | describe 'latest_since_when_have_not_seen_latest' do 18 | 19 | it 'should return the most recent pull notification because it has not yet been seen' do 20 | app = APN::App.first 21 | noty1 = PullNotificationFactory.create({:app_id => app.id}) 22 | noty1.created_at = Time.now + 1.week 23 | noty1.save 24 | latest = APN::PullNotification.latest_since(app.id,Time.now - 1.week) 25 | puts "latest is #{latest}" 26 | latest.should == noty1 27 | end 28 | 29 | end 30 | 31 | describe 'latest_since_with_no_date_when_there_is_no_launch_notification' do 32 | it 'should return the most recent pull notification because no date is given' do 33 | app = APN::App.first 34 | noty1 = APN::PullNotification.find(:first, :order => "created_at DESC") 35 | APN::PullNotification.latest_since(app.id).should == noty1 36 | end 37 | end 38 | 39 | describe 'latest_since_with_no_date_when_there_is_a_launch_notification' do 40 | it 'should return the launch notification even though there is a more recent notification' do 41 | app = APN::App.first 42 | noty_launch = PullNotificationFactory.create({:app_id => app.id, :launch_notification => true}) 43 | noty_launch.created_at = Time.now - 1.week 44 | noty_launch.save 45 | noty_nonlaunch = PullNotificationFactory.create({:app_id => app.id}) 46 | APN::PullNotification.latest_since(app.id).should == noty_launch 47 | end 48 | end 49 | 50 | describe 'older_non_launch_noty_with_newer_launch_noty' do 51 | it 'should return the older non launch notification even though a newer launch notification exists' do 52 | APN::PullNotification.all.each { |n| n.destroy } 53 | app = APN::App.first 54 | noty_launch = PullNotificationFactory.create({:app_id => app.id, :launch_notification => true}) 55 | puts "noty_launch id is #{noty_launch.id}" 56 | noty_nonlaunch = PullNotificationFactory.create({:app_id => app.id}) 57 | noty_nonlaunch.created_at = Time.now - 1.week 58 | noty_nonlaunch.save 59 | puts "noty_nonlaunch id is #{noty_nonlaunch.id}" 60 | APN::PullNotification.latest_since(app.id, Time.now - 2.weeks).should == noty_nonlaunch 61 | end 62 | end 63 | 64 | describe 'all_since_date_with_date_given' do 65 | it 'should return all the non-launch notifications after the given date but not the ones before it' do 66 | APN::PullNotification.all.each { |n| n.destroy } 67 | app = APN::App.first 68 | noty_launch = PullNotificationFactory.create({:app_id => app.id, :launch_notification => true}) 69 | noty_launch.created_at = Time.now - 2.weeks 70 | noty_launch.save 71 | old_noty = PullNotificationFactory.create({:app_id => app.id}) 72 | old_noty.created_at = Time.now - 2.weeks 73 | old_noty.save 74 | new_noty_one = PullNotificationFactory.create({:app_id => app.id}) 75 | new_noty_one.created_at = Time.now - 1.day 76 | new_noty_one.save 77 | new_noty_two = PullNotificationFactory.create({:app_id => app.id}) 78 | APN::PullNotification.all_since(app.id, Time.now - 1.week).should == [new_noty_two,new_noty_one] 79 | end 80 | end 81 | 82 | describe 'all_since_with_no_since_date_given' do 83 | it 'should return all of the non-launch notifications' do 84 | APN::PullNotification.all.each { |n| n.destroy } 85 | app = APN::App.first 86 | noty_launch = PullNotificationFactory.create({:app_id => app.id, :launch_notification => true}) 87 | noty_launch.created_at = Time.now - 2.weeks 88 | noty_launch.save 89 | old_noty = PullNotificationFactory.create({:app_id => app.id}) 90 | old_noty.created_at = Time.now - 2.weeks 91 | old_noty.save 92 | new_noty_one = PullNotificationFactory.create({:app_id => app.id}) 93 | new_noty_one.created_at = Time.now - 1.day 94 | new_noty_one.save 95 | new_noty_two = PullNotificationFactory.create({:app_id => app.id}) 96 | APN::PullNotification.all_since(app.id, Time.now - 3.weeks).should == [new_noty_two,new_noty_one,old_noty] 97 | end 98 | end 99 | 100 | end -------------------------------------------------------------------------------- /lib/apn_on_rails/app/models/apn/app.rb: -------------------------------------------------------------------------------- 1 | class APN::App < APN::Base 2 | 3 | has_many :groups, :class_name => 'APN::Group', :dependent => :destroy 4 | has_many :devices, :class_name => 'APN::Device', :dependent => :destroy 5 | has_many :notifications, :through => :devices, :dependent => :destroy 6 | has_many :unsent_notifications, :through => :devices 7 | has_many :group_notifications, :through => :groups 8 | has_many :unsent_group_notifications, :through => :groups 9 | 10 | def cert 11 | (RAILS_ENV == 'production' ? apn_prod_cert : apn_dev_cert) 12 | end 13 | 14 | # Opens a connection to the Apple APN server and attempts to batch deliver 15 | # an Array of group notifications. 16 | # 17 | # 18 | # As each APN::GroupNotification is sent the sent_at column will be timestamped, 19 | # so as to not be sent again. 20 | # 21 | def send_notifications 22 | if self.cert.nil? 23 | raise APN::Errors::MissingCertificateError.new 24 | return 25 | end 26 | APN::App.send_notifications_for_cert(self.cert, self.id) 27 | end 28 | 29 | def self.send_notifications 30 | apps = APN::App.all 31 | apps.each do |app| 32 | app.send_notifications 33 | end 34 | if !configatron.apn.cert.blank? 35 | global_cert = File.read(configatron.apn.cert) 36 | send_notifications_for_cert(global_cert, nil) 37 | end 38 | end 39 | 40 | def self.send_notifications_for_cert(the_cert, app_id) 41 | # 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 56 | end 57 | rescue Exception => e 58 | log_connection_exception(e) 59 | end 60 | # end 61 | end 62 | 63 | def send_group_notifications 64 | if self.cert.nil? 65 | raise APN::Errors::MissingCertificateError.new 66 | return 67 | 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)) 73 | end 74 | gnoty.sent_at = Time.now 75 | gnoty.save 76 | end 77 | end 78 | end 79 | end 80 | 81 | def send_group_notification(gnoty) 82 | if self.cert.nil? 83 | raise APN::Errors::MissingCertificateError.new 84 | return 85 | end 86 | unless gnoty.nil? 87 | APN::Connection.open_for_delivery({:cert => self.cert}) do |conn, sock| 88 | gnoty.devices.find_each do |device| 89 | conn.write(gnoty.message_for_sending(device)) 90 | end 91 | gnoty.sent_at = Time.now 92 | gnoty.save 93 | end 94 | end 95 | end 96 | 97 | def self.send_group_notifications 98 | apps = APN::App.all 99 | apps.each do |app| 100 | app.send_group_notifications 101 | end 102 | end 103 | 104 | # Retrieves a list of APN::Device instnces from Apple using 105 | # the devices method. It then checks to see if the 106 | # last_registered_at date of each APN::Device is 107 | # before the date that Apple says the device is no longer 108 | # accepting notifications then the device is deleted. Otherwise 109 | # it is assumed that the application has been re-installed 110 | # and is available for notifications. 111 | # 112 | # This can be run from the following Rake task: 113 | # $ rake apn:feedback:process 114 | def process_devices 115 | if self.cert.nil? 116 | raise APN::Errors::MissingCertificateError.new 117 | return 118 | end 119 | APN::App.process_devices_for_cert(self.cert) 120 | end # process_devices 121 | 122 | def self.process_devices 123 | apps = APN::App.all 124 | apps.each do |app| 125 | app.process_devices 126 | end 127 | if !configatron.apn.cert.blank? 128 | global_cert = File.read(configatron.apn.cert) 129 | APN::App.process_devices_for_cert(global_cert) 130 | end 131 | end 132 | 133 | def self.process_devices_for_cert(the_cert) 134 | puts "in APN::App.process_devices_for_cert" 135 | APN::Feedback.devices(the_cert).each do |device| 136 | if device.last_registered_at < device.feedback_at 137 | puts "device #{device.id} -> #{device.last_registered_at} < #{device.feedback_at}" 138 | device.destroy 139 | else 140 | puts "device #{device.id} -> #{device.last_registered_at} not < #{device.feedback_at}" 141 | end 142 | end 143 | end 144 | 145 | protected 146 | 147 | def self.log_connection_exception(ex) 148 | STDERR.puts ex.message 149 | raise ex 150 | end 151 | 152 | end 153 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | =APN on Rails (Apple Push Notifications on Rails) 2 | 3 | APN on Rails is a Ruby on Rails gem that allows you to easily add Apple Push Notification (iPhone) 4 | support to your Rails application. 5 | 6 | It supports: 7 | * Multiple iPhone apps managed from the same Rails application as well as a legacy default "app" with certs stored in config 8 | * Individual notifications and group notifications 9 | * Alerts, badges, sounds, and custom properties in notifications 10 | * Pull notifications 11 | 12 | == Feature Descriptions 13 | 14 | Multiple iPhone Apps: In previous versions of this gem a single Rails application was set up to 15 | manage push notifications for a single iPhone app. In many cases it is useful to have a single Rails 16 | app manage push notifications for multiple iPhone apps. With the addition of an APN::App model, this 17 | is now possible. The certificates are now stored on instances of APN::App and devices are intended to be associated 18 | with a particular app. For compatibility with existing implementations it is still possible to create devices that 19 | are not associated with an APN::App and to send individual notifications to them using the certs stored in the 20 | config directory. 21 | 22 | Individual and Group Notifications: Previous versions of this gem treated each notification individually 23 | and did not provide a built-in way to send a broadcast notification to a group of devices. Group notifications 24 | are now built into the gem. A group notification is associated with a group of devices and shares its 25 | contents across the entire group of devices. (Group notifications are only available for groups of devices associated 26 | with an APN::App) 27 | 28 | Notification Content Areas: Notifications may contain alerts, badges, sounds, and custom properties. 29 | 30 | Pull Notifications: This version of the gem supports an alternative notification method that relies 31 | on pulls from client devices and does not interact with the Apple Push Notification servers. This feature 32 | may be used entirely independently of the push notification features. Pull notifications may be 33 | created for an app. A client app can query for the most recent pull notification available since a 34 | given date to retrieve any notifications waiting for it. 35 | 36 | ==Version 0.4.1 Notes 37 | 38 | * 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. 39 | * Batched finds. Finds on the APN::Device model that can return large numbers of records have been batched to limit memory impact. 40 | * 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. 41 | * 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. 42 | 43 | ==Acknowledgements: 44 | 45 | From Mark Bates: 46 | 47 | This gem is a re-write of a plugin that was written by Fabien Penso and Sam Soffes. 48 | Their plugin was a great start, but it just didn't quite reach the level I hoped it would. 49 | I've re-written, as a gem, added a ton of tests, and I would like to think that I made it 50 | a little nicer and easier to use. 51 | 52 | From Rebecca Nesson (PRX.org): 53 | 54 | This gem extends the original version that Mark Bates adapted. His gem did the hard 55 | work of setting up and handling all communication with the Apple push notification servers. 56 | 57 | ==Converting Your Certificate: 58 | 59 | Once you have the certificate from Apple for your application, export your key 60 | and the apple certificate as p12 files. Here is a quick walkthrough on how to do this: 61 | 62 | 1. Click the disclosure arrow next to your certificate in Keychain Access and select the certificate and the key. 63 | 2. Right click and choose `Export 2 items...`. 64 | 3. Choose the p12 format from the drop down and name it `cert.p12`. 65 | 66 | Now covert the p12 file to a pem file: 67 | 68 | $ openssl pkcs12 -in cert.p12 -out apple_push_notification_production.pem -nodes -clcerts 69 | 70 | If you are using a development certificate, then change the name to apple_push_notification_development.pem instead. 71 | 72 | Store the contents of the certificate files on the app model for the app you want to send notifications to. 73 | 74 | ==Installing: 75 | 76 | ===Stable (RubyForge): 77 | 78 | $ sudo gem install apn_on_rails 79 | 80 | ===Edge (GitHub): 81 | 82 | $ sudo gem install PRX-apn_on_rails.git --source=http://gems.github.com 83 | 84 | ===Rails Gem Management: 85 | 86 | If you like to use the built in Rails gem management: 87 | 88 | config.gem 'apn_on_rails' 89 | 90 | Or, if you like to live on the edge: 91 | 92 | config.gem 'PRX-apn_on_rails', :lib => 'apn_on_rails', :source => 'http://gems.github.com' 93 | 94 | ==Setup and Configuration: 95 | 96 | Once you have the gem installed via your favorite gem installation, you need to require it so you can 97 | start to use it: 98 | 99 | Add the following require, wherever it makes sense to you: 100 | 101 | require 'apn_on_rails' 102 | 103 | You also need to add the following to your Rakefile so you can use the 104 | Rake tasks that ship with APN on Rails: 105 | 106 | begin 107 | require 'apn_on_rails_tasks' 108 | rescue MissingSourceFile => e 109 | puts e.message 110 | end 111 | 112 | Now, to create the tables you need for APN on Rails, run the following task: 113 | 114 | $ ruby script/generate apn_migrations 115 | 116 | APN on Rails uses the Configatron gem, http://github.com/markbates/configatron/tree/master, 117 | to configure itself. (With the change to multi-app support, the certifications are stored in the 118 | database rather than in the config directory, however, it is still possible to use the default "app" and the certificates 119 | stored in the config directory. For this setup, the following configurations apply.) 120 | APN on Rails has the following default configurations that you change as you see fit: 121 | 122 | # development (delivery): 123 | configatron.apn.passphrase # => '' 124 | configatron.apn.port # => 2195 125 | configatron.apn.host # => 'gateway.sandbox.push.apple.com' 126 | configatron.apn.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_development.pem') 127 | 128 | # production (delivery): 129 | configatron.apn.host # => 'gateway.push.apple.com' 130 | configatron.apn.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_production.pem') 131 | 132 | # development (feedback): 133 | configatron.apn.feedback.passphrase # => '' 134 | configatron.apn.feedback.port # => 2196 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') 137 | 138 | # production (feedback): 139 | configatron.apn.feedback.host # => 'feedback.push.apple.com' 140 | configatron.apn.feedback.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_production.pem') 141 | 142 | That's it, now you're ready to start creating notifications. 143 | 144 | ===Upgrade Notes: 145 | 146 | If you are upgrading to a new version of APN on Rails you should always run: 147 | 148 | $ ruby script/generate apn_migrations 149 | 150 | That way you ensure you have the latest version of the database tables needed. 151 | 152 | ==Example (assuming you have created an app and stored your keys on it): 153 | 154 | $ ./script/console 155 | >> app = APN::App.create(:name => "My App", :apn_dev_cert => "PASTE YOUR DEV CERT HERE", :apn_prod_cert => "PASTE YOUR PROD CERT HERE") 156 | >> device = APN::Device.create(:token => "XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX",:app_id => app.id) 157 | >> notification = APN::Notification.new 158 | >> notification.device = device 159 | >> notification.badge = 5 160 | >> notification.sound = true 161 | >> notification.alert = "foobar" 162 | >> notification.custom_properties = {:link => "http://www.prx.org"} 163 | >> notification.save 164 | 165 | You can use the following Rake task to deliver your individual notifications: 166 | 167 | $ rake apn:notifications:deliver 168 | 169 | And the following task to deliver your group notifications: 170 | 171 | $ rake apn:group_notifications:deliver 172 | 173 | The Rake task will find any unsent notifications in the database. If there aren't any notifications 174 | it will simply do nothing. If there are notifications waiting to be delivered it will open a single connection 175 | to Apple and push all the notifications through that one connection. Apple does not like people opening/closing 176 | connections constantly, so it's pretty important that you are careful about batching up your notifications so 177 | Apple doesn't shut you down. 178 | 179 | Released under the MIT license. 180 | -------------------------------------------------------------------------------- /spec/apn_on_rails/app/models/apn/app_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'spec_helper.rb') 2 | 3 | describe APN::App do 4 | 5 | describe 'send_notifications' do 6 | 7 | it 'should send the unsent notifications' do 8 | 9 | app = AppFactory.create 10 | device = DeviceFactory.create({:app_id => app.id}) 11 | notifications = [NotificationFactory.create({:device_id => device.id}), 12 | NotificationFactory.create({:device_id => device.id})] 13 | 14 | notifications.each_with_index do |notify, i| 15 | notify.stub(:message_for_sending).and_return("message-#{i}") 16 | notify.should_receive(:sent_at=).with(instance_of(Time)) 17 | notify.should_receive(:save) 18 | end 19 | 20 | APN::App.should_receive(:all).once.and_return([app]) 21 | app.should_receive(:cert).twice.and_return(app.apn_dev_cert) 22 | 23 | APN::Device.should_receive(:find_each).twice.and_yield(device) 24 | 25 | device.should_receive(:unsent_notifications).and_return(notifications,[]) 26 | 27 | 28 | ssl_mock = mock('ssl_mock') 29 | ssl_mock.should_receive(:write).with('message-0') 30 | ssl_mock.should_receive(:write).with('message-1') 31 | APN::Connection.should_receive(:open_for_delivery).twice.and_yield(ssl_mock, nil) 32 | APN::App.send_notifications 33 | 34 | end 35 | 36 | end 37 | 38 | describe 'send_notifications_not_associated_with_an_app' do 39 | 40 | it 'should send unsent notifications that are associated with devices that are not with any app' do 41 | RAILS_ENV = 'staging' 42 | device = DeviceFactory.create 43 | device.app_id = nil 44 | device.save 45 | APN::App.all.each { |a| a.destroy } 46 | notifications = [NotificationFactory.create({:device_id => device.id}), 47 | NotificationFactory.create({:device_id => device.id})] 48 | 49 | notifications.each_with_index do |notify, i| 50 | notify.stub(:message_for_sending).and_return("message-#{i}") 51 | notify.should_receive(:sent_at=).with(instance_of(Time)) 52 | notify.should_receive(:save) 53 | end 54 | 55 | APN::Device.should_receive(:find_each).and_yield(device) 56 | device.should_receive(:unsent_notifications).and_return(notifications) 57 | 58 | ssl_mock = mock('ssl_mock') 59 | ssl_mock.should_receive(:write).with('message-0') 60 | ssl_mock.should_receive(:write).with('message-1') 61 | APN::Connection.should_receive(:open_for_delivery).and_yield(ssl_mock, nil) 62 | APN::App.send_notifications 63 | end 64 | end 65 | 66 | describe 'send_group_notifications' do 67 | 68 | it 'should send the unsent group notifications' do 69 | 70 | app = AppFactory.create 71 | device = DeviceFactory.create({:app_id => app.id}) 72 | group = GroupFactory.create({:app_id => app.id}) 73 | device_grouping = DeviceGroupingFactory.create({:group_id => group.id,:device_id => device.id}) 74 | gnotys = [GroupNotificationFactory.create({:group_id => group.id}), 75 | GroupNotificationFactory.create({:group_id => group.id})] 76 | gnotys.each_with_index do |gnoty, i| 77 | gnoty.stub!(:message_for_sending).and_return("message-#{i}") 78 | gnoty.should_receive(:sent_at=).with(instance_of(Time)) 79 | gnoty.should_receive(:save) 80 | end 81 | 82 | APN::App.should_receive(:all).and_return([app]) 83 | app.should_receive(:unsent_group_notifications).at_least(:once).and_return(gnotys) 84 | app.should_receive(:cert).twice.and_return(app.apn_dev_cert) 85 | 86 | ssl_mock = mock('ssl_mock') 87 | ssl_mock.should_receive(:write).with('message-0') 88 | ssl_mock.should_receive(:write).with('message-1') 89 | APN::Connection.should_receive(:open_for_delivery).and_yield(ssl_mock, nil) 90 | 91 | APN::App.send_group_notifications 92 | 93 | end 94 | 95 | end 96 | 97 | describe 'send single group notification' do 98 | 99 | it 'should send the argument group notification' do 100 | app = AppFactory.create 101 | device = DeviceFactory.create({:app_id => app.id}) 102 | group = GroupFactory.create({:app_id => app.id}) 103 | device_grouping = DeviceGroupingFactory.create({:group_id => group.id,:device_id => device.id}) 104 | gnoty = GroupNotificationFactory.create({:group_id => group.id}) 105 | gnoty.stub!(:message_for_sending).and_return("message-0") 106 | gnoty.should_receive(:sent_at=).with(instance_of(Time)) 107 | gnoty.should_receive(:save) 108 | 109 | app.should_receive(:cert).at_least(:once).and_return(app.apn_dev_cert) 110 | 111 | ssl_mock = mock('ssl_mock') 112 | ssl_mock.should_receive(:write).with('message-0') 113 | APN::Connection.should_receive(:open_for_delivery).and_yield(ssl_mock, nil) 114 | 115 | app.send_group_notification(gnoty) 116 | end 117 | 118 | end 119 | 120 | describe 'nil cert when sending notifications' do 121 | 122 | it 'should raise an exception for sending notifications for an app with no cert' do 123 | app = AppFactory.create 124 | APN::App.should_receive(:all).and_return([app]) 125 | app.should_receive(:cert).and_return(nil) 126 | lambda { 127 | APN::App.send_notifications 128 | }.should raise_error(APN::Errors::MissingCertificateError) 129 | end 130 | 131 | end 132 | 133 | describe 'nil cert when sending group notifications' do 134 | 135 | it 'should raise an exception for sending group notifications for an app with no cert' do 136 | app = AppFactory.create 137 | APN::App.should_receive(:all).and_return([app]) 138 | app.should_receive(:cert).and_return(nil) 139 | lambda { 140 | APN::App.send_group_notifications 141 | }.should raise_error(APN::Errors::MissingCertificateError) 142 | end 143 | 144 | end 145 | 146 | describe 'nil cert when sending single group notification' do 147 | 148 | it 'should raise an exception for sending group notifications for an app with no cert' do 149 | app = AppFactory.create 150 | device = DeviceFactory.create({:app_id => app.id}) 151 | group = GroupFactory.create({:app_id => app.id}) 152 | device_grouping = DeviceGroupingFactory.create({:group_id => group.id,:device_id => device.id}) 153 | gnoty = GroupNotificationFactory.create({:group_id => group.id}) 154 | app.should_receive(:cert).and_return(nil) 155 | lambda { 156 | app.send_group_notification(gnoty) 157 | }.should raise_error(APN::Errors::MissingCertificateError) 158 | end 159 | 160 | end 161 | 162 | describe 'process_devices' do 163 | 164 | it 'should destroy devices that have a last_registered_at date that is before the feedback_at date' do 165 | app = AppFactory.create 166 | devices = [DeviceFactory.create(:app_id => app.id, :last_registered_at => 1.week.ago, :feedback_at => Time.now), 167 | DeviceFactory.create(:app_id => app.id, :last_registered_at => 1.week.from_now, :feedback_at => Time.now)] 168 | puts "device ids are #{devices[0].id} and #{devices[1].id}" 169 | devices[0].last_registered_at = 1.week.ago 170 | devices[0].save 171 | devices[1].last_registered_at = 1.week.from_now 172 | devices[1].save 173 | APN::Feedback.should_receive(:devices).twice.and_return(devices) 174 | APN::App.should_receive(:all).and_return([app]) 175 | app.should_receive(:cert).twice.and_return(app.apn_dev_cert) 176 | lambda { 177 | APN::App.process_devices 178 | }.should change(APN::Device, :count).by(-1) 179 | end 180 | 181 | end 182 | 183 | describe 'process_devices for global app' do 184 | 185 | it 'should destroy devices that have a last_registered_at date that is before the feedback_at date that have no app' do 186 | device = DeviceFactory.create(:app_id => nil, :last_registered_at => 1.week.ago, :feedback_at => Time.now) 187 | device.app_id = nil 188 | device.last_registered_at = 1.week.ago 189 | device.save 190 | APN::Feedback.should_receive(:devices).and_return([device]) 191 | APN::App.should_receive(:all).and_return([]) 192 | lambda { 193 | APN::App.process_devices 194 | }.should change(APN::Device, :count).by(-1) 195 | end 196 | end 197 | 198 | describe 'nil cert when processing devices' do 199 | 200 | it 'should raise an exception for processing devices for an app with no cert' do 201 | app = AppFactory.create 202 | APN::App.should_receive(:all).and_return([app]) 203 | app.should_receive(:cert).and_return(nil) 204 | lambda { 205 | APN::App.process_devices 206 | }.should raise_error(APN::Errors::MissingCertificateError) 207 | end 208 | 209 | end 210 | 211 | describe 'cert for production environment' do 212 | 213 | it 'should return the production cert for the app' do 214 | app = AppFactory.create 215 | RAILS_ENV = 'production' 216 | app.cert.should == app.apn_prod_cert 217 | end 218 | 219 | end 220 | 221 | describe 'cert for development and staging environment' do 222 | 223 | it 'should return the development cert for the app' do 224 | app = AppFactory.create 225 | RAILS_ENV = 'staging' 226 | app.cert.should == app.apn_dev_cert 227 | end 228 | end 229 | 230 | end -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. APN on Rails (Apple Push Notifications on Rails) 2 | 3 | APN on Rails is a Ruby on Rails gem that allows you to easily add Apple Push Notification (iPhone) 4 | support to your Rails application. 5 | 6 | It supports: 7 | * Multiple iPhone apps managed from the same Rails application as well as a legacy default "app" with certs stored in config 8 | * Individual notifications and group notifications 9 | * Alerts, badges, sounds, and custom properties in notifications 10 | * Pull notifications 11 | 12 | h2. Feature Descriptions 13 | 14 | Multiple iPhone Apps: In previous versions of this gem a single Rails application was set up to 15 | manage push notifications for a single iPhone app. In many cases it is useful to have a single Rails 16 | app manage push notifications for multiple iPhone apps. With the addition of an APN::App model, this 17 | is now possible. The certificates are now stored on instances of APN::App and all devices are intended to be associated 18 | with a particular app. For compatibility with existing implementations it is still possible to create devices that 19 | are not associated with an APN::App and to send individual notifications to them using the certs stored in the 20 | config directory. 21 | 22 | Individual and Group Notifications: Previous versions of this gem treated each notification individually 23 | and did not provide a built-in way to send a broadcast notification to a group of devices. Group notifications 24 | are now built into the gem. A group notification is associated with a group of devices and shares its 25 | contents across the entire group of devices. (Group notifications are only available for groups of devices associated 26 | with an APN::App) 27 | 28 | Notification Content Areas: Notifications may contain alerts, badges, sounds, and custom properties. 29 | 30 | Pull Notifications: This version of the gem supports an alternative notification method that relies 31 | on pulls from client devices and does not interact with the Apple Push Notification servers. This feature 32 | may be used entirely independently of the push notification features. Pull notifications may be 33 | created for an app. A client app can query for the most recent pull notification available since a 34 | given date to retrieve any notifications waiting for it. 35 | 36 | h2. Version 0.4.1 Notes 37 | 38 | * 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. 39 | * Batched finds. Finds on the APN::Device model that can return large numbers of records have been batched to limit memory impact. 40 | * 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. 41 | * 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. 42 | 43 | h2. Acknowledgements: 44 | 45 | From Mark Bates: 46 | 47 | This gem is a re-write of a plugin that was written by Fabien Penso and Sam Soffes. 48 | Their plugin was a great start, but it just didn't quite reach the level I hoped it would. 49 | I've re-written, as a gem, added a ton of tests, and I would like to think that I made it 50 | a little nicer and easier to use. 51 | 52 | From Rebecca Nesson (PRX.org): 53 | 54 | This gem extends the original version that Mark Bates adapted. His gem did the hard 55 | work of setting up and handling all communication with the Apple push notification servers. 56 | 57 | h2. Converting Your Certificate: 58 | 59 | Once you have the certificate from Apple for your application, export your key 60 | and the apple certificate as p12 files. Here is a quick walkthrough on how to do this: 61 | 62 | 1. Click the disclosure arrow next to your certificate in Keychain Access and select the certificate and the key. 63 | 2. Right click and choose `Export 2 items...`. 64 | 3. Choose the p12 format from the drop down and name it `cert.p12`. 65 | 66 | Now covert the p12 file to a pem file: 67 | 68 |
69 | $ openssl pkcs12 -in cert.p12 -out apple_push_notification_production.pem -nodes -clcerts
70 |
71 |
72 | If you are using a development certificate, then change the name to apple_push_notification_development.pem instead.
73 |
74 | Store the contents of the certificate files on the app model for the app you want to send notifications to.
75 |
76 | h2. Installing:
77 |
78 | h3. Stable (RubyForge):
79 |
80 |
81 | $ sudo gem install apn_on_rails
82 |
83 |
84 | h3. Edge (GitHub):
85 |
86 |
87 | $ sudo gem install PRX-apn_on_rails.git --source=http://gems.github.com
88 |
89 |
90 | h3. Rails Gem Management:
91 |
92 | If you like to use the built in Rails gem management:
93 |
94 |
95 | config.gem 'apn_on_rails'
96 |
97 |
98 | Or, if you like to live on the edge:
99 |
100 |
101 | config.gem 'PRX-apn_on_rails', :lib => 'apn_on_rails', :source => 'http://gems.github.com'
102 |
103 |
104 | h2. Setup and Configuration:
105 |
106 | Once you have the gem installed via your favorite gem installation, you need to require it so you can
107 | start to use it:
108 |
109 | Add the following require, wherever it makes sense to you:
110 |
111 |
112 | require 'apn_on_rails'
113 |
114 |
115 | You also need to add the following to your Rakefile so you can use the
116 | Rake tasks that ship with APN on Rails:
117 |
118 |
119 | begin
120 | require 'apn_on_rails_tasks'
121 | rescue MissingSourceFile => e
122 | puts e.message
123 | end
124 |
125 |
126 | You now need to run the APN generator to create the migration files.
127 |
128 |
129 | ruby script/rails generate apn_on_rails:install
130 |
131 |
132 | Now, to create the tables you need for APN on Rails, run the following task:
133 |
134 |
135 | $ rake db:migrate
136 |
137 |
138 | APN on Rails uses the Configatron gem, http://github.com/markbates/configatron/tree/master,
139 | to configure itself. With the change to multi-app support, the certifications are stored in the
140 | database rather than in the config directory.
141 |
142 | However, it is still possible to use the default "app" and the certificates
143 | stored in the config directory. For this setup, the following configurations apply.)
144 | APN on Rails has the following default configurations that you change for your setup.
145 |
146 | To generate the files that will be used here, run
147 |
148 |
149 | script/rails generate configatron:install
150 |
151 |
152 | Then customize the config/configatron files that were generated to your settings.
153 |
154 |
155 | # development (delivery):
156 | configatron.apn.passphrase # => ''
157 | configatron.apn.port # => 2195
158 | configatron.apn.host # => 'gateway.sandbox.push.apple.com'
159 | configatron.apn.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_development.pem')
160 |
161 | # production (delivery):
162 | configatron.apn.host # => 'gateway.push.apple.com'
163 | configatron.apn.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_production.pem')
164 |
165 | # development (feedback):
166 | configatron.apn.feedback.passphrase # => ''
167 | configatron.apn.feedback.port # => 2196
168 | configatron.apn.feedback.host # => 'feedback.sandbox.push.apple.com'
169 | configatron.apn.feedback.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_development.pem')
170 |
171 | # production (feedback):
172 | configatron.apn.feedback.host # => 'feedback.push.apple.com'
173 | configatron.apn.feedback.cert #=> File.join(RAILS_ROOT, 'config', 'apple_push_notification_production.pem')
174 |
175 |
176 | That's it, now you're ready to start creating notifications.
177 |
178 | h3. Upgrade Notes:
179 |
180 | If you are upgrading to a new version of APN on Rails you should always run:
181 |
182 |
183 | ruby script/rails generate apn_on_rails:install
184 |
185 |
186 | That way you ensure you have the latest version of the database tables needed.
187 | (There is an unaddressed problem in which migration 002 was modified in the repo to add the column custom_properties.
188 | If you installed the gem prior to that change and try to upgrade following this path you will have to add the
189 | custom_properties column to the apn_notifications table by hand.)
190 |
191 | h2. Example (assuming you have created an app and stored your keys on it):
192 |
193 |
194 | $ ./script/console
195 | >> app = APN::App.create(:apn_dev_cert => "PASTE YOUR DEV CERT HERE", :apn_prod_cert => "PASTE YOUR PROD CERT HERE")
196 | >> device = APN::Device.create(:token => "XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX",:app_id => app.id)
197 | >> notification = APN::Notification.new
198 | >> notification.device = device
199 | >> notification.badge = 5
200 | >> notification.sound = true
201 | >> notification.alert = "foobar"
202 | >> notification.custom_properties = {:link => "http://www.prx.org"}
203 | >> notification.save
204 |
205 |
206 | You can use the following Rake task to deliver your individual notifications:
207 |
208 |
209 | $ rake apn:notifications:deliver
210 |
211 |
212 | And the following task to deliver your group notifications:
213 |
214 |
215 | $ rake apn:group_notifications:deliver
216 |
217 |
218 | The Rake task will find any unsent notifications in the database. If there aren't any notifications
219 | it will simply do nothing. If there are notifications waiting to be delivered it will open a single connection
220 | to Apple and push all the notifications through that one connection. Apple does not like people opening/closing
221 | connections constantly, so it's pretty important that you are careful about batching up your notifications so
222 | Apple doesn't shut you down.
223 |
224 | Released under the MIT license.
225 |
--------------------------------------------------------------------------------