urbanairship_on_rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/Gemfile +4 -0
  2. data/Gemfile.lock +12 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README +188 -0
  5. data/Rakefile +26 -0
  6. data/generators/apn_migrations/apn_migrations_generator.rb +31 -0
  7. data/generators/apn_migrations/templates/apn_migrations/001_create_apn_devices.rb +15 -0
  8. data/generators/apn_migrations/templates/apn_migrations/002_create_apn_notifications.rb +23 -0
  9. data/generators/apn_migrations/templates/apn_migrations/003_create_apn_feedbacks.rb +17 -0
  10. data/generators/apn_migrations/templates/apn_migrations/004_create_apn_broadcast_notifications.rb +20 -0
  11. data/generators/apn_migrations/templates/apn_migrations/005_change_badge_to_string_on_notifications.rb +11 -0
  12. data/generators/apn_migrations/templates/apn_migrations/006_rename_errors_nb_to_last_error_code_on_notifications.rb +11 -0
  13. data/generators/apn_migrations/templates/apn_migrations/007_rename_errors_nb_to_last_error_code_on_broadcast_notifications.rb +11 -0
  14. data/generators/apn_migrations/templates/apn_migrations/008_add_custom_properties_on_notifications.rb +9 -0
  15. data/generators/apn_migrations/templates/apn_migrations/009_add_custom_properties_on_broadcast_notifications.rb +9 -0
  16. data/generators/apn_migrations/templates/apn_migrations/010_create_apn_excluded_devices_for_notifications.rb +17 -0
  17. data/generators/apn_migrations/templates/apn_migrations/011_add_response_code_message_and_body_to_devices.rb +15 -0
  18. data/generators/apn_migrations/templates/apn_migrations/012_add_last_inactive_at_to_devices.rb +9 -0
  19. data/init.rb +2 -0
  20. data/install.rb +1 -0
  21. data/lib/urbanairship_on_rails.rb +4 -0
  22. data/lib/urbanairship_on_rails/libs/config.rb +29 -0
  23. data/lib/urbanairship_on_rails/models/apn/base.rb +124 -0
  24. data/lib/urbanairship_on_rails/models/apn/broadcast_notification.rb +95 -0
  25. data/lib/urbanairship_on_rails/models/apn/device.rb +129 -0
  26. data/lib/urbanairship_on_rails/models/apn/excluded_devices_for_notification.rb +21 -0
  27. data/lib/urbanairship_on_rails/models/apn/feedback.rb +74 -0
  28. data/lib/urbanairship_on_rails/models/apn/notification.rb +101 -0
  29. data/lib/urbanairship_on_rails/urbanairship_on_rails.rb +21 -0
  30. data/lib/urbanairship_on_rails/version.rb +3 -0
  31. data/tasks/urban_airship_on_rails_tasks.rake +22 -0
  32. data/test/test_helper.rb +3 -0
  33. data/test/urban_airship_on_rails_test.rb +8 -0
  34. data/uninstall.rb +1 -0
  35. data/urbanairship_on_rails.gemspec +21 -0
  36. metadata +92 -0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'aasm'
