apn_on_rails 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. data/.gitignore +16 -0
  2. data/.rspec +2 -0
  3. data/.specification +80 -0
  4. data/Gemfile +18 -0
  5. data/Gemfile.lock +45 -0
  6. data/README +51 -9
  7. data/README.textile +198 -0
  8. data/Rakefile +49 -0
  9. data/VERSION +1 -0
  10. data/apn_on_rails.gemspec +110 -0
  11. data/generators/templates/apn_migrations/004_create_apn_apps.rb +18 -0
  12. data/generators/templates/apn_migrations/005_create_groups.rb +23 -0
  13. data/generators/templates/apn_migrations/006_alter_apn_groups.rb +11 -0
  14. data/generators/templates/apn_migrations/007_create_device_groups.rb +27 -0
  15. data/generators/templates/apn_migrations/008_create_apn_group_notifications.rb +23 -0
  16. data/generators/templates/apn_migrations/009_create_pull_notifications.rb +16 -0
  17. data/lib/apn_on_rails/apn_on_rails.rb +22 -3
  18. data/lib/apn_on_rails/app/models/apn/app.rb +115 -0
  19. data/lib/apn_on_rails/app/models/apn/device.rb +3 -1
  20. data/lib/apn_on_rails/app/models/apn/device_grouping.rb +16 -0
  21. data/lib/apn_on_rails/app/models/apn/group.rb +12 -0
  22. data/lib/apn_on_rails/app/models/apn/group_notification.rb +79 -0
  23. data/lib/apn_on_rails/app/models/apn/notification.rb +6 -30
  24. data/lib/apn_on_rails/app/models/apn/pull_notification.rb +15 -0
  25. data/lib/apn_on_rails/libs/connection.rb +2 -1
  26. data/lib/apn_on_rails/libs/feedback.rb +6 -18
  27. data/lib/apn_on_rails/tasks/apn.rake +13 -4
  28. data/spec/active_record/setup_ar.rb +19 -0
  29. data/spec/apn_on_rails/app/models/apn/app_spec.rb +178 -0
  30. data/spec/apn_on_rails/app/models/apn/device_spec.rb +60 -0
  31. data/spec/apn_on_rails/app/models/apn/group_notification_spec.rb +66 -0
  32. data/spec/apn_on_rails/app/models/apn/notification_spec.rb +71 -0
  33. data/spec/apn_on_rails/app/models/apn/pull_notification_spec.rb +37 -0
  34. data/spec/apn_on_rails/libs/connection_spec.rb +40 -0
  35. data/spec/apn_on_rails/libs/feedback_spec.rb +45 -0
  36. data/spec/extensions/string.rb +10 -0
  37. data/spec/factories/app_factory.rb +27 -0
  38. data/spec/factories/device_factory.rb +29 -0
  39. data/spec/factories/device_grouping_factory.rb +22 -0
  40. data/spec/factories/group_factory.rb +27 -0
  41. data/spec/factories/group_notification_factory.rb +22 -0
  42. data/spec/factories/notification_factory.rb +22 -0
  43. data/spec/factories/pull_notification_factory.rb +22 -0
  44. data/spec/fixtures/hexa.bin +1 -0
  45. data/spec/fixtures/message_for_sending.bin +0 -0
  46. data/spec/rails_root/config/apple_push_notification_development.pem +19 -0
  47. data/spec/spec_helper.rb +55 -0
  48. metadata +214 -24
@@ -10,9 +10,11 @@
10
10
  # Device.create(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz')
11
11
  class APN::Device < APN::Base
12
12
 
13
+ belongs_to :app, :class_name => 'APN::App'
13
14
  has_many :notifications, :class_name => 'APN::Notification'
15
+ has_many :unsent_notifications, :class_name => 'APN::Notification', :conditions => 'sent_at is null'
14
16
 
15
- validates_uniqueness_of :token
17
+ validates_uniqueness_of :token, :scope => :app_id
16
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}$/
17
19
 
18
20
  before_save :set_last_registered_at
@@ -0,0 +1,16 @@
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
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,79 @@
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 <tt>...</tt>
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
@@ -20,6 +20,7 @@ class APN::Notification < APN::Base
20
20
  serialize :custom_properties
21
21
 
22
22
  belongs_to :device, :class_name => 'APN::Device'
23
+ has_one :app, :class_name => 'APN::App', :through => :device
23
24
 
