urbanairship_on_rails 0.0.1

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