fireinc-apn_on_rails 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/.rspec +2 -0
  2. data/.specification +80 -0
  3. data/Gemfile +19 -0
  4. data/Gemfile.lock +47 -0
  5. data/LICENSE +21 -0
  6. data/README +179 -0
  7. data/README.textile +209 -0
  8. data/Rakefile +49 -0
  9. data/VERSION +1 -0
  10. data/apn_on_rails.gemspec +144 -0
  11. data/autotest/discover.rb +1 -0
  12. data/generators/apn_migrations_generator.rb +31 -0
  13. data/generators/templates/apn_migrations/001_create_apn_devices.rb +13 -0
  14. data/generators/templates/apn_migrations/002_create_apn_notifications.rb +23 -0
  15. data/generators/templates/apn_migrations/003_alter_apn_devices.rb +25 -0
  16. data/generators/templates/apn_migrations/004_create_apn_apps.rb +18 -0
  17. data/generators/templates/apn_migrations/005_create_groups.rb +23 -0
  18. data/generators/templates/apn_migrations/006_alter_apn_groups.rb +11 -0
  19. data/generators/templates/apn_migrations/007_create_device_groups.rb +27 -0
  20. data/generators/templates/apn_migrations/008_create_apn_group_notifications.rb +23 -0
  21. data/generators/templates/apn_migrations/009_create_pull_notifications.rb +16 -0
  22. data/generators/templates/apn_migrations/010_alter_apn_notifications.rb +21 -0
  23. data/generators/templates/apn_migrations/011_make_device_token_index_nonunique.rb +11 -0
  24. data/generators/templates/apn_migrations/012_add_launch_notification_to_apn_pull_notifications.rb +9 -0
  25. data/lib/apn_on_rails.rb +4 -0
  26. data/lib/apn_on_rails/apn_on_rails.rb +81 -0
  27. data/lib/apn_on_rails/app/models/apn/app.rb +151 -0
  28. data/lib/apn_on_rails/app/models/apn/base.rb +9 -0
  29. data/lib/apn_on_rails/app/models/apn/device.rb +50 -0
  30. data/lib/apn_on_rails/app/models/apn/device_grouping.rb +16 -0
  31. data/lib/apn_on_rails/app/models/apn/group.rb +12 -0
  32. data/lib/apn_on_rails/app/models/apn/group_notification.rb +79 -0
  33. data/lib/apn_on_rails/app/models/apn/notification.rb +93 -0
  34. data/lib/apn_on_rails/app/models/apn/pull_notification.rb +28 -0
  35. data/lib/apn_on_rails/libs/connection.rb +70 -0
  36. data/lib/apn_on_rails/libs/feedback.rb +39 -0
  37. data/lib/apn_on_rails/tasks/apn.rake +30 -0
  38. data/lib/apn_on_rails/tasks/db.rake +19 -0
  39. data/lib/apn_on_rails_tasks.rb +3 -0
  40. data/spec/active_record/setup_ar.rb +19 -0
  41. data/spec/apn_on_rails/app/models/apn/app_spec.rb +230 -0
  42. data/spec/apn_on_rails/app/models/apn/device_spec.rb +60 -0
  43. data/spec/apn_on_rails/app/models/apn/group_notification_spec.rb +66 -0
  44. data/spec/apn_on_rails/app/models/apn/notification_spec.rb +71 -0
  45. data/spec/apn_on_rails/app/models/apn/pull_notification_spec.rb +100 -0
  46. data/spec/apn_on_rails/libs/connection_spec.rb +40 -0
  47. data/spec/apn_on_rails/libs/feedback_spec.rb +43 -0
  48. data/spec/extensions/string.rb +10 -0
  49. data/spec/factories/app_factory.rb +27 -0
  50. data/spec/factories/device_factory.rb +29 -0
  51. data/spec/factories/device_grouping_factory.rb +22 -0
  52. data/spec/factories/group_factory.rb +27 -0
  53. data/spec/factories/group_notification_factory.rb +22 -0
  54. data/spec/factories/notification_factory.rb +22 -0
  55. data/spec/factories/pull_notification_factory.rb +22 -0
  56. data/spec/fixtures/hexa.bin +1 -0
  57. data/spec/fixtures/message_for_sending.bin +0 -0
  58. data/spec/rails_root/config/apple_push_notification_development.pem +19 -0
  59. data/spec/spec_helper.rb +55 -0
  60. metadata +283 -0
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,70 @@
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
@@ -0,0 +1,39 @@
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 <tt>feedback_at</tt> 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
@@ -0,0 +1,30 @@
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
@@ -0,0 +1,19 @@
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
+ Then just run the migrations like you would normally:
12
+
13
+ $ rake db:migrate
14
+ }.strip
15
+ end
16
+
17
+ end # db
18
+
19
+ end # apn
@@ -0,0 +1,3 @@
1
+ Dir.glob(File.join(File.dirname(__FILE__), 'apn_on_rails', 'tasks', '**/*.rake')).each do |f|
2
+ load File.expand_path(f)
3
+ end
@@ -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,230 @@
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