24
25
  # Stores the text alert message you want to send to the device.
25
26
  #
@@ -46,7 +47,7 @@ class APN::Notification < APN::Base
46
47
  # apn.badge = 0
47
48
  # apn.sound = true
48
49
  # apn.custom_properties = {"typ" => 1}
49
- # apn.apple_hast # => {"aps" => {"badge" => 0}}
50
+ # apn.apple_hash # => {"aps" => {"badge" => 0, "sound" => "1.aiff"}, "typ" => "1"}
50
51
  def apple_hash
51
52
  result = {}
52
53
  result['aps'] = {}
@@ -84,34 +85,9 @@ class APN::Notification < APN::Base
84
85
  message
85
86
  end
86
87
 
87
- class << self
88
-
89
- # Opens a connection to the Apple APN server and attempts to batch deliver
90
- # an Array of notifications.
91
- #
92
- # This method expects an Array of APN::Notifications. If no parameter is passed
93
- # in then it will use the following:
94
- # APN::Notification.all(:conditions => {:sent_at => nil})
95
- #
96
- # As each APN::Notification is sent the <tt>sent_at</tt> column will be timestamped,
97
- # so as to not be sent again.
98
- #
99
- # This can be run from the following Rake task:
100
- # $ rake apn:notifications:deliver
101
- def send_notifications(notifications = APN::Notification.all(:conditions => {:sent_at => nil}))
102
- unless notifications.nil? || notifications.empty?
103
-
104
- APN::Connection.open_for_delivery do |conn, sock|
105
- notifications.each do |noty|
106
- conn.write(noty.message_for_sending)
107
- noty.sent_at = Time.now
108
- noty.save
109
- end
110
- end
111
-
112
- end
113
- end
114
-
115
- end # class << self
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
116
92
 
117
93
  end # APN::Notification
@@ -0,0 +1,15 @@
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
+ conditions = if since_date
8
+ ["app_id = ? AND created_at > ?", app_id, since_date]
9
+ else
10
+ ["app_id = ?", app_id]
11
+ end
12
+
13
+ first(:order => "created_at DESC", :conditions => conditions)
14
+ end
15
+ end
@@ -47,7 +47,8 @@ module APN
47
47
  :passphrase => configatron.apn.passphrase,
48
48
  :host => configatron.apn.host,
49
49
  :port => configatron.apn.port}.merge(options)
50
- cert = File.read(options[:cert])
50
+ #cert = File.read(options[:cert])
51
+ cert = options[:cert]
51
52
  ctx = OpenSSL::SSL::SSLContext.new
52
53
  ctx.key = OpenSSL::PKey::RSA.new(cert, options[:passphrase])
53
54
  ctx.cert = OpenSSL::X509::Certificate.new(cert)
@@ -10,9 +10,10 @@ module APN
10
10
  # has received feedback from Apple. Each APN::Device will
11
11
  # have it's <tt>feedback_at</tt> accessor marked with the time
12
12
  # that Apple believes the device de-registered itself.
13
- def devices(&block)
13
+ def devices(cert, &block)
14
14
  devices = []
15
- APN::Connection.open_for_feedback do |conn, sock|
15
+ return if cert.nil?
16
+ APN::Connection.open_for_feedback({:cert => cert}) do |conn, sock|
16
17
  while line = sock.gets # Read lines from the socket
17
18
  line.strip!
18
19
  feedback = line.unpack('N1n1H140')
@@ -28,23 +29,10 @@ module APN
28
29
  return devices
29
30
  end # devices
30
31
 
31
- # Retrieves a list of APN::Device instnces from Apple using
32
- # the <tt>devices</tt> method. It then checks to see if the
33
- # <tt>last_registered_at</tt> date of each APN::Device is
34
- # before the date that Apple says the device is no longer
35
- # accepting notifications then the device is deleted. Otherwise
36
- # it is assumed that the application has been re-installed
37
- # and is available for notifications.
38
- #
39
- # This can be run from the following Rake task:
40
- # $ rake apn:feedback:process
41
32
  def process_devices
42
- APN::Feedback.devices.each do |device|
43
- if device.last_registered_at < device.feedback_at
44
- device.destroy
45
- end
46
- end
47
- end # process_devices
33
+ ActiveSupport::Deprecation.warn("The method APN::Feedback.process_devices is deprecated. Use APN::App.process_devices instead.")
34
+ APN::App.process_devices
35
+ end
48
36
 
