gwong-apn_on_rails 0.4.2

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 (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