apn_on_rails 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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