4
+ gem 'json'
@@ -0,0 +1,12 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ aasm (2.2.0)
5
+ json (1.5.1)
6
+
7
+ PLATFORMS
8
+ ruby
9
+
10
+ DEPENDENCIES
11
+ aasm
12
+ json
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,188 @@
1
+ ===================
2
+ URBAN AIRSHIP ON RAILS
3
+ ===================
4
+
5
+ Borrowed Liberally from APN on Rails Gem but designed to interface with urbanairship for simplicity, ease of use, and speedy deployment of push notifications
6
+ uses identical data schema to APN.. perhaps allowing future upgrade to APN
7
+
8
+ =======
9
+ DEPENDENCIES
10
+ =======
11
+ requires JSON gem
12
+ AASM
13
+
14
+ Note: If you use the aasm gem, you may need to install the gem before you 'config.gem "aasm"' in your environment.rb or include it in your Gemfile.
15
+
16
+ =======
17
+ HOW TO INSTALL
18
+ =======
19
+ ruby script/plugin install git://github.com/yonnage/urbanairship_on_rails
20
+
21
+ =======
22
+ HOW TO GENERATE MIGRATIONS
23
+ =======
24
+ ruby script/generate apn_migrations
25
+ rake db:migrate
26
+
27
+ =======
28
+ HOW TO CONFIGURE
29
+ =======
30
+
31
+ =====
32
+ Create an initializer named urbanairship.rb with the following content
33
+ require File.dirname(__FILE__) + '/../../vendor/plugins/urbanairship_on_rails/lib/urbanairship_on_rails/libs/config'
34
+ UA::Config::app_key = 'YOUR_APP_KEY'
35
+ UA::Config::app_secret = 'YOUR_APP_SECRET'
36
+ UA::Config::push_secret = 'YOUR_PUSH_SECRET' ### CURRENTLY KNOWN AS "Application Master Secret" on Urban Airship
37
+ UA::Config::push_host = 'go.urbanairship.com'
38
+ UA::Config::push_port = '443'
39
+
40
+ =====
41
+ Set up a daily cron job to set inactive devices to "inactive"
42
+ rake apn:feedback or
43
+ script/runner APN:Feedback.create().run
44
+
45
+ Set up a cron job to push and manage notifications
46
+ rake apn:push or
47
+ script/runner APN::Notification.process_pending
48
+
49
+ Set up a cron job to push and manage broadcast notifications
50
+ rake apn:broadcast_push or
51
+ script/runner APN::BroadcastNotification.process_pending
52
+
53
+
54
+ === Examples of Use
55
+
56
+ == Create and register a device:
57
+
58
+ # Create device:
59
+ ## Requires a unique token. Token format should be: 5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz
60
+
61
+ device = APN::Device.find_or_create_by_token(:token => token)
62
+
63
+
64
+ #Register the device with Urban Airship
65
+
66
+ device.register
67
+
68
+ # Your device is now registered and ready to receive notifications.
69
+
70
+
71
+ # Your mobile application should try to register itself each time the application is launched, which is why it's important
72
+ # to do "find_or_create_by_token"
73
+
74
+
75
+ == Create and send a single simple Notification:
76
+
77
+ # Basic Options for Notifications:
78
+ # alert - The message the device will receive.
79
+ # badge - The number of badges (red circle with number) on the device.
80
+ # sound - This is the sound heard on the device. "default" is the standard device notification sound.
81
+ # device - The device to send the notification to.
82
+
83
+ n = APN::Notification.create(:alert => "Hello, World", :badge => 1, :sound => "default", :device => device)
84
+
85
+ # Now send the notification to the device:
86
+
87
+ n.push
88
+
89
+
90
+ == Create and send a single Notification with more options:
91
+
92
+ # Adding more options to a notification.
93
+ # sound - You can also use specific sounds for the alert which are already included as part of the mobile app.
94
+ # badge - Urban Airship supports auto increment / decrement of badges. Pass "+1" or "-1".
95
+ # custom_properties - Extra information you need to pass to the device.
96
+
97
+ n = APN::Notification.create(:alert => "Hello, World", :badge => "+1", :device => device, :sound => "alert1.wav", :custom_properties => {:name => "Bobby", :person_id => 7})
98
+
99
+
100
+ # Now send the notification to the device
101
+
102
+ n.push
103
+
104
+
105
+ == Send all pending notifications:
106
+
107
+ # Instead of sending each notification as it is created, you can send them all at once.
108
+
109
+ APN::Notification.process_pending
110
+
111
+
112
+ # Or setup a cron rake task:
113
+
114
+ rake apn:push
115
+
116
+
117
+ == Broadcast Notifications
118
+
119
+ # Send the same notification to all devices.
120
+ # Use this to avoid making multiple connections if you are sending the same message to all devices.
121
+
122
+ APN::BroadcastNotification.create(:alert => "Hello, World", :badge => 1, :sound => "default")
123
+
124
+
125
+ # Push the BroadcastNotification
126
+
127
+ APN::BroadcastNotification.process_pending
128
+
129
+
130
+ == Broadcast Notifications with Device exclusions.
131
+
132
+ # You can exclude devices from receiving a broadcast notification.
133
+
134
+ n = APN::BroadcastNotification.create(:alert => "Hello, World", :badge => 1, :sound => "default")
135
+
136
+
137
+ # Exclude any devices that should not receive the above notification.
138
+
139
+ n.excluded_devices.create(:broadcast_notification => n, :device => APN::Device.find(1))
140
+ n.excluded_devices.create(:broadcast_notification => n, :device => APN::Device.find(2))
141
+ n.excluded_devices.create(:broadcast_notification => n, :device => APN::Device.find(3))
142
+
143
+
144
+ # Push the Broadcast Notification:
145
+
146
+ n.push
147
+
148
+ # -- OR -- Push all pending Broadcast Notifications:
149
+
150
+ APN::BroadcastNotification.process_pending
151
+
152
+
153
+ == Feedback for Devices
154
+
155
+ # When a device no longer accepts notifications from your application because of reasons such as the application being removed,
156
+ # Urban Airship will stop trying to send notifications to those devices. You should run the "feedback" task daily to find and
157
+ # mark all inactive devices as such.
158
+
159
+ rake apn:feedback
160
+
161
+
162
+ =======
163
+ API CALLS and current support level
164
+ =======
165
+
166
+ REGISTRATION
167
+
168
+ APN::Device.register => HTTP PUT to /api/device_tokens/<device_token>
169
+ APN::Device.read => HTTP GET to /api/device_tokens/<device_token>
170
+ APN::Device.unregister => HTTP DELETE to /api/device_tokens/<device_token>
171
+
172
+ PUSH
173
+ APN::Notifcation.push => HTTP POST to /api/push/
174
+
175
+ BATCH PUSH
176
+ NOT YET SUPPORTED
177
+
178
+ BROADCAST
179
+ APN::BroadcastNotifcation.push => HTTP POST to /api/push/broadcast/
180
+
181
+ FEEDBACK SERVICE
182
+ APN::Feedback.create().run => HTTP GET to /api/device_tokens/feedback/?since=<timestamp>
183
+
184
+ STATISTICS
185
+ NOT YET SUPPORTED
186
+
187
+
188
+ Copyright (c) 2009 [Russell Sherman(code-ronin@code-ronin.com)], released under the MIT license
@@ -0,0 +1,26 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ require 'bundler'
6
+ Bundler::GemHelper.install_tasks
7
+
8
+ desc 'Default: run unit tests.'
9
+ task :default => :test
10
+
11
+ desc 'Test the urban_airship_on_rails plugin.'
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'lib'
14
+ t.libs << 'test'
15
+ t.pattern = 'test/**/*_test.rb'
16
+ t.verbose = true
17
+ end
18
+
19
+ desc 'Generate documentation for the urban_airship_on_rails plugin.'
20
+ Rake::RDocTask.new(:rdoc) do |rdoc|
21
+ rdoc.rdoc_dir = 'rdoc'
22
+ rdoc.title = 'UrbanAirshipOnRails'
23
+ rdoc.options << '--line-numbers' << '--inline-source'
24
+ rdoc.rdoc_files.include('README')
25
+ rdoc.rdoc_files.include('lib/**/*.rb')
26
+ end
@@ -0,0 +1,31 @@
1
+ require 'rails_generator'
2
+ # Generates the migrations necessary for APN on Rails.
3
+ # This should be run upon install and upgrade of the
4
+ # APN on Rails gem.
5
+ #
6
+ # $ ruby script/generate apn_migrations
7
+ class ApnMigrationsGenerator < Rails::Generator::Base
8
+
9
+ def manifest # :nodoc:
10
+ record do |m|
11
+ timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
12
+ db_migrate_path = File.join('db', 'migrate')
13
+
14
+ m.directory(db_migrate_path)
15
+
16
+ Dir.glob(File.join(File.dirname(__FILE__), 'templates', 'apn_migrations', '*.rb')).sort.each_with_index do |f, i|
17
+ f = File.basename(f)
18
+ f.match(/\d+\_(.+)/)
19
+ timestamp = timestamp.succ
20
+ if Dir.glob(File.join(db_migrate_path, "*_#{$1}")).empty?
21
+ m.file(File.join('apn_migrations', f),
22
+ File.join(db_migrate_path, "#{timestamp}_#{$1}"),
23
+ {:collision => :skip})
24
+ end
25
+ end
26
+
27
+ end # record
28
+
29
+ end # manifest
30
+
31
+ end # ApnMigrationsGenerator
@@ -0,0 +1,15 @@
1
+ class CreateApnDevices < ActiveRecord::Migration # :nodoc:
2
+ def self.up
3
+ create_table :apn_devices do |t|
4
+ t.string :token, :size => 100, :null => false
5
+ t.datetime :last_registered_at
6
+ t.timestamps
7
+ end
8
+ add_index :apn_devices, :token, :unique => true
9
+ end
10
+
11
+ def self.down
12
+ remove_index :apn_devices, :column => :token
13
+ drop_table :apn_devices
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ class CreateApnNotifications < ActiveRecord::Migration # :nodoc:
2
+
3
+ def self.up
4
+ create_table :apn_notifications do |t|
5
+ t.integer :device_id, :null => false
6
+ t.integer :errors_nb, :default => 0 # used for storing errors from apple feedbacks
7
+ t.string :device_language, :size => 5 # if you don't want to send localized strings
8
+ t.string :sound
9
+ t.string :alert, :size => 150
10
+ t.integer :badge
11
+ t.datetime :sent_at
12
+ t.string :state
13
+ t.timestamps
14
+ end
15
+ add_index :apn_notifications, :device_id
16
+ end
17
+
18
+ def self.down
19
+ remove_index :apn_notifications, :device_id
20
+ drop_table :apn_notifications
21
+ end
22
+
23
+ end
@@ -0,0 +1,17 @@
1
+ class CreateApnFeedbacks < ActiveRecord::Migration # :nodoc:
2
+
3
+ def self.up
4
+ create_table :apn_feedbacks do |t|
5
+ t.string :code
6
+ t.string :message
7
+ t.text :body
8
+ t.string :state
9
+ t.timestamps
10
+ end
11
+ end
12
+
13
+ def self.down
14
+ drop_table :apn_feedbacks
15
+ end
16
+
17
+ end
@@ -0,0 +1,20 @@
1
+ class CreateApnBroadcastNotifications < ActiveRecord::Migration # :nodoc:
2
+
3
+ def self.up
4
+ create_table :apn_broadcast_notifications do |t|
5
+ t.integer :errors_nb, :default => 0 # used for storing errors from apple feedbacks
6
+ t.string :device_language, :size => 5 # if you don't want to send localized strings
7
+ t.string :sound
8
+ t.string :alert, :size => 150
9
+ t.string :badge
10
+ t.datetime :sent_at
11
+ t.string :state
12
+ t.timestamps
13
+ end
14
+ end
15
+
16
+ def self.down
17
+ drop_table :apn_broadcast_notifications
18
+ end
19
+
20
+ end
@@ -0,0 +1,11 @@
1
+ class ChangeBadgeToStringOnNotifications < ActiveRecord::Migration # :nodoc:
2
+
3
+ def self.up
4
+ change_column :apn_notifications, :badge, :string
5
+ end
6
+
7
+ def self.down
8
+ change_column :apn_notifications, :badge, :integer
9
+ end
10
+
11
+ end
@@ -0,0 +1,11 @@
1
+ class RenameErrorsNbToLastErrorCodeOnNotifications < ActiveRecord::Migration # :nodoc:
2
+
3
+ def self.up
4
+ rename_column :apn_notifications, :errors_nb, :last_response_code
5
+ end
6
+
7
+ def self.down
8
+ rename_column :apn_notifications, :last_response_code, :errors_nb
9
+ end
10
+
11
+ end
@@ -0,0 +1,11 @@
1
+ class RenameErrorsNbToLastErrorCodeOnBroadcastNotifications < ActiveRecord::Migration # :nodoc:
2
+
3
+ def self.up
4
+ rename_column :apn_broadcast_notifications, :errors_nb, :last_response_code
5
+ end
6
+
7
+ def self.down
8
+ rename_column :apn_broadcast_notifications, :last_response_code, :errors_nb
9
+ end
10
+
11
+ end
@@ -0,0 +1,9 @@
1
+ class AddCustomPropertiesOnNotifications < ActiveRecord::Migration # :nodoc:
2
+ def self.up
3
+ add_column :apn_notifications, :custom_properties, :text, :after => :badge
4
+ end
5
+
6
+ def self.down
7
+ remove_column :apn_notifications, :custom_properties
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class AddCustomPropertiesOnBroadcastNotifications < ActiveRecord::Migration # :nodoc:
2
+ def self.up
3
+ add_column :apn_broadcast_notifications, :custom_properties, :text, :after => :badge
4
+ end
5
+
6
+ def self.down
7
+ remove_column :apn_broadcast_notifications, :custom_properties
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ class CreateApnExcludedDevicesForNotifications < ActiveRecord::Migration # :nodoc:
2
+ def self.up
3
+ create_table :apn_excluded_devices_for_notifications do |t|
4
+ t.integer :device_id
5
+ t.integer :notification_id
6
+ t.integer :broadcast_notification_id
7
+ t.timestamps
8
+ end
9
+ add_index(:apn_excluded_devices_for_notifications, [:device_id, :notification_id], :unique => true, :name => 'by_device_notfication')
10
+ add_index(:apn_excluded_devices_for_notifications, [:device_id, :broadcast_notification_id], :unique => true, :name => 'by_device_broadcast_notfication')
11
+ end
12
+
13
+ def self.down
14
+ remove_index :apn_excluded_devices_for_notifications, :device_id
15
+ drop_table :apn_excluded_devices_for_notifications
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ class AddLastInactiveAtToDevices < ActiveRecord::Migration # :nodoc:
2
+ def self.up
3
+ add_column :apn_devices, :state, :string, :default => 'created', :after => :token
4
+ add_column :apn_devices, :response_code, :string, :after => :state
5
+ add_column :apn_devices, :response_message, :string, :after => :response_code
6
+ add_column :apn_devices, :response_body, :text, :after => :response_message
7
+ end
8
+
9
+ def self.down
10
+ remove_column :apn_devices, :state
11
+ remove_column :apn_devices, :response_code
12
+ remove_column :apn_devices, :response_message
13
+ remove_column :apn_devices, :response_body
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ class AddLastInactiveAtToDevices < ActiveRecord::Migration # :nodoc:
2
+ def self.up
3
+ add_column :apn_devices, :last_inactive_at, :datetime, :after => :last_registered_at
4
+ end
5
+
6
+ def self.down
7
+ remove_column :apn_devices, :last_inactive_at
8
+ end
9
+ end
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ # Include hook code here
2
+ require File.dirname(__FILE__) + '/lib/urbanairship_on_rails'
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,4 @@
1
+ # UrbanAirshipOnRails
2
+ Dir.glob(File.join(File.dirname(__FILE__), 'urbanairship_on_rails', '**/*.rb')).sort.each do |f|
3
+ require File.expand_path(f)
4
+ end
@@ -0,0 +1,29 @@
1
+ module UA
2
+
3
+ module Config
4
+
5
+ @@app_key = 'YOUR_APP_KEY'
6
+ @@app_secret = 'YOUR_APP_SECRET'
7
+ @@push_secret = 'YOUR_PUSH_SECRET'
8
+ @@push_host = 'go.urbanairship.com'
9
+ @@push_port = 443
10
+
11
+ [:app_key, :app_secret, :push_secret, :push_host, :push_port].each do |sym|
12
+ class_eval <<-EOS, __FILE__, __LINE__
13
+ def self.#{sym}
14
+ if defined?(#{sym.to_s.upcase})
15
+ #{sym.to_s.upcase}
16
+ else
17
+ @@#{sym}
18
+ end
19
+ end
20
+ def self.#{sym}=(obj)
21
+ @@#{sym} = obj
22
+ end
23
+ EOS
24
+
25
+ end
26
+
27
+ end # Config
28
+
29
+ end # UA
@@ -0,0 +1,124 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+
4
+ module APN
5
+
6
+ class ConnectionError < StandardError
7
+ end
8
+
9
+ class RetriableConnectionError < ConnectionError
10
+ end
11
+
12
+ class Base < ActiveRecord::Base # :nodoc:
13
+
14
+ MAX_RETRIES = 3
15
+ OPEN_TIMEOUT = 60
16
+ READ_TIMEOUT = 60
17
+
18
+ def self.table_name # :nodoc:
19
+ self.to_s.gsub("::", "_").tableize
20
+ end
21
+
22
+ def http_get(url, data=nil, headers={}, push=false)
23
+ puts "APN::Base.http_get"
24
+ http_request(:get, url, data, headers, push)
25
+ end
26
+
27
+ def http_post(url, data=nil, headers={}, push=false)
28
+ puts "APN::Base.http_post"
29
+ http_request(:post, url, data, headers, push)
30
+ end
31
+
32
+ def http_put(url, data=nil, headers={}, push=false)
33
+ puts "APN::Base.http_put"
34
+ http_request(:put, url, data, headers, push)
35
+ end
36
+
37
+ def http_delete(url, data=nil, headers={}, push=false)
38
+ puts "APN::Base.http_delete"
39
+ http_request(:delete, url, data, headers, push)
40
+ end
41
+
42
+ private
43
+
44
+ def self.open_timeout
45
+ OPEN_TIMEOUT
46
+ end
47
+
48
+ def self.read_timeout
49
+ READ_TIMEOUT
50
+ end
51
+
52
+ def retry_exceptions
53
+ retries = MAX_RETRIES
54
+ begin
55
+ yield
56
+ rescue RetriableConnectionError => e
57
+ puts e
58
+ retries -= 1
59
+ retry unless retries.zero?
60
+ raise ConnectionError, e.message
61
+ rescue ConnectionError
62
+ puts e
63
+ retries -= 1
64
+ retry if retry_safe && !retries.zero?
65
+ raise
66
+ end
67
+ end
68
+
69
+ def http_request(method, url, data=nil, headers={}, push=false)
70
+ headers['Content-Type'] = 'application/json'
71
+
72
+ puts "APN::Base.http_request(#{method.inspect}, #{url.inspect}, #{data.inspect}, #{push.inspect})"
73
+
74
+ case method
75
+ when :get
76
+ req = Net::HTTP::Get.new(url, headers)
77
+ when :put
78
+ req = Net::HTTP::Put.new(url, headers)
79
+ when :post
80
+ req = Net::HTTP::Post.new(url, headers)
81
+ when :delete
82
+ req = Net::HTTP::Delete.new(url, headers)
83
+ end
84
+
85
+ if push
86
+ req.basic_auth(UA::Config::app_key, UA::Config::push_secret)
87
+ else
88
+ req.basic_auth(UA::Config::app_key, UA::Config::app_secret)
89
+ end
90
+
91
+ puts "auth #{UA::Config::app_key}:#{UA::Config::app_secret}"
92
+
93
+ if data
94
+ req.body = data.to_json
95
+ else
96
+ req.body = "{}"
97
+ end
98
+ puts "body #{req.body}"
99
+
100
+ puts "#{UA::Config::push_host}:#{UA::Config::push_port}"
101
+ http = Net::HTTP.new(UA::Config::push_host, UA::Config::push_port)
102
+ http.use_ssl = true
103
+
104
+ http.start {|it|
105
+ retry_exceptions do
106
+ begin
107
+ response = it.request(req)
108
+ puts "\nResponse #{response.code} #{response.message}:#{response.body}"
109
+ response
110
+ rescue EOFError => e
111
+ raise ConnectionError, "The remote server dropped the connection"
112
+ rescue Errno::ECONNRESET => e
113
+ raise ConnectionError, "The remote server reset the connection"
114
+ rescue Errno::ECONNREFUSED => e
115
+ raise RetriableConnectionError, "The remote server refused the connection"
116
+ rescue Timeout::Error, Errno::ETIMEDOUT => e
117
+ raise ConnectionError, "The connection to the remote server timed out"
118
+ end
119
+ end
120
+ }
121
+ end
122
+
123
+ end
124
+ end
@@ -0,0 +1,95 @@
1
+ # Represents the message you wish to send.
2
+ # An APN::Notification belongs to an APN::Device.
3
+ #
4
+ # Example:
5
+ # apn = APN::BroadcastNotification.new
6
+ # apn.badge = 5
7
+ # apn.sound = 'my_sound.aiff'
8
+ # apn.alert = 'Hello!'
9
+ # apn.custom_properties = {:email_id => "6", :link => "http://google.com"}
10
+ # apn.save
11
+ #
12
+ # To deliver call the following method:
13
+ # APN::BroadcastNotification.process_pending
14
+ #
15
+ # As each APN::BroadcastNotification is sent the <tt>sent_at</tt> column will be timestamped,
16
+ # so as to not be sent again.
17
+ class APN::BroadcastNotification < APN::Base
18
+ include AASM
19
+ include ::ActionView::Helpers::TextHelper
20
+ extend ::ActionView::Helpers::TextHelper
21
+
22
+ has_many :excluded_devices, :class_name => 'APN::ExcludedDevicesForNotification'
23
+
24
+ serialize :custom_properties
25
+
26
+ #
27
+ # MODEL STATE MACHINE
28
+ #
29
+ aasm_initial_state :pending
30
+ aasm_column :state
31
+
32
+ aasm_state :pending
33
+ aasm_state :processed, :enter => :update_sent_at
34
+
35
+ aasm_event :process do
36
+ transitions :from => :pending, :to => :processed, :guard => :check_response
37
+ end
38
+
39
+ def apple_hash
40
+ result = {}
41
+ result['aps'] = {}
42
+ result['aps']['alert'] = self.alert if self.alert
43
+ result['aps']['badge'] = convert(self.badge) if self.badge
44
+ if self.sound
45
+ result['aps']['sound'] = self.sound if self.sound
46
+ end
47
+ if self.custom_properties
48
+ self.custom_properties.each do |key,value|
49
+ result["#{key}"] = "#{value}"
50
+ end
51
+ end
52
+
53
+ result['exclude_tokens'] = []
54
+ self.excluded_devices.each do |excluded|
55
+ result['exclude_tokens'] << excluded.device.token_for_ua
56
+ end
57
+
58
+ result
59
+ end
60
+
61
+ def push
62
+ puts "process #{self.inspect}"
63
+ result = http_post("/api/push/broadcast/", apple_hash, {}, true)
64
+ self.last_response_code = result.code
65
+ self.save
66
+ self.process!
67
+ end
68
+
69
+ def update_sent_at
70
+ self.sent_at = Time.now
71
+ end
72
+
73
+ def self.process_pending
74
+ self.pending.each do |n|
75
+ n.push
76
+ end
77
+ end
78
+
79
+ def check_response
80
+ self.last_response_code == 200 ? true : false
81
+ end
82
+
83
+ def convert(input)
84
+ if input.to_i > 0 && input.to_i.to_s == input
85
+ return input.to_i
86
+ else
87
+ return input
88
+ end
89
+ end
90
+
91
+ def check_response
92
+ self.last_response_code == 200 ? true : false
93
+ end
94
+
95
+ end # APN::Notification
@@ -0,0 +1,129 @@
1
+ # Represents an iPhone (or other APN enabled device).
2
+ # An APN::Device can have many APN::Notification.
3
+ #
4
+ # In order for the APN::Feedback system to work properly you *MUST*
5
+ # touch the <tt>last_registered_at</tt> column everytime someone opens
6
+ # your application. If you do not, then it is possible, and probably likely,
7
+ # that their device will be removed and will no longer receive notifications.
8
+ #
9
+ # Example:
10
+ # Device.create(:token => '5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz')
11
+ class APN::Device < APN::Base
12
+ include AASM
13
+
14
+ before_destroy :unregister, :destroy_undelivered_notifications, :destroy_undelivered_broadcast_exclusions
15
+ # before_save :set_last_registered_at
16
+
17
+ belongs_to :user, :dependent => :delete
18
+ has_many :notifications, :class_name => 'APN::Notification'
19
+ has_many :exclusions_from_notifications, :class_name => 'APN::ExcludedDevicesForNotification'
20
+
21
+ validates_presence_of :token
22
+ validates_uniqueness_of :token
23
+ 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}$/
24
+
25
+ # The <tt>feedback_at</tt> accessor is set when the
26
+ # device is marked as potentially disconnected from your
27
+ # application by Apple.
28
+ attr_accessor :feedback_at
29
+
30
+ aasm_initial_state :created
31
+ aasm_column :state
32
+
33
+ aasm_state :created
34
+ aasm_state :activated, :enter => :set_last_registered_at
35
+ aasm_state :inactive, :enter => :set_last_inactive_at
36
+
37
+ aasm_event :activate do
38
+ transitions :from => [:created, :inactive, :activated], :to => :activated, :guard => :check_response
39
+ end
40
+
41
+ aasm_event :deactivate do
42
+ transitions :from => [:created, :inactive, :activated], :to => :inactive
43
+ end
44
+
45
+ # Stores the token (Apple's device ID) of the iPhone (device).
46
+ #
47
+ # If the token comes in like this:
48
+ # '<5gxadhy6 6zmtxfl6 5zpbcxmw ez3w7ksf qscpr55t trknkzap 7yyt45sc g6jrw7qz>'
49
+ # Then the '<' and '>' will be stripped off.
50
+
51
+ def token=(token)
52
+ res = token.scan(/\<(.+)\>/).first
53
+ unless res.nil? || res.empty?
54
+ token = res.first
55
+ end
56
+ write_attribute('token', token)
57
+ end
58
+
59
+ def register(options=nil)
60
+ puts "APN::Device.register"
61
+ # options = options.merge({:alias => self.user.id}) if self.user
62
+ result = http_put("/api/device_tokens/#{self.token_for_ua}", options)
63
+ self.response_code = result.code.to_s
64
+ self.response_message = result.message.to_s
65
+ self.response_body = result.body.to_s
66
+ self.save
67
+ self.activate!
68
+ end
69
+
70
+ # You can read a device token’s alias with an HTTP GET to /api/device_tokens/<device_token>, which returns application/json:
71
+ # {"device_token": "some device token","alias": "your_user_id"}
72
+ def read
73
+ puts "APN::Device.read"
74
+ http_get("/api/device_tokens/#{self.token_for_ua}")
75
+ end
76
+
77
+ # The DELETE returns HTTP 204 No Content, and needs no payload.
78
+ # When a token is DELETEd in this manner, any alias or tags will be cleared.
79
+
80
+ def token_for_ua
81
+ self.token.gsub(' ', '').upcase
82
+ end
83
+
84
+ def self.find_by_ua_token(ua_token)
85
+ find_by_token(ua_token.downcase.scan(/.{8}/).join(" "))
86
+ end
87
+
88
+ private
89
+
90
+ def unregister
91
+ puts "APN::Device.unregister"
92
+ http_delete("/api/device_tokens/#{self.token_for_ua}")
93
+ end
94
+
95
+ def destroy_undelivered_notifications
96
+ self.notifications.pending.each do |notification|
97
+ notification.excluded_devices.first.destroy unless notification.excluded_devices.first.nil?
98
+ notification.destroy
99
+ end
100
+ end
101
+
102
+ def destroy_undelivered_broadcast_exclusions
103
+ self.exclusions_from_notifications.broadcasts.each do |exclusion|
104
+ unless exclusion.broadcast_notification.nil?
105
+ exclusion.destroy unless exclusion.broadcast_notification.processed?
106
+ end
107
+ end
108
+ end
109
+
110
+ def set_last_registered_at
111
+ self.last_registered_at = Time.now
112
+ end
113
+
114
+ def set_last_inactive_at
115
+ self.last_inactive_at = Time.now
116
+ self.notifications.pending.each do |n|
117
+ n.set_to_inactive!
118
+ end
119
+ end
120
+
121
+ def check_response
122
+ if self.response_code == "200" || self.response_code == "201"
123
+ return true
124
+ else
125
+ self.deactivate!
126
+ return false
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,21 @@
1
+ class APN::ExcludedDevicesForNotification < APN::Base
2
+
3
+ belongs_to :broadcast_notification, :class_name => 'APN::BroadcastNotification'
4
+ belongs_to :notification, :class_name => 'APN::Notification'
5
+ belongs_to :device, :class_name => 'APN::Device'
6
+
7
+ validates_presence_of :device, :on => :create, :message => "is required"
8
+ validates_presence_of :notification, :on => :create, :message => "or BroadcastNotification is required", :unless => :has_broadcast_notification?
9
+
10
+ validates_uniqueness_of :notification_id, :scope => :device_id, :allow_nil => true
11
+ validates_uniqueness_of :broadcast_notification_id, :scope => :device_id, :allow_nil => true
12
+
13
+ named_scope :broadcasts, lambda {
14
+ { :conditions => "broadcast_notification_id IS NOT NULL" }
15
+ }
16
+
17
+
18
+ def has_broadcast_notification?
19
+ broadcast_notification.nil? ? false : true
20
+ end
21
+ end
@@ -0,0 +1,74 @@
1
+ require 'json'
2
+ #
3
+ # Apple notifies UA when a notification is sent to a device that has push notifications enabled (for at least one application)
4
+ # but has turned off or uninstalled your application. We immediate stop sending notifications through to avoid angering Apple.
5
+ #
6
+ # To query what device tokens are now invalid, do an HTTP GET to /api/device_tokens/feedback/?since=<timestamp> with the push secret;
7
+ # the query argument since is required, and is a timestamp in ISO 8601 format, e.g. /api/device_tokens/feedback/?since=2009-06-01+13:00:00.
8
+ # The return value is application/json with the following structure:
9
+ #
10
+ class APN::Feedback < APN::Base
11
+ include AASM
12
+
13
+ named_scope :last_feedback_request, lambda {
14
+ { :conditions => "state = 'processed'", :order => "id DESC" }
15
+ }
16
+
17
+ #
18
+ # MODEL STATE MACHINE
19
+ #
20
+ aasm_initial_state :pending
21
+ aasm_column :state
22
+
23
+ aasm_state :pending
24
+ aasm_state :active
25
+ aasm_state :processed
26
+
27
+ aasm_event :pend do
28
+ transitions :from => [:active, :processed], :to => :pending
29
+ end
30
+
31
+ aasm_event :activate do
32
+ transitions :from => [:pending, :processed], :to => :active
33
+ end
34
+
35
+ aasm_event :process do
36
+ transitions :from => :active, :to => :processed
37
+ end
38
+
39
+ def run
40
+ raise "save feedback record before running" if self.new_record?
41
+ get_feedback { |results|
42
+
43
+ puts results.inspect
44
+ if results.code.to_i == 200
45
+ result = JSON.parse(results.body) # parse json results
46
+ result.each do |item| # iterate results and delete devices that have been deactivated
47
+ d = APN::Device.find_by_ua_token(item['device_token'])
48
+ d.deactivate! if d
49
+ end
50
+ self.process!
51
+ end
52
+ }
53
+ end
54
+
55
+ def get_feedback
56
+ self.activate!
57
+ # puts "APN::get_feedback"
58
+ # time = 1.day.ago.iso8601
59
+ time = last_feedback_time
60
+ # puts " since #{time}"
61
+ result = http_get("/api/device_tokens/feedback/?since=#{time}", nil, {}, true)
62
+ self.code = result.code.to_s
63
+ self.message = result.message.to_s
64
+ self.body = result.body.to_s
65
+
66
+ yield result if block_given?
67
+ end
68
+
69
+ def last_feedback_time
70
+ f = APN::Feedback.last_feedback_request.first
71
+ f.nil? ? Time.at(0).iso8601 : f.created_at.iso8601
72
+ end
73
+
74
+ end
@@ -0,0 +1,101 @@
1
+ # Represents the message you wish to send.
2
+ # An APN::Notification belongs to an APN::Device.
3
+ #
4
+ # Example:
5
+ # apn = APN::Notification.new
6
+ # apn.badge = 5
7
+ # apn.sound = 'my_sound.aiff'
8
+ # apn.alert = 'Hello!'
9
+ # apn.custom_properties = {:email_id => "6", :link => "http://google.com"}
10
+ # apn.device = APN::Device.find(1)
11
+ # apn.save
12
+ #
13
+ # To deliver call the following method:
14
+ # APN::Notification.process_pending
15
+ #
16
+
17
+ class APN::Notification < APN::Base
18
+ include AASM
19
+ include ::ActionView::Helpers::TextHelper
20
+ extend ::ActionView::Helpers::TextHelper
21
+
22
+ serialize :custom_properties
23
+
24
+ has_many :excluded_devices, :class_name => 'APN::ExcludedDevicesForNotification'
25
+ belongs_to :device, :class_name => 'APN::Device'
26
+
27
+ validates_presence_of :device, :on => :create, :message => "can't be blank"
28
+ validate_on_create :device_state
29
+
30
+ #
31
+ # MODEL STATE MACHINE
32
+ #
33
+ aasm_initial_state :pending
34
+ aasm_column :state
35
+
36
+ aasm_state :pending
37
+ aasm_state :processed, :enter => :update_sent_at
38
+ aasm_state :inactive_device
39
+
40
+ aasm_event :process do
41
+ transitions :from => :pending, :to => :processed, :guard => :check_response
42
+ end
43
+
44
+ aasm_event :set_to_inactive do
45
+ transitions :from => :pending, :to => :inactive_device
46
+ end
47
+
48
+ def apple_hash
49
+ result = {}
50
+ result['aps'] = {}
51
+ result['aps']['alert'] = self.alert if self.alert
52
+ result['aps']['badge'] = convert(self.badge) if self.badge
53
+ if self.sound
54
+ result['aps']['sound'] = self.sound if self.sound
55
+ end
56
+ if self.custom_properties
57
+ self.custom_properties.each do |key,value|
58
+ result["#{key}"] = "#{value}"
59
+ end
60
+ end
61
+ result['device_tokens'] = self.device.token_for_ua
62
+ result
63
+ end
64
+
65
+ def push
66
+ puts "process #{self.inspect}"
67
+ result = http_post("/api/push/", apple_hash, {}, true)
68
+ self.last_response_code = result.code
69
+ self.save
70
+ self.process!
71
+ end
72
+
73
+ def update_sent_at
74
+ self.sent_at = Time.now
75
+ end
76
+
77
+ def self.process_pending
78
+ self.pending.each do |n|
79
+ unless n.device.inactive?
80
+ n.push
81
+ end
82
+ end
83
+ end
84
+
85
+ def convert(input)
86
+ if input.to_i > 0 && input.to_i.to_s == input
87
+ return input.to_i
88
+ else
89
+ return input
90
+ end
91
+ end
92
+
93
+ def check_response
94
+ self.last_response_code == 200 ? true : false
95
+ end
96
+
97
+ def device_state
98
+ errors.add(:device, "must not be marked as inactive") if device.inactive?
99
+ end
100
+
101
+ end # APN::Notification
@@ -0,0 +1,21 @@
1
+
2
+
3
+ # ############################# this belongs in initializer
4
+ # require File.dirname(__FILE__) + '/libs/config'
5
+ # UA::Config::app_key = 'YOUR_APP_KEY'
6
+ # UA::Config::app_secret = 'YOUR_APP_SECRET'
7
+ # UA::Config::push_secret = 'YOUR_PUSH_SECRET'
8
+ # UA::Config::push_host = 'https://go.urbanairship.com'
9
+ # #############################
10
+
11
+ Dir.glob(File.join(File.dirname(__FILE__), 'app', 'models', 'apn', '*.rb')).sort.each do |f|
12
+ require f
13
+ end
14
+
15
+ %w{ models controllers helpers }.each do |dir|
16
+ path = File.join(File.dirname(__FILE__), 'app', dir)
17
+ $LOAD_PATH << path
18
+ # puts "Adding #{path}"
19
+ ActiveSupport::Dependencies.load_paths << path
20
+ ActiveSupport::Dependencies.load_once_paths.delete(path)
21
+ end
@@ -0,0 +1,3 @@
1
+ module UrbanairshipOnRails
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,22 @@
1
+ # desc "Explaining what the task does"
2
+ # task :urban_airship_on_rails do
3
+ # # Task goes here
4
+ # end
5
+
6
+ namespace :apn do
7
+ desc "retreive and process list of inactive devices"
8
+ task :feedback => [:environment] do
9
+ APN::Feedback.create().run
10
+ end
11
+
12
+ desc "send all pending notifications to devices"
13
+ task :push => [:environment] do
14
+ APN::Notification.process_pending
15
+ end
16
+
17
+ desc "send all pending broadcast notifications to devices"
18
+ task :broadcast_push => [:environment] do
19
+ APN::BroadcastNotification.process_pending
20
+ end
21
+
22
+ end
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+ require 'active_support/test_case'
@@ -0,0 +1,8 @@
1
+ require 'test_helper'
2
+
3
+ class UrbanAirshipOnRailsTest < ActiveSupport::TestCase
4
+ # Replace this with your real tests.
5
+ test "the truth" do
6
+ assert true
7
+ end
8
+ end
@@ -0,0 +1 @@
1
+ # Uninstall hook code here
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "urbanairship_on_rails/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "urbanairship_on_rails"
7
+ s.version = UrbanairshipOnRails::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["John Kassimatis"]
10
+ s.email = ["yonnage@gmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{URBAN AIRSHIP ON RAILS}
13
+ s.description = %q{push notifications}
14
+
15
+ s.rubyforge_project = "urbanairship_on_rails"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: urbanairship_on_rails
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - John Kassimatis
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-03-23 00:00:00 +02:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description: push notifications
18
+ email:
19
+ - yonnage@gmail.com
20
+ executables: []
21
+
22
+ extensions: []
23
+
24
+ extra_rdoc_files: []
25
+
26
+ files:
27
+ - Gemfile
28
+ - Gemfile.lock
29
+ - MIT-LICENSE
30
+ - README
31
+ - Rakefile
32
+ - generators/apn_migrations/apn_migrations_generator.rb
33
+ - generators/apn_migrations/templates/apn_migrations/001_create_apn_devices.rb
34
+ - generators/apn_migrations/templates/apn_migrations/002_create_apn_notifications.rb
35
+ - generators/apn_migrations/templates/apn_migrations/003_create_apn_feedbacks.rb
36
+ - generators/apn_migrations/templates/apn_migrations/004_create_apn_broadcast_notifications.rb
37
+ - generators/apn_migrations/templates/apn_migrations/005_change_badge_to_string_on_notifications.rb
38
+ - generators/apn_migrations/templates/apn_migrations/006_rename_errors_nb_to_last_error_code_on_notifications.rb
39
+ - generators/apn_migrations/templates/apn_migrations/007_rename_errors_nb_to_last_error_code_on_broadcast_notifications.rb
40
+ - generators/apn_migrations/templates/apn_migrations/008_add_custom_properties_on_notifications.rb
41
+ - generators/apn_migrations/templates/apn_migrations/009_add_custom_properties_on_broadcast_notifications.rb
42
+ - generators/apn_migrations/templates/apn_migrations/010_create_apn_excluded_devices_for_notifications.rb
43
+ - generators/apn_migrations/templates/apn_migrations/011_add_response_code_message_and_body_to_devices.rb
44
+ - generators/apn_migrations/templates/apn_migrations/012_add_last_inactive_at_to_devices.rb
45
+ - init.rb
46
+ - install.rb
47
+ - lib/urbanairship_on_rails.rb
48
+ - lib/urbanairship_on_rails/libs/config.rb
49
+ - lib/urbanairship_on_rails/models/apn/base.rb
50
+ - lib/urbanairship_on_rails/models/apn/broadcast_notification.rb
51
+ - lib/urbanairship_on_rails/models/apn/device.rb
52
+ - lib/urbanairship_on_rails/models/apn/excluded_devices_for_notification.rb
53
+ - lib/urbanairship_on_rails/models/apn/feedback.rb
54
+ - lib/urbanairship_on_rails/models/apn/notification.rb
55
+ - lib/urbanairship_on_rails/urbanairship_on_rails.rb
56
+ - lib/urbanairship_on_rails/version.rb
57
+ - tasks/urban_airship_on_rails_tasks.rake
58
+ - test/test_helper.rb
59
+ - test/urban_airship_on_rails_test.rb
60
+ - uninstall.rb
61
+ - urbanairship_on_rails.gemspec
62
+ has_rdoc: true
63
+ homepage: ""
64
+ licenses: []
65
+
66
+ post_install_message:
67
+ rdoc_options: []
68
+
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: "0"
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: "0"
83
+ requirements: []
84
+
85
+ rubyforge_project: urbanairship_on_rails
86
+ rubygems_version: 1.6.1
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: URBAN AIRSHIP ON RAILS
90
+ test_files:
91
+ - test/test_helper.rb
92
+ - test/urban_airship_on_rails_test.rb