gwong-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 +150 -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 +49 -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 +226 -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 +282 -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,226 @@
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::Notification.should_receive(:find).and_return(notifications, [])
24
+
25
+ ssl_mock = mock('ssl_mock')
26
+ ssl_mock.should_receive(:write).with('message-0')
27
+ ssl_mock.should_receive(:write).with('message-1')
28
+ APN::Connection.should_receive(:open_for_delivery).twice.and_yield(ssl_mock, nil)
29
+ APN::App.send_notifications
30
+
31
+ end
32
+
33
+ end
34
+
35
+ describe 'send_notifications_not_associated_with_an_app' do
36
+
37
+ it 'should send unsent notifications that are associated with devices that are not with any app' do
38
+ RAILS_ENV = 'staging'
39
+ device = DeviceFactory.create
40
+ device.app_id = nil
41
+ device.save
42
+ APN::App.all.each { |a| a.destroy }
43
+ notifications = [NotificationFactory.create({:device_id => device.id}),
44
+ NotificationFactory.create({:device_id => device.id})]
45
+
46
+ notifications.each_with_index do |notify, i|
47
+ notify.stub(:message_for_sending).and_return("message-#{i}")
48
+ notify.should_receive(:sent_at=).with(instance_of(Time))
49
+ notify.should_receive(:save)
50
+ end
51
+
52
+ APN::Notification.should_receive(:find).and_return(notifications)
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
+ APN::App.send_notifications
59
+ end
60
+ end
61
+
62
+ describe 'send_group_notifications' do
63
+
64
+ it 'should send the unsent group notifications' do
65
+
66
+ app = AppFactory.create
67
+ device = DeviceFactory.create({:app_id => app.id})
68
+ group = GroupFactory.create({:app_id => app.id})
69
+ device_grouping = DeviceGroupingFactory.create({:group_id => group.id,:device_id => device.id})
70
+ gnotys = [GroupNotificationFactory.create({:group_id => group.id}),
71
+ GroupNotificationFactory.create({:group_id => group.id})]
72
+ gnotys.each_with_index do |gnoty, i|
73
+ gnoty.stub!(:message_for_sending).and_return("message-#{i}")
74
+ gnoty.should_receive(:sent_at=).with(instance_of(Time))
75
+ gnoty.should_receive(:save)
76
+ end
77
+
78
+ APN::App.should_receive(:all).and_return([app])
79
+ app.should_receive(:unsent_group_notifications).at_least(:once).and_return(gnotys)
80
+ app.should_receive(:cert).twice.and_return(app.apn_dev_cert)
81
+
82
+ ssl_mock = mock('ssl_mock')
83
+ ssl_mock.should_receive(:write).with('message-0')
84
+ ssl_mock.should_receive(:write).with('message-1')
85
+ APN::Connection.should_receive(:open_for_delivery).and_yield(ssl_mock, nil)
86
+
87
+ APN::App.send_group_notifications
88
+
89
+ end
90
+
91
+ end
92
+
93
+ describe 'send single group notification' do
94
+
95
+ it 'should send the argument group notification' do
96
+ app = AppFactory.create
97
+ device = DeviceFactory.create({:app_id => app.id})
98
+ group = GroupFactory.create({:app_id => app.id})
99
+ device_grouping = DeviceGroupingFactory.create({:group_id => group.id,:device_id => device.id})
100
+ gnoty = GroupNotificationFactory.create({:group_id => group.id})
101
+ gnoty.stub!(:message_for_sending).and_return("message-0")
102
+ gnoty.should_receive(:sent_at=).with(instance_of(Time))
103
+ gnoty.should_receive(:save)
104
+
105
+ app.should_receive(:cert).at_least(:once).and_return(app.apn_dev_cert)
106
+
107
+ ssl_mock = mock('ssl_mock')
108
+ ssl_mock.should_receive(:write).with('message-0')
109
+ APN::Connection.should_receive(:open_for_delivery).and_yield(ssl_mock, nil)
110
+
111
+ app.send_group_notification(gnoty)
112
+ end
113
+
114
+ end
115
+
116
+ describe 'nil cert when sending notifications' do
117
+
118
+ it 'should raise an exception for sending notifications for an app with no cert' do
119
+ app = AppFactory.create
120
+ APN::App.should_receive(:all).and_return([app])
121
+ app.should_receive(:cert).and_return(nil)
122
+ lambda {
123
+ APN::App.send_notifications
124
+ }.should raise_error(APN::Errors::MissingCertificateError)
125
+ end
126
+
127
+ end
128
+
129
+ describe 'nil cert when sending group notifications' do
130
+
131
+ it 'should raise an exception for sending group notifications for an app with no cert' do
132
+ app = AppFactory.create
133
+ APN::App.should_receive(:all).and_return([app])
134
+ app.should_receive(:cert).and_return(nil)
135
+ lambda {
136
+ APN::App.send_group_notifications
137
+ }.should raise_error(APN::Errors::MissingCertificateError)
138
+ end
139
+
140
+ end
141
+
142
+ describe 'nil cert when sending single group notification' do
143
+
144
+ it 'should raise an exception for sending group notifications for an app with no cert' do
145
+ app = AppFactory.create
146
+ device = DeviceFactory.create({:app_id => app.id})
147
+ group = GroupFactory.create({:app_id => app.id})
148
+ device_grouping = DeviceGroupingFactory.create({:group_id => group.id,:device_id => device.id})
149
+ gnoty = GroupNotificationFactory.create({:group_id => group.id})
150
+ app.should_receive(:cert).and_return(nil)
151
+ lambda {
152
+ app.send_group_notification(gnoty)
153
+ }.should raise_error(APN::Errors::MissingCertificateError)
154
+ end
155
+
156
+ end
157
+
158
+ describe 'process_devices' do
159
+
160
+ it 'should destroy devices that have a last_registered_at date that is before the feedback_at date' do
161
+ app = AppFactory.create
162
+ devices = [DeviceFactory.create(:app_id => app.id, :last_registered_at => 1.week.ago, :feedback_at => Time.now),
163
+ DeviceFactory.create(:app_id => app.id, :last_registered_at => 1.week.from_now, :feedback_at => Time.now)]
164
+ puts "device ids are #{devices[0].id} and #{devices[1].id}"
165
+ devices[0].last_registered_at = 1.week.ago
166
+ devices[0].save
167
+ devices[1].last_registered_at = 1.week.from_now
168
+ devices[1].save
169
+ APN::Feedback.should_receive(:devices).twice.and_return(devices)
170
+ APN::App.should_receive(:all).and_return([app])
171
+ app.should_receive(:cert).twice.and_return(app.apn_dev_cert)
172
+ lambda {
173
+ APN::App.process_devices
174
+ }.should change(APN::Device, :count).by(-1)
175
+ end
176
+
177
+ end
178
+
179
+ describe 'process_devices for global app' do
180
+
181
+ it 'should destroy devices that have a last_registered_at date that is before the feedback_at date that have no app' do
182
+ device = DeviceFactory.create(:app_id => nil, :last_registered_at => 1.week.ago, :feedback_at => Time.now)
183
+ device.app_id = nil
184
+ device.last_registered_at = 1.week.ago
185
+ device.save
186
+ APN::Feedback.should_receive(:devices).and_return([device])
187
+ APN::App.should_receive(:all).and_return([])
188
+ lambda {
189
+ APN::App.process_devices
190
+ }.should change(APN::Device, :count).by(-1)
191
+ end
192
+ end
193
+
194
+ describe 'nil cert when processing devices' do
195
+
196
+ it 'should raise an exception for processing devices for an app with no cert' do
197
+ app = AppFactory.create
198
+ APN::App.should_receive(:all).and_return([app])
199
+ app.should_receive(:cert).and_return(nil)
200
+ lambda {
201
+ APN::App.process_devices
202
+ }.should raise_error(APN::Errors::MissingCertificateError)
203
+ end
204
+
205
+ end
206
+
207
+ describe 'cert for production environment' do
208
+
209
+ it 'should return the production cert for the app' do
210
+ app = AppFactory.create
211
+ RAILS_ENV = 'production'
212
+ app.cert.should == app.apn_prod_cert
213
+ end
214
+
215
+ end
216
+
217
+ describe 'cert for development and staging environment' do
218
+
219
+ it 'should return the development cert for the app' do
220
+ app = AppFactory.create
221
+ RAILS_ENV = 'staging'
222
+ app.cert.should == app.apn_dev_cert
223
+ end
224
+ end
225
+
226
+ end
@@ -0,0 +1,60 @@
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
+ device = DeviceFactory.new(:token => APN::Device.first.token)
9
+ device.should_not be_valid
10
+ device.errors['token'].should include('has already been taken')
11
+
12
+ device = DeviceFactory.new(:token => device.token.succ)
13
+ device.should be_valid
14
+ end
15
+
16
+ it 'should get cleansed if it contains brackets' do
17
+ token = DeviceFactory.random_token
18
+ device = DeviceFactory.new(:token => "<#{token}>")
19
+ device.token.should == token
20
+ device.token.should_not == "<#{token}>"
21
+ end
22
+
23
+ it 'should be in the correct pattern' do
24
+ device = DeviceFactory.new(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz')
25
+ device.should be_valid
26
+ device.token = '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6'
27
+ device.should_not be_valid
28
+ device.token = '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7!!'
29
+ device.should_not be_valid
30
+ end
31
+
32
+ end
33
+
34
+ describe 'to_hexa' do
35
+
36
+ it 'should convert the text string to hexadecimal' do
37
+ device = DeviceFactory.new(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz')
38
+ device.to_hexa.should == fixture_value('hexa.bin')
39
+ end
40
+
41
+ end
42
+
43
+ describe 'before_create' do
44
+
45
+ it 'should set the last_registered_at date to Time.now' do
46
+ time = Time.now
47
+ Time.stub(:now).and_return(time)
48
+ device = DeviceFactory.create
49
+ device.last_registered_at.should_not be_nil
50
+ device.last_registered_at.to_s.should == time.to_s
51
+
52
+ # ago = 1.week.ago
53
+ # device = DeviceFactory.create(:last_registered_at => ago)
54
+ # device.last_registered_at.should_not be_nil
55
+ # device.last_registered_at.to_s.should == ago.to_s
56
+ end
57
+
58
+ end
59
+
60
+ end