riproad_rapns 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/History.md +5 -0
  2. data/README.md +145 -0
  3. data/bin/rapns +28 -0
  4. data/lib/generators/rapns_generator.rb +33 -0
  5. data/lib/generators/templates/add_alert_is_json_to_rapns_notifications.rb +9 -0
  6. data/lib/generators/templates/add_retry_count_to_rapns_notifications.rb +9 -0
  7. data/lib/generators/templates/add_web_service_notification_request_id_to_rapns_notifications.rb +9 -0
  8. data/lib/generators/templates/create_rapns_feedback.rb +15 -0
  9. data/lib/generators/templates/create_rapns_notifications.rb +26 -0
  10. data/lib/generators/templates/rapns.yml +31 -0
  11. data/lib/rapns.rb +7 -0
  12. data/lib/rapns/binary_notification_validator.rb +10 -0
  13. data/lib/rapns/daemon.rb +111 -0
  14. data/lib/rapns/daemon/certificate.rb +27 -0
  15. data/lib/rapns/daemon/configuration.rb +98 -0
  16. data/lib/rapns/daemon/connection.rb +109 -0
  17. data/lib/rapns/daemon/database_reconnectable.rb +52 -0
  18. data/lib/rapns/daemon/delivery_error.rb +15 -0
  19. data/lib/rapns/daemon/delivery_handler.rb +131 -0
  20. data/lib/rapns/daemon/delivery_handler_pool.rb +20 -0
  21. data/lib/rapns/daemon/delivery_queue.rb +28 -0
  22. data/lib/rapns/daemon/disconnection_error.rb +14 -0
  23. data/lib/rapns/daemon/feedback_receiver.rb +57 -0
  24. data/lib/rapns/daemon/feeder.rb +43 -0
  25. data/lib/rapns/daemon/interruptible_sleep.rb +18 -0
  26. data/lib/rapns/daemon/logger.rb +53 -0
  27. data/lib/rapns/daemon/pool.rb +36 -0
  28. data/lib/rapns/device_token_format_validator.rb +10 -0
  29. data/lib/rapns/feedback.rb +10 -0
  30. data/lib/rapns/notification.rb +76 -0
  31. data/lib/rapns/patches.rb +6 -0
  32. data/lib/rapns/patches/rails/3.1.0/postgresql_adapter.rb +12 -0
  33. data/lib/rapns/patches/rails/3.1.1/postgresql_adapter.rb +17 -0
  34. data/lib/rapns/version.rb +3 -0
  35. data/spec/rapns/daemon/certificate_spec.rb +22 -0
  36. data/spec/rapns/daemon/configuration_spec.rb +231 -0
  37. data/spec/rapns/daemon/connection_spec.rb +293 -0
  38. data/spec/rapns/daemon/database_reconnectable_spec.rb +106 -0
  39. data/spec/rapns/daemon/delivery_error_spec.rb +13 -0
  40. data/spec/rapns/daemon/delivery_handler_pool_spec.rb +21 -0
  41. data/spec/rapns/daemon/delivery_handler_spec.rb +198 -0
  42. data/spec/rapns/daemon/delivery_queue_spec.rb +29 -0
  43. data/spec/rapns/daemon/feedback_receiver_spec.rb +86 -0
  44. data/spec/rapns/daemon/feeder_spec.rb +99 -0
  45. data/spec/rapns/daemon/interruptible_sleep_spec.rb +40 -0
  46. data/spec/rapns/daemon/logger_spec.rb +130 -0
  47. data/spec/rapns/daemon_spec.rb +183 -0
  48. data/spec/rapns/feedback_spec.rb +12 -0
  49. data/spec/rapns/notification_spec.rb +145 -0
  50. data/spec/spec_helper.rb +59 -0
  51. metadata +119 -0