49
37
  end # class << self
50
38
 
@@ -4,18 +4,27 @@ namespace :apn do
4
4
 
5
5
  desc "Deliver all unsent APN notifications."
6
6
  task :deliver => [:environment] do
7
- APN::Notification.send_notifications
7
+ APN::App.send_notifications
8
8
  end
9
-
9
+
10
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
11
20
 
12
21
  namespace :feedback do
13
22
 
14
23
  desc "Process all devices that have feedback from APN."
15
24
  task :process => [:environment] do
16
- APN::Feedback.process_devices
25
+ APN::App.process_devices
17
26
  end
18
27
 
19
28
  end
20
29
 
21
- end # apn
30
+ end # apn
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+
4
+ logger = Logger.new(STDOUT)
5
+ logger.level = Logger::INFO
6
+ ActiveRecord::Base.logger = logger
7
+
8
+ db_file = File.join(File.dirname(__FILE__), 'test.db')
9
+ FileUtils.rm(db_file) if File.exists?(db_file)
10
+ # File.open(db_file, 'w')
11
+
12
+ ActiveRecord::Base.establish_connection({
13
+ :adapter => 'sqlite3',
14
+ :database => db_file
15
+ })
16
+
17
+ ActiveRecord::Migrator.up(File.join(File.dirname(__FILE__), '..', '..', 'generators', 'templates', 'apn_migrations'))
18
+
19
+ # raise hell
@@ -0,0 +1,178 @@
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
+ notifications.each_with_index do |notify, i|
14
+ notify.stub(:message_for_sending).and_return("message-#{i}")
15
+ notify.should_receive(:sent_at=).with(instance_of(Time))
16
+ notify.should_receive(:save)
17
+ end
18
+
19
+ APN::App.should_receive(:all).and_return([app])
20
+ app.should_receive(:unsent_notifications).at_least(:once).and_return(notifications)
21
+ app.should_receive(:cert).twice.and_return(app.apn_dev_cert)
22
+
23
+ ssl_mock = mock('ssl_mock')
24
+ ssl_mock.should_receive(:write).with('message-0')
25
+ ssl_mock.should_receive(:write).with('message-1')
26
+ APN::Connection.should_receive(:open_for_delivery).and_yield(ssl_mock, nil)
27
+
28
+ APN::App.send_notifications
29
+
30
+ end
31
+
32
+ end
33
+
34
+ describe 'send_group_notifications' do
35
+
36
+ it 'should send the unsent group notifications' do
37
+
38
+ app = AppFactory.create
39
+ device = DeviceFactory.create({:app_id => app.id})
40
+ group = GroupFactory.create({:app_id => app.id})
41
+ device_grouping = DeviceGroupingFactory.create({:group_id => group.id,:device_id => device.id})
42
+ gnotys = [GroupNotificationFactory.create({:group_id => group.id}),
43
+ GroupNotificationFactory.create({:group_id => group.id})]
44
+ gnotys.each_with_index do |gnoty, i|
45
+ gnoty.stub!(:message_for_sending).and_return("message-#{i}")
46
+ gnoty.should_receive(:sent_at=).with(instance_of(Time))
47
+ gnoty.should_receive(:save)
48
+ end
49
+
50
+ APN::App.should_receive(:all).and_return([app])
51
+ app.should_receive(:unsent_group_notifications).at_least(:once).and_return(gnotys)
52
+ app.should_receive(:cert).twice.and_return(app.apn_dev_cert)
53
+
54
+ ssl_mock = mock('ssl_mock')
55
+ ssl_mock.should_receive(:write).with('message-0')
56
+ ssl_mock.should_receive(:write).with('message-1')
57
+ APN::Connection.should_receive(:open_for_delivery).and_yield(ssl_mock, nil)
58
+
59
+ APN::App.send_group_notifications
60
+
61
+ end
62
+
63
+ end
64
+
65
+ describe 'send single group notification' do
66
+
67
+ it 'should send the argument group notification' do
68
+ app = AppFactory.create
69
+ device = DeviceFactory.create({:app_id => app.id})
70
+ group = GroupFactory.create({:app_id => app.id})
71
+ device_grouping = DeviceGroupingFactory.create({:group_id => group.id,:device_id => device.id})
72
+ gnoty = GroupNotificationFactory.create({:group_id => group.id})
73
+ gnoty.stub!(:message_for_sending).and_return("message-0")
74
+ gnoty.should_receive(:sent_at=).with(instance_of(Time))
75
+ gnoty.should_receive(:save)
76
+
77
+ app.should_receive(:cert).at_least(:once).and_return(app.apn_dev_cert)
78
+
79
+ ssl_mock = mock('ssl_mock')
80
+ ssl_mock.should_receive(:write).with('message-0')
81
+ APN::Connection.should_receive(:open_for_delivery).and_yield(ssl_mock, nil)
82
+
83
+ app.send_group_notification(gnoty)
84
+ end
85
+
86
+ end
87
+
88
+ describe 'nil cert when sending notifications' do
89
+
90
+ it 'should raise an exception for sending notifications for an app with no cert' do
91
+ app = AppFactory.create
92
+ APN::App.should_receive(:all).and_return([app])
93
+ app.should_receive(:cert).and_return(nil)
94
+ lambda {
95
+ APN::App.send_notifications
96
+ }.should raise_error(APN::Errors::MissingCertificateError)
97
+ end
98
+
99
+ end
100
+
101
+ describe 'nil cert when sending group notifications' do
102
+
103
+ it 'should raise an exception for sending group notifications for an app with no cert' do
104
+ app = AppFactory.create
105
+ APN::App.should_receive(:all).and_return([app])
106
+ app.should_receive(:cert).and_return(nil)
107
+ lambda {
108
+ APN::App.send_group_notifications
109
+ }.should raise_error(APN::Errors::MissingCertificateError)
110
+ end
111
+
112
+ end
113
+
114
+ describe 'nil cert when sending single group notification' do
115
+
116
+ it 'should raise an exception for sending group notifications for an app with no cert' do
117
+ app = AppFactory.create
118
+ device = DeviceFactory.create({:app_id => app.id})
119
+ group = GroupFactory.create({:app_id => app.id})
120
+ device_grouping = DeviceGroupingFactory.create({:group_id => group.id,:device_id => device.id})
121
+ gnoty = GroupNotificationFactory.create({:group_id => group.id})
122
+ app.should_receive(:cert).and_return(nil)
123
+ lambda {
124
+ app.send_group_notification(gnoty)
125
+ }.should raise_error(APN::Errors::MissingCertificateError)
126
+ end
127
+
128
+ end
129
+
130
+ describe 'process_devices' do
131
+
132
+ it 'should destroy devices that have a last_registered_at date that is before the feedback_at date' do
133
+ app = AppFactory.create
134
+ devices = [DeviceFactory.create(:app_id => app.id, :last_registered_at => 1.week.ago, :feedback_at => Time.now),
135
+ DeviceFactory.create(:app_id => app.id, :last_registered_at => 1.week.from_now, :feedback_at => Time.now)]
136
+ APN::Feedback.should_receive(:devices).and_return(devices)
137
+ APN::App.should_receive(:all).and_return([app])
138
+ app.should_receive(:cert).twice.and_return(app.apn_dev_cert)
139
+ lambda {
140
+ APN::App.process_devices
141
+ }.should change(APN::Device, :count).by(-1)
142
+ end
143
+
144
+ end
145
+
146
+ describe 'nil cert when processing devices' do
147
+
148
+ it 'should raise an exception for processing devices for an app with no cert' do
149
+ app = AppFactory.create
150
+ APN::App.should_receive(:all).and_return([app])
151
+ app.should_receive(:cert).and_return(nil)
152
+ lambda {
153
+ APN::App.process_devices
154
+ }.should raise_error(APN::Errors::MissingCertificateError)
155
+ end
156
+
157
+ end
158
+
159
+ describe 'cert for production environment' do
160
+
161
+ it 'should return the production cert for the app' do
162
+ app = AppFactory.create
163
+ RAILS_ENV = 'production'
164
+ app.cert.should == app.apn_prod_cert
165
+ end
166
+
167
+ end
168
+
169
+ describe 'cert for development and staging environment' do
170
+
171
+ it 'should return the development cert for the app' do
172
+ app = AppFactory.create
173
+ RAILS_ENV = 'staging'
174
+ app.cert.should == app.apn_dev_cert
175
+ end
176
+ end
177
+
178
+ end