@@ -0,0 +1,5 @@
1
+
2
+ 1.0.7 / 2012-04-22
3
+ ==================
4
+
5
+ * Reconnect if the connnection has been idle for more than 30 minutes. TCP Keepalive alone is not enough to ensure the connection is still connected.
@@ -0,0 +1,145 @@
1
+ # rapns [![Build Status](https://secure.travis-ci.org/ileitch/rapns.png)](http://travis-ci.org/ileitch/rapns)
2
+
3
+ Easy to use library for Apple's Push Notification Service with Rails 3.
4
+
5
+ ## Features
6
+
7
+ * Works with Rails 3 and Ruby 1.9 & 1.8.
8
+ * Uses a daemon process to keep open a persistent connection to the Push Notification Service, as recommended by Apple.
9
+ * Uses the [enhanced binary format](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW4) (Figure 5-2) so that delivery errors can be reported.
10
+ * Records feedback from [The Feedback Service](http://developer.apple.com/library/mac/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3).
11
+ * [Airbrake](http://airbrakeapp.com/) (Hoptoad) integration.
12
+ * Support for [dictionary `alert` properties](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1) (Table 3-2).
13
+ * Reconnects to the APNs if connections are lost.
14
+ * Reconnects to your database if the connect is lost.
15
+
16
+ ## Getting Started
17
+
18
+ Add rapns to your Gemfile:
19
+
20
+ gem 'rapns'
21
+
22
+ Generate the migration, rapns.yml and migrate:
23
+
24
+ rails g rapns
25
+ rake db:migrate
26
+
27
+ ## Generating Certificates
28
+
29
+ 1. Open up Keychain Access and select the `Certificates` category in the sidebar.
30
+ 2. Expand the disclosure arrow next to the iOS Push Services certificate you want to export.
31
+ 3. Select both the certificate and private key.
32
+ 4. Right click and select `Export 2 items...`.
33
+ 5. Save the file as `cert.p12`, make sure the File Format is `Personal Information Exchange (p12)`.
34
+ 6. If you decide to set a password for your exported certificate, please read the Configuration section below.
35
+ 7. Convert the certificate to a .pem, where `<environment>` should be `development` or `production`, depending on the certificate you exported.
36
+
37
+ `openssl pkcs12 -nodes -clcerts -in cert.p12 -out <environment>.pem`
38
+
39
+ 8. Move the .pem file into your Rails application under `config/rapns`.
40
+
41
+ ## Configuration
42
+
43
+ Environment configuration lives in `config/rapns/rapns.yml`. For common setups you probably wont need to change this file.
44
+
45
+ If you want to use rapns in environments other than development or production, you will need to create an entry for it. Simply duplicate the configuration for development or production, depending on which iOS Push Certificate you wish to use.
46
+
47
+ ### Options:
48
+
49
+ * `push` this section contains options to configure the delivery of notifications.
50
+ * `host` the APNs host to connect to, either `gateway.push.apple.com` or `gateway.sandbox.push.apple.com`.
51
+ * `port` the APNs port. Currently `2195` for both hosts.
52
+ * `poll` (default: 2) Frequency in seconds to check for new notifications to deliver.
53
+ * `connections` (default: 3) the number of connections to keep open to the APNs. Consider increasing this if you are sending a very large number of notifications.
54
+
55
+ * `feedback` this section contains options to configure feedback checking.
56
+ * `host` the APNs host to connect to, either `feedback.push.apple.com` or `feedback.sandbox.push.apple.com`.
57
+ * `port` the APNs port. Currently `2196` for both hosts.
58
+ * `poll` (default: 60) Frequency in seconds to check for new feedback.
59
+
60
+ * `certificate` The path to your .pem certificate, `config/rapns` is automatically checked if a relative path is given.
61
+ * `certificate_password` (default: blank) the password you used when exporting your certificate, if any.
62
+ * `airbrake_notify` (default: true) Enables/disables error notifications via Airbrake.
63
+ * `pid_file` (default: blank) the file that rapns will write its process ID to. Paths are relative to your project's RAILS_ROOT unless an absolute path is given.
64
+
65
+ ## Starting the rapns Daemon
66
+
67
+ cd /path/to/rails/app
68
+ bundle exec rapns <Rails environment>
69
+
70
+ ### Options
71
+
72
+ * `--foreground` will prevent rapns from forking into a daemon.
73
+
74
+ ## Logging
75
+
76
+ rapns logs activity to `rapns.log` in your Rails log directory. This is also printed to STDOUT when running in the foreground. When running as a daemon rapns does not print to STDOUT or STDERR.
77
+
78
+ ## Sending a Notification
79
+
80
+ n = Rapns::Notification.new
81
+ n.device_token = "934f7a..."
82
+ n.alert = "This is the message shown on the device."
83
+ n.badge = 1
84
+ n.sound = "1.aiff"
85
+ n.expiry = 1.day.to_i
86
+ n.attributes_for_device = {"question" => nil, "answer" => 42}
87
+ n.deliver_after = 1.hour.from_now
88
+ n.save!
89
+
90
+ * `sound` defaults to `1.aiff`. You can either set it to a custom .aiff file, or `nil` for no sound.
91
+ * `expiry` is the time in seconds the APNs (not rapns) will spend trying to deliver the notification to the device. The notification is discarded if it has not been delivered in this time. Default is 1 day.
92
+ * `attributes_for_device` is the `NSDictionary` argument passed to your iOS app in either `didFinishLaunchingWithOptions` or `didReceiveRemoteNotification`.
93
+ * `deliver_after` is not required, but may be set if you'd like to delay delivery of the notification to a specific time in the future.
94
+
95
+ ### Assigning a Hash to alert
96
+
97
+ Please refer to Apple's [documentation](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1) (Tables 3-1 and 3-2).
98
+
99
+ ## Delivery Failures
100
+
101
+ The APNs provides two mechanism for delivery failure notification:
102
+
103
+ ### Immediately, when processing a notification for delivery.
104
+
105
+ Although rapns makes such errors highly unlikely due to validation, the APNs reports processing errors immediately after being sent a notification. These errors are all centred around the well-formedness of the notification payload. Should a notification be rejected due to such an error, rapns will update the following attributes on the notification and send a notification via Airbrake/Hoptoad (if enabled):
106
+
107
+ `failed` flag is set to true.
108
+ `failed_at` is set to the time of failure.
109
+ `error` is set to Apple's code for the error.
110
+ `error_description` is set to a (somewhat brief) description of the error.
111
+
112
+ rapns will not attempt to deliver the notification again.
113
+
114
+ ### Via the Feedback Service.
115
+
116
+ rapns checks for feedback periodically and stores results in the `Rapns::Feedback` model. Each record contains the device token and a timestamp of when the APNs determined that the app no longer exists on the device.
117
+
118
+ It is your responsibility to avoid creating new notifications for devices that no longer have your app installed. rapns does not and will not check `Rapns::Feedback` before sending notifications.
119
+
120
+ *Note: In my testing and from other reports on the Internet, it appears you may not receive feedback when using the APNs sandbox environment.*
121
+
122
+ ## Updating rapns
123
+
124
+ After updating you should run `rails g rapns` to check for any new migrations or configuration changes.
125
+
126
+ ## Wiki
127
+
128
+ * [Why open multiple connections to the APNs?](https://github.com/ileitch/rapns/wiki/Why-open-multiple-connections-to-the-APNs%3F)
129
+
130
+ ## Contributing to rapns
131
+
132
+ Fork as usual and go crazy!
133
+
134
+ When running specs, please note that the ActiveRecord adapter can be changed by setting the `ADAPTER` environment variable. For example: `ADAPTER=postgresql rake`.
135
+
136
+ Available adapters for testing are `mysql`, `mysql2` and `postgresql`.
137
+
138
+ ### Contributors
139
+
140
+ Thank you to the following wonderful people for contributing to rapns:
141
+
142
+ * [@blakewatters](https://github.com/blakewatters)
143
+ * [@forresty](https://github.com/forresty)
144
+ * [@sjmadsen](https://github.com/sjmadsen)
145
+ * [@ivanyv](https://github.com/ivanyv)
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'rapns'
5
+ require 'rapns/daemon'
6
+
7
+ foreground = false
8
+ environment = ARGV[0]
9
+ banner = 'Usage: rapns <Rails environment> [options]'
10
+ ARGV.options do |opts|
11
+ opts.banner = banner
12
+ opts.on('-f', '--foreground', 'Run in the foreground.') { foreground = true }
13
+ opts.on('-v', '--version', 'Print this version of rapns.') { puts "rapns #{Rapns::VERSION}"; exit }
14
+ opts.on('-h', '--help', 'You\'re looking at it.') { puts opts; exit }
15
+ opts.parse!
16
+ end
17
+
18
+ if environment.nil?
19
+ puts banner
20
+ exit 1
21
+ end
22
+
23
+ ENV['RAILS_ENV'] = environment
24
+ load 'config/environment.rb'
25
+
26
+ require 'rapns/patches'
27
+
28
+ Rapns::Daemon.start(environment, foreground)
@@ -0,0 +1,33 @@
1
+ class RapnsGenerator < Rails::Generators::Base
2
+ include Rails::Generators::Migration
3
+ source_root File.expand_path('../templates', __FILE__)
4
+
5
+ def self.next_migration_number(path)
6
+ @time ||= Time.now.utc
7
+ @calls ||= -1
8
+ @calls += 1
9
+ (@time + @calls.seconds).strftime("%Y%m%d%H%M%S")
10
+ end
11
+
12
+ def copy_migration
13
+ add_rapns_migration('create_rapns_notifications')
14
+ add_rapns_migration('create_rapns_feedback')
15
+ add_rapns_migration('add_alert_is_json_to_rapns_notifications')
16
+ add_rapns_migration('add_retry_count_to_rapns_notifications')
17
+ add_rapns_migration('add_web_service_notification_request_id_to_rapns_notifications')
18
+ end
19
+
20
+ def copy_config
21
+ copy_file 'rapns.yml', 'config/rapns/rapns.yml'
22
+ end
23
+
24
+ protected
25
+
26
+ def add_rapns_migration(template)
27
+ migration_dir = File.expand_path('db/migrate')
28
+
29
+ if !self.class.migration_exists?(migration_dir, template)
30
+ migration_template "#{template}.rb", "db/migrate/#{template}.rb"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ class AddAlertIsJsonToRapnsNotifications < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :rapns_notifications, :alert_is_json, :boolean, :null => true, :default => false
4
+ end
5
+
6
+ def self.down
7
+ remove_column :rapns_notifications, :alert_is_json
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class AddRetryCountToRapnsNotifications < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :rapns_notifications, :retry_count, :integer, :null => true
4
+ end
5
+
6
+ def self.down
7
+ remove_column :rapns_notifications, :retry_count
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class AddWebServiceNotificationRequestIdToRapnsNotifications < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :rapns_notifications, :web_service_notification_request_id, :integer, :null => true
4
+ end
5
+
6
+ def self.down
7
+ remove_column :rapns_notifications, :web_service_notification_request_id
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ class CreateRapnsFeedback < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :rapns_feedback do |t|
4
+ t.string :device_token, :null => false, :limit => 64
5
+ t.timestamp :failed_at, :null => false
6
+ t.timestamps
7
+ end
8
+
9
+ add_index :rapns_feedback, :device_token
10
+ end
11
+
12
+ def self.down
13
+ drop_table :rapns_feedback
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ class CreateRapnsNotifications < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :rapns_notifications do |t|
4
+ t.integer :badge, :null => true
5
+ t.string :device_token, :null => false, :limit => 64
6
+ t.string :sound, :null => true, :default => "1.aiff"
7
+ t.string :alert, :null => true
8
+ t.text :attributes_for_device, :null => true
9
+ t.integer :expiry, :null => false, :default => 1.day.to_i
10
+ t.boolean :delivered, :null => false, :default => false
11
+ t.timestamp :delivered_at, :null => true
12
+ t.boolean :failed, :null => false, :default => false
13
+ t.timestamp :failed_at, :null => true
14
+ t.integer :error_code, :null => true
15
+ t.string :error_description, :null => true
16
+ t.timestamp :deliver_after, :null => true
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :rapns_notifications, [:delivered, :failed, :deliver_after], :name => "index_rapns_notifications_on_delivered_failed_deliver_after"
21
+ end
22
+
23
+ def self.down
24
+ drop_table :rapns_notifications
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ development:
2
+ certificate: development.pem
3
+ certificate_password:
4
+
5
+ push:
6
+ host: gateway.sandbox.push.apple.com
7
+ port: 2195
8
+ poll: 2
9
+ connections: 3
10
+
11
+ feedback:
12
+ host: feedback.sandbox.push.apple.com
13
+ port: 2196
14
+ poll: 60
15
+
16
+ production:
17
+ certificate: production.pem
18
+ certificate_password:
19
+ airbrake_notify: true
20
+ pid_file: tmp/pids/rapns.pid
21
+
22
+ push:
23
+ host: gateway.push.apple.com
24
+ port: 2195
25
+ poll: 2
26
+ connections: 3
27
+
28
+ feedback:
29
+ host: feedback.push.apple.com
30
+ port: 2196
31
+ poll: 60
@@ -0,0 +1,7 @@
1
+ require 'active_record'
2
+
3
+ require 'rapns/version'
4
+ require 'rapns/binary_notification_validator'
5
+ require 'rapns/device_token_format_validator'
6
+ require 'rapns/notification'
7
+ require 'rapns/feedback'
@@ -0,0 +1,10 @@
1
+ module Rapns
2
+ class BinaryNotificationValidator < ActiveModel::Validator
3
+
4
+ def validate(record)
5
+ if record.payload_size > 256
6
+ record.errors[:base] << "APN notification cannot be larger than 256 bytes. Try condensing your alert and device attributes."
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,111 @@
1
+ require 'thread'
2
+ require 'socket'
3
+ require 'pathname'
4
+
5
+ require 'rapns/daemon/interruptible_sleep'
6
+ require 'rapns/daemon/configuration'
7
+ require 'rapns/daemon/certificate'
8
+ require 'rapns/daemon/delivery_error'
9
+ require 'rapns/daemon/disconnection_error'
10
+ require 'rapns/daemon/pool'
11
+ require 'rapns/daemon/connection'
12
+ require 'rapns/daemon/database_reconnectable'
13
+ require 'rapns/daemon/delivery_queue'
14
+ require 'rapns/daemon/delivery_handler'
15
+ require 'rapns/daemon/delivery_handler_pool'
16
+ require 'rapns/daemon/feedback_receiver'
17
+ require 'rapns/daemon/feeder'
18
+ require 'rapns/daemon/logger'
19
+
20
+ module Rapns
21
+ module Daemon
22
+ class << self
23
+ attr_accessor :logger, :configuration, :certificate,
24
+ :delivery_queue, :delivery_handler_pool, :foreground
25
+ alias_method :foreground?, :foreground
26
+ end
27
+
28
+ def self.start(environment, foreground)
29
+ @foreground = foreground
30
+ setup_signal_hooks
31
+
32
+ self.configuration = Configuration.new(environment, File.join(Rails.root, 'config', 'rapns', 'rapns.yml'))
33
+ configuration.load
34
+
35
+ self.logger = Logger.new(:foreground => foreground, :airbrake_notify => configuration.airbrake_notify)
36
+
37
+ self.certificate = Certificate.new(configuration.certificate)
38
+ certificate.load
39
+
40
+ self.delivery_queue = DeliveryQueue.new
41
+
42
+ daemonize unless foreground?
43
+
44
+ write_pid_file
45
+
46
+ self.delivery_handler_pool = DeliveryHandlerPool.new(configuration.push.connections)
47
+ delivery_handler_pool.populate
48
+
49
+ logger.info('Ready')
50
+
51
+ FeedbackReceiver.start
52
+ Feeder.start(foreground?)
53
+ end
54
+
55
+ protected
56
+
57
+ def self.setup_signal_hooks
58
+ @shutting_down = false
59
+
60
+ ['SIGINT', 'SIGTERM'].each do |signal|
61
+ Signal.trap(signal) do
62
+ handle_shutdown_signal
63
+ end
64
+ end
65
+ end
66
+
67
+ def self.handle_shutdown_signal
68
+ exit 1 if @shutting_down
69
+ @shutting_down = true
70
+ shutdown
71
+ end
72
+
73
+ def self.shutdown
74
+ puts "\nShutting down..."
75
+ Rapns::Daemon::FeedbackReceiver.stop
76
+ Rapns::Daemon::Feeder.stop
77
+ Rapns::Daemon.delivery_handler_pool.drain if Rapns::Daemon.delivery_handler_pool
78
+ delete_pid_file
79
+ end
80
+
81
+ def self.daemonize
82
+ exit if pid = fork
83
+ Process.setsid
84
+ exit if pid = fork
85
+
86
+ Dir.chdir '/'
87
+ File.umask 0000
88
+
89
+ STDIN.reopen '/dev/null'
90
+ STDOUT.reopen '/dev/null', 'a'
91
+ STDERR.reopen STDOUT
92
+ end
93
+
94
+ def self.write_pid_file
95
+ if !configuration.pid_file.blank?
96
+ begin
97
+ File.open(configuration.pid_file, 'w') do |f|
98
+ f.puts $$
99
+ end
100
+ rescue SystemCallError => e
101
+ logger.error("Failed to write PID to '#{configuration.pid_file}': #{e.inspect}")
102
+ end
103
+ end
104
+ end
105
+
106
+ def self.delete_pid_file
107
+ pid_file = configuration.pid_file
108
+ File.delete(pid_file) if !pid_file.blank? && File.exists?(pid_file)
109
+ end
110
+ end
111
